What changed, and why it matters
This is a large commit that adds and reorganizes automated integration tests for the Cake Wallet app. Most of the changes are test code, CI workflow files, and small app-side widget key additions so tests can find on-screen elements. There is one minor production bug fix buried in the history: a Solana token-decimals default was corrected so amounts parse correctly. The commit does not appear to introduce a security vulnerability; if anything it improves quality assurance by adding many regression tests and hardening CI behavior.
Treat as a normal QA/CI improvement commit. Review the Solana token-decimals fix in isolation to confirm it is correct and complete. Verify that the empty funded_wallets.dart stub is never populated in the repository and that FUNDS_SECRETS_FILE remains a protected repository secret. No security patch or incident response is indicated.
Security signals we found
Large test-only refactor with no obvious malicious code
Production-side changes are additive widget keys and one Solana decimals fix
CI now posts Slack reports and supports manual funds-spending tests with a default-off SPEND flag
No new secret handling beyond existing repository secrets and a stub funded_wallets.dart kept empty in git
No changes to cryptography, key storage, transaction signing, or network trust boundaries
Evidence from the diff
The commit introduces a full integration-test harness (BaseRobot, AppLauncher, TestConfig, TestWallets, flows, robots, tier0/tier1/funds suites), replaces hard sleeps with bounded waits, adds ValueKeys to many UI widgets, fixes AlertWithOneAction’s dropped buttonKey, and wires up declared-but-unattached navbar action keys. It also deletes stale workflows and Linux desktop integration-test steps, adds GitHub Actions workflows for emulator-based PR gating and manual funds tests, and includes a cherry-picked fix for token decimals defaulting to zero in Solana. The diff is overwhelmingly test/CI infrastructure; production code changes are limited to adding widget keys and the Solana decimals fix.
Changed components
integration_test/*.github/workflows/*lib/new-ui/*lib/src/widgets/*lib/src/screens/*cw_mweb/pubspec.yaml / generated_bindings.g.dartSolana token amount parsing (cherry-picked fix)Inspect captured patch +6024 / −4772
### .github/workflows/automated_integration_test.yml
@@ -1,487 +0,0 @@
-
-name: Automated Integration Tests
-
-# Temporarily disabled - uncomment to re-enable
-on:
- # push:
- # pull_request:
- # branches: [main]
- workflow_dispatch: # This is to manually trigger if needed
-defaults:
- run:
- shell: bash
-jobs:
- Automated_integration_test:
- runs-on: linux-amd64
- timeout-minutes: 90
- container:
- image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly
- env:
- STORE_PASS: test@cake_wallet
- KEY_PASS: test@cake_wallet
- MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
- BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
- ANDROID_AVD_HOME: /root/.android/avd
- volumes:
- - /opt/cw_cache_android/root/.cache:/root/.cache
- - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
- - /opt/cw_cache_android/root/.ccache:/root/.ccache
- - /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
- - /opt/cw_cache_android/root/.gradle/:/root/.gradle
- - /opt/cw_cache_android/root/.android/:/root/.android
- - /opt/cw_cache_android/root/go/pkg:/root/go/pkg
- - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
- - /dev/kvm:/dev/kvm
- strategy:
- matrix:
- api-level: [29]
-
- steps:
- - name: Fix github actions messing up $HOME...
- run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV'
- - uses: actions/checkout@v4
- with:
- ref: ${{ github.event.pull_request.head.sha || github.sha }}
- - name: configure git
- run: |
- git config --global --add safe.directory '*'
- git config --global user.email "ci@cakewallet.com"
- git config --global user.name "CakeWallet CI"
- - name: Get the full commit message
- run: |
- FULL_MESSAGE="$(git log -1 --pretty=%B)"
- echo "message<<EOF" >> $GITHUB_ENV
- echo "$FULL_MESSAGE" >> $GITHUB_ENV
- echo "EOF" >> $GITHUB_ENV
-
- - name: Add secrets
- run: |
- touch cw_bitcoin/lib/.secrets.g.dart
- touch cw_evm/lib/.secrets.g.dart
- touch cw_solana/lib/.secrets.g.dart
- touch cw_core/lib/.secrets.g.dart
- touch cw_nano/lib/.secrets.g.dart
- touch cw_tron/lib/.secrets.g.dart
-
- echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
- echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
- echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
- echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
- echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
- echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
- echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
-
- echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
- echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
-
- echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
- echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
-
- echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart
-
- echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart
-
- - name: prepare monero_c and cache
- run: |
- export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }')
- echo MONEROC_HASH=$MONEROC_HASH >> /etc/environment
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c"
- pushd scripts
- ln -s "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c"
- ./prepare_moneroc.sh
- popd
- pushd scripts/monero_c
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" || true
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" || true
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" || true
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" || true
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" || true
- mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" || true
-
- rm -rf "$PWD/contrib/depends/built" "$PWD/monero/contrib/depends/built" "$PWD/wownero/contrib/depends/built"
- rm -rf "$PWD/contrib/depends/sources" "$PWD/monero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources"
- mkdir -p contrib/depends || true
- ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" "$PWD/contrib/depends/built"
- ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" "$PWD/monero/contrib/depends/built"
- ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" "$PWD/wownero/contrib/depends/built"
- ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" "$PWD/contrib/depends/sources"
- ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" "$PWD/monero/contrib/depends/sources"
- ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources"
- popd
-
- - name: Generate KeyStore
- run: |
- pushd /opt/generic_cache
- if [[ ! -f key.jks ]];
- then
- keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS
- else
- echo "$PWD/key.jks exist, not generating"
- fi
- popd
- cp /opt/generic_cache/key.jks android/app
-
- - name: Execute Build and Setup Commands
- run: |
- pushd scripts/android
- source ./app_env.sh cakewallet
- ./app_config.sh
- popd
-
- - name: Build monero_c
- run: |
- pushd scripts/android/
- source ./app_env.sh cakewallet
- ./build_monero_all.sh
- popd
-
- - name: Install Flutter dependencies
- run: |
- flutter pub get
-
- - name: Build mwebd
- run: |
- set -x -e
- export MWEBD_HASH=$(cat scripts/android/build_mwebd.sh | grep 'git reset --hard' | xargs | awk '{ print $4 }')
- echo MWEBD_HASH=$MWEBD_HASH >> /etc/environment
- pushd scripts/android
- gomobile init;
- ./build_mwebd.sh
- popd
-
- - name: Build Decred
- run: |
- set -x -e
- pushd scripts/android
- ./build_decred.sh
- popd
-
- - name: Build generated code
- run: |
- flutter --version
- flutter pub get
- ./model_generator.sh
-
- - name: Generate key properties
- run: |
- dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS
-
- - name: Generate localization
- run: |
- dart run tool/generate_localization.dart
-
- - name: Rename app
- run: |
- sanitized_branch_name=${BRANCH_NAME#origin/} # Remove 'origin/' prefix if it exists
- sanitized_branch_name=${sanitized_branch_name:0:16} # Take only the first 16 characters
- sanitized_branch_name=$(echo "$sanitized_branch_name" | tr '[:upper:]' '[:lower:]') # Convert to lowercase
- sanitized_branch_name=$(echo "$sanitized_branch_name" | sed 's/[^a-z0-9]//g') # Remove all special characters
-
- echo -e "id=com.cakewallet.test_${sanitized_branch_name}\nname=${BRANCH_NAME}" > android/app.properties
-
- - name: Build
- run: |
- flutter build apk --dart-define=hasDevOptions=true --release --split-per-abi
-
- - name: Rename apk file
- run: |
- sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
- cd build/app/outputs/flutter-apk
- mkdir test-apk
-
- cp app-arm64-v8a-release.apk test-apk/${sanitized_branch_name}.apk
- cp app-x86_64-release.apk test-apk/${sanitized_branch_name}_x86.apk
- echo "APK files created: test-apk/${sanitized_branch_name}.apk and test-apk/${sanitized_branch_name}_x86.apk"
-
- - name: Find APK file
- id: find_apk
- run: |
- set -x
- # Read the sanitized branch name from the app.properties file
- sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
-
- echo "Looking for APK file: build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk"
- ls -la build/app/outputs/flutter-apk/test-apk/ || echo "test-apk directory not found"
- apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk || exit 1)
- echo "APK_FILE=$apk_file" >> $GITHUB_ENV
- echo "APK file found: $apk_file"
- ls -la "$apk_file"
-
- - name: 🔍 Debug System Environment
- run: |
- echo "=== System Information ==="
- uname -a
- echo "=== Container Info ==="
- cat /proc/version || echo "Cannot read /proc/version"
- echo "=== KVM Device Check ==="
- ls -la /dev/kvm || echo "KVM device not found"
- echo "=== udev Status ==="
- which udevadm || echo "udevadm not found"
- echo "=== Current User ==="
- whoami
- echo "=== Groups ==="
- groups
- echo "=== Environment ==="
- env | grep -E "(HOME|USER|PATH)" | sort
-
- - name: 🧹 Clean up existing emulators
- run: |
- echo "=== Cleaning up existing emulators ==="
- # Kill any existing emulator processes
- pkill -f emulator || echo "No emulator processes found"
- # Kill any existing adb processes
- pkill -f adb || echo "No adb processes found"
- # Wait a moment for processes to terminate
- sleep 2
- # Start adb server fresh
- adb start-server || echo "Failed to start adb server"
- # Kill adb server to ensure clean state
- adb kill-server || echo "Failed to kill adb server"
- echo "=== Cleanup complete ==="
-
- - name: 🔍 Verify AVD Configuration
- run: |
- echo "=== AVD Configuration Check ==="
-
- # Check if AVD directory exists
- echo "Checking AVD directory..."
- ls -la ~/.android/avd/ || echo "AVD directory not found"
-
- # List available AVDs
- echo "Available AVDs:"
- emulator -list-avds || echo "Failed to list AVDs"
-
- # Check Android SDK location
- echo "Android SDK location:"
- echo $ANDROID_HOME
- echo $ANDROID_SDK_ROOT
-
- # Check emulator binary
- echo "Emulator binary:"
- which emulator || echo "Emulator not found in PATH"
- emulator -version || echo "Failed to get emulator version"
-
- echo "=== AVD Check Complete ==="
-
- - name: 🦾 Enable KVM
- run: |
- echo "=== KVM Setup and Verification ==="
-
- # Check if KVM device exists
- if [ -e /dev/kvm ]; then
- echo "✅ KVM device found at /dev/kvm"
-
- # Check current permissions
- echo "Current KVM permissions:"
- ls -la /dev/kvm
-
- # Set proper permissions
- sudo chmod 666 /dev/kvm || echo "⚠️ Failed to set KVM permissions"
-
- # Verify permissions were set
- if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
- echo "✅ KVM is readable and writable"
- else
- echo "⚠️ KVM permissions may not be optimal"
- fi
-
- # Test KVM functionality
- echo "Testing KVM functionality..."
- if command -v kvm-ok >/dev/null 2>&1; then
- if kvm-ok 2>/dev/null; then
- echo "✅ KVM is working properly"
- else
- echo "⚠️ KVM check failed, but continuing..."
- fi
- else
- echo "ℹ️ kvm-ok command not available, testing manually..."
- # Manual KVM test
- if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
- echo "✅ KVM device is accessible"
- fi
- fi
-
- # Try to create udev rules (may fail in container, that's OK)
- echo "Setting up udev rules..."
- sudo mkdir -p /etc/udev/rules.d || echo "⚠️ Could not create udev directory"
- echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules || echo "⚠️ Could not create udev rule"
-
- # Try to reload udev rules (will likely fail in container, that's OK)
- sudo udevadm control --reload-rules || echo "ℹ️ udevadm control failed (expected in container)"
- sudo udevadm trigger --name-match=kvm || echo "ℹ️ udevadm trigger failed (expected in container)"
-
- else
- echo "❌ KVM device not found at /dev/kvm"
- echo "This will cause the emulator to run in software mode (slower)"
- fi
-
- # Check system resources
- echo "=== System Resources ==="
- echo "Memory:"
- free -h
- echo "Disk space:"
- df -h
- echo "CPU cores:"
- nproc
- echo "KVM module loaded:"
- lsmod | grep kvm || echo "KVM module not loaded (may be built-in)"
-
- echo "=== KVM Setup Complete ==="
-
- - name: 🦾 Cache gradle
- uses: gradle/actions/setup-gradle@v3
-
- - name: 🦾 Cache AVD
- uses: actions/cache@v4
- id: avd-cache
- with:
- path: |
- ~/.android/avd/*
- ~/.android/adb*
- key: avd-${{ matrix.api-level }}
-
- - name: 🔧 Prepare emulator utilities
- run: |
- # Create a helper script for emulator readiness checks
- cat > /tmp/emulator_ready.sh << 'EOF'
- #!/bin/bash
-
- echo "=== Emulator Readiness Check ==="
-
- # Wait for boot completion
- echo "1. Checking boot completion..."
- timeout 300 bash -c 'until adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; do sleep 5; echo " Waiting for boot completion..."; done'
- if [ $? -eq 0 ]; then
- echo "✅ Boot completed"
- else
- echo "❌ Boot completion timeout"
- echo "⚠️ Continuing anyway..."
- fi
-
- # Wait for input service
- echo "2. Checking input service..."
- timeout 60 bash -c 'until adb shell service list 2>/dev/null | grep -q "input"; do sleep 2; echo " Waiting for input service..."; done'
- if [ $? -eq 0 ]; then
- echo "✅ Input service ready"
- else
- echo "❌ Input service timeout"
- echo "⚠️ Continuing anyway..."
- fi
-
- # Wait for package manager
- echo "3. Checking package manager..."
- timeout 60 bash -c 'until adb shell pm list packages 2>/dev/null >/dev/null; do sleep 2; echo " Waiting for package manager..."; done'
- if [ $? -eq 0 ]; then
- echo "✅ Package manager ready"
- else
- echo "❌ Package manager timeout"
- echo "⚠️ Continuing anyway..."
- fi
-
- # Wait for settings service
- echo "4. Checking settings service..."
- timeout 60 bash -c 'until adb shell settings get global window_animation_scale 2>/dev/null >/dev/null; do sleep 2; echo " Waiting for settings service..."; done'
- if [ $? -eq 0 ]; then
- echo "✅ Settings service ready"
- else
- echo "❌ Settings service timeout"
- echo "⚠️ Continuing anyway..."
- fi
-
- echo "✅ Emulator readiness check completed"
- EOF
-
- chmod +x /tmp/emulator_ready.sh
-
- - name: 🦾 Create AVD and generate snapshot for caching
- if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@v2
- with:
- api-level: ${{ matrix.api-level }}
- force-avd-creation: false
- # arch: ${{ matrix.arch }}
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 1024 -no-snapshot-save
- working-directory: ${{ github.workspace }}
- disable-animations: false
- script: |
- echo "=== AVD Snapshot Generation ==="
-
- # Use the helper script to ensure emulator is fully ready
- /tmp/emulator_ready.sh
-
- # Manually disable animations with error handling
- echo "Disabling animations manually..."
- adb shell settings put global window_animation_scale 0.0 || echo "Failed to disable window animations"
- adb shell settings put global transition_animation_scale 0.0 || echo "Failed to disable transition animations"
- adb shell settings put global animator_duration_scale 0.0 || echo "Failed to disable animator duration"
-
- echo "=== Checking emulator status before termination ==="
- adb devices || echo "ADB devices check failed"
- echo "=== AVD snapshot generation complete ==="
-
- - name: 🚀 Integration tests on Android Emulator
- timeout-minutes: 60
- uses: reactivecircus/android-emulator-runner@v2
- with:
- api-level: ${{ matrix.api-level }}
- force-avd-creation: false
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 1024 -no-snapshot-save
- disable-animations: false
- working-directory: ${{ github.workspace }}
- script: |
- echo "=== Pre-test Environment Check ==="
- pwd
- ls -la integration_test_runner.sh || echo "integration_test_runner.sh not found"
-
- echo "=== Emulator Status Check ==="
- adb devices
-
- echo "=== Emulator Info ==="
- adb shell getprop ro.build.version.release || echo "Failed to get Android version"
- adb shell getprop ro.product.model || echo "Failed to get device model"
-
- echo "=== Checking Emulator Performance ==="
- adb shell cat /proc/cpuinfo | grep -i "model name" | head -1 || echo "Could not check CPU info"
- adb shell cat /proc/meminfo | grep -i "memtotal" || echo "Could not check memory info"
-
- echo "=== Checking Emulator Process ==="
- ps aux | grep emulator | grep -v grep || echo "Emulator process not found in ps output"
-
- echo "=== Making script executable ==="
- chmod a+rx integration_test_runner.sh
-
- echo "=== Running integration tests ==="
-
- echo "Final emulator readiness check..."
- /tmp/emulator_ready.sh || echo "Emulator readiness check failed, but continuing..."
-
- echo "Disabling animations for test performance..."
- adb shell settings put global window_animation_scale 0.0 || echo "Failed to disable window animations"
- adb shell settings put global transition_animation_scale 0.0 || echo "Failed to disable transition animations"
- adb shell settings put global animator_duration_scale 0.0 || echo "Failed to disable animator duration"
-
- echo "Starting integration test runner..."
- ./integration_test_runner.sh
- TEST_EXIT_CODE=$?
-
- if [ $TEST_EXIT_CODE -eq 0 ]; then
- echo "Integration tests completed successfully"
- else
- echo "Integration tests failed with exit code $TEST_EXIT_CODE"
- fi
-
- - name: 🧹 Post-test cleanup
- if: always()
- run: |
- echo "=== Post-test cleanup ==="
- # Kill any remaining emulator processes
- pkill -f emulator || echo "No emulator processes to kill"
- # Kill any remaining adb processes
- pkill -f adb || echo "No adb processes to kill"
- # Wait for processes to terminate
- sleep 2
- # Check for any remaining processes
- ps aux | grep -E "(emulator|adb)" | grep -v grep || echo "No remaining emulator/adb processes"
- echo "=== Cleanup complete ==="
- - name: cleanup
- run: rm -rf build/app/outputs/flutter-apk/test-apk/
### .github/workflows/funds_integration_tests.yml
@@ -0,0 +1,64 @@
+name: Funds Integration Tests
+
+permissions:
+ contents: read
+ packages: write
+
+# This test is never triggered automatically as it moves real funds
+on:
+ workflow_dispatch:
+ inputs:
+ flows:
+ description: "Which flows to run"
+ required: true
+ type: choice
+ options:
+ - all
+ - send
+ - swap
+ default: all
+ chains:
+ description: "Chains the send suites run on. auto discovers every funded chain, or a comma separated list of wallet type names"
+ required: true
+ type: string
+ default: auto
+ swap_from:
+ description: "Wallet type the swap deposits from. Empty takes the first funded chain"
+ required: false
+ type: string
+ default: ""
+ swap_to:
+ description: "Currency the swap receives, for example eth or btc. Empty leaves the swap page's own pairing"
+ required: false
+ type: string
+ default: ""
+ spend:
+ description: "Actually broadcast and create trades. Off runs the dry runs only"
+ required: true
+ type: boolean
+ default: false
+ record_screen:
+ description: "Record the emulator screen, uploaded GPG encrypted"
+ required: true
+ type: boolean
+ default: true
+
+concurrency:
+ group: funds-integration-tests
+ cancel-in-progress: false
+
+jobs:
+ funds-tests:
+ uses: ./.github/workflows/reusable-integration-test.yml
+ with:
+ environment: funds
+ ref: ${{ github.sha }}
+ suite_dir: integration_test/funds_suites
+ test_tier: all
+ write_funds_secrets: true
+ record_screen: ${{ inputs.record_screen }}
+ timeout_minutes: 120
+ slack_notify: true
+ extra_dart_defines: "FLOWS=${{ inputs.flows }};CHAINS=${{ inputs.chains }};SPEND=${{ inputs.spend }};SWAP_FROM=${{ inputs.swap_from }};SWAP_RECEIVE=${{ inputs.swap_to }}"
+ secrets: inherit
+
### .github/workflows/integration_tests.yml
@@ -0,0 +1,28 @@
+name: Integration Tests
+
+permissions:
+ contents: read
+ packages: write
+
+on:
+ pull_request:
+ branches: [dev]
+ paths-ignore:
+ - "**.md"
+ - "docs/**"
+ workflow_dispatch:
+
+concurrency:
+ group: integration-tests-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ android-integration-tests:
+ if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.fork == false
+ uses: ./.github/workflows/reusable-integration-test.yml
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ suite_dir: integration_test/suites
+ test_tier: tier0
+ slack_notify: true
+ secrets: inherit
### .github/workflows/pr_test_build_linux.yml
@@ -24,7 +24,6 @@ jobs:
KEY_PASS: test@cake_wallet
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
- DESKTOP_FORCE_MOBILE: Y
volumes:
- /opt/cw_cache_linux/root/.cache:/root/.cache
- /opt/cw_cache_linux/root/.ccache:/root/.ccache
@@ -61,14 +60,6 @@ jobs:
git config --global --add safe.directory '*'
git config --global user.email "ci@cakewallet.com"
git config --global user.name "CakeWallet CI"
-
- - name: Get the full commit message
- run: |
- FULL_MESSAGE="$(git log -1 --pretty=%B)"
- echo "message<<EOF" >> $GITHUB_ENV
- echo "$FULL_MESSAGE" >> $GITHUB_ENV
- echo "EOF" >> $GITHUB_ENV
-
- name: Add secrets
run: |
touch cw_bitcoin/lib/.secrets.g.dart
@@ -143,87 +134,11 @@ jobs:
path: ${{ github.workspace }}/build/linux/x64/release/cakewallet_linux.zip
name: cakewallet_linux
- - name: Prepare virtual desktop
- if: ${{ contains(env.message, 'run tests') }}
- run: |
- nohup Xvfb :99 -screen 0 720x1280x16 &
- echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV
- dbus-daemon --system --fork
- nohup NetworkManager &
- nohup ffmpeg -framerate 60 -video_size 720x1280 -f x11grab -i :99 -c:v libx264 -c:a aac /opt/screen_grab.mkv &
-
- # Note for people adding tests:
- # - Tests are ran on Linux, with some things being mocked out.
- # - Screen recording is being provided for the entire length of the test, you can download it in github articats.
- # - Screen recordeding is encrypted, look at step "Stop screen recording, encrypt and upload", and add your key if you want
- # Reason for encryption is the fact that we restore the wallet from seed, and we don't want to leak that, while there
- # isn't much in those wallets anyway, we still wouldn't like to leak it to anyone who is able to access github.
-
- - name: Test [confirm_seeds_flow_test]
- if: ${{ contains(env.message, 'run tests') }}
- timeout-minutes: 20
- run: |
- xmessage -timeout 30 "confirm_seeds_flow_test" &
- rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
- exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/confirm_seeds_flow_test.dart
-
- - name: Test [create_wallet_flow_test]
- if: ${{ contains(env.message, 'run tests') }}
- timeout-minutes: 20
- run: |
- xmessage -timeout 30 "create_wallet_flow_test" &
- rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
- exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/create_wallet_flow_test.dart
-
- # - name: Test [exchange_flow_test]
- # if: ${{ contains(env.message, 'run tests') }}
- # timeout-minutes: 20
- # run: |
- # xmessage -timeout 30 "exchange_flow_test" &
- # rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
- # exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/exchange_flow_test.dart
-
- - name: Test [restore_wallet_through_seeds_flow_test]
- if: ${{ contains(env.message, 'run tests') }}
- timeout-minutes: 20
- run: |
- xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &
- rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
- exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart
-
+ # Integration tests moved to integration_tests.yml
- name: Test [cw_core]
timeout-minutes: 15
run: cd cw_core && flutter test
- name: Test [cw_monero]
timeout-minutes: 15
run: cd cw_monero && flutter test
-
- - name: Stop screen recording, encrypt and upload
- if: always()
- run: |
- if [[ ! -f "/opt/screen_grab.mkv" ]];
- then
- exit 0;
- fi
- killall ffmpeg
- sleep 5
- killall -9 ffmpeg || true
- sleep 5
- # Feel free to add your own public key if you wish
- gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 6B3199AD9B3D23B8 # konstantin@cakewallet.com
- gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 35C8DBAFB8D9ACAC # cyjan@mrcyjanek.net
- gpg --trust-model always --encrypt --output /opt/screen_grab.mkv.gpg \
- --recipient 6B3199AD9B3D23B8 \
- --recipient 35C8DBAFB8D9ACAC \
- /opt/screen_grab.mkv
- rm /opt/screen_grab.mkv
- mv /opt/screen_grab.mkv.gpg ./screen_grab.mkv.gpg
-
- - name: Upload Artifact to github
- if: always()
- continue-on-error: true
- uses: actions/upload-artifact@v4
- with:
- path: ${{ github.workspace }}/screen_grab.mkv.gpg
- name: tests_screen_grab
### .github/workflows/reusable-build.yml
@@ -27,21 +27,15 @@ jobs:
KEY_PASS: test@cake_wallet
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
- ANDROID_AVD_HOME: /root/.android/avd
volumes:
- /opt/cw_cache_android/root/.cache:/root/.cache
- - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
- /opt/cw_cache_android/root/.ccache:/root/.ccache
- /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
- /opt/cw_cache_android/root/.gradle/:/root/.gradle
- /opt/cw_cache_android/root/.android/:/root/.android
- /opt/cw_cache_android/root/go/pkg:/root/go/pkg
- /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
- - /dev/kvm:/dev/kvm
- /var/run/docker.sock:/var/run/docker.sock
- strategy:
- matrix:
- api-level: [29]
steps:
- name: Fix github actions messing up $HOME...
### .github/workflows/reusable-integration-test.yml
@@ -0,0 +1,598 @@
+name: Reusable Integration Test Logic
+
+permissions:
+ contents: read
+ packages: write
+
+on:
+ workflow_call:
+ inputs:
+ ref:
+ required: true
+ type: string
+ suite_dir:
+ required: false
+ type: string
+ default: "integration_test/suites"
+ test_tier:
+ required: false
+ type: string
+ default: "tier0"
+ extra_dart_defines:
+ required: false
+ type: string
+ default: ""
+ environment:
+ required: false
+ type: string
+ default: ""
+ record_screen:
+ required: false
+ type: boolean
+ default: false
+ write_funds_secrets:
+ required: false
+ type: boolean
+ default: false
+ timeout_minutes:
+ required: false
+ type: number
+ default: 240
+ slack_notify:
+ required: false
+ type: boolean
+ default: false
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ integration-test:
+ runs-on: ubuntu-latest
+ environment: ${{ inputs.environment }}
+ timeout-minutes: ${{ inputs.timeout_minutes }}
+ env:
+ STORE_PASS: test@cake_wallet
+ KEY_PASS: test@cake_wallet
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.ref }}
+ - name: Free disk space
+ run: |
+ sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup /usr/local/lib/node_modules /opt/hostedtoolcache/CodeQL
+ sudo rm -rf /usr/share/swift /usr/local/share/powershell /usr/share/miniconda /opt/az /usr/local/graalvm
+ sudo docker image prune -af > /dev/null
+ df -h /
+
+ - name: Enable KVM
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ if [[ ! -c /dev/kvm ]]; then
+ echo "::error::/dev/kvm is not a character device, this runner has no usable KVM"
+ exit 1
+ fi
+
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Restore flutter sdk
+ id: flutter-cache
+ uses: actions/cache/restore@v4
+ with:
+ path: ~/flutter
+ key: flutter-sdk-3.41.9-${{ runner.os }}
+
+ - name: Install Flutter
+ run: |
+ if [[ ! -d "$HOME/flutter/bin" ]]; then
+ git clone --branch 3.41.9 --depth 1 https://github.com/flutter/flutter.git "$HOME/flutter"
+ fi
+ echo "$HOME/flutter/bin" >> $GITHUB_PATH
+ export PATH="$PATH:$HOME/flutter/bin"
+ flutter --version
+
+ - name: Save flutter sdk
+ if: steps.flutter-cache.outputs.cache-hit != 'true'
+ continue-on-error: true
+ uses: actions/cache/save@v4
+ with:
+ path: ~/flutter
+ key: flutter-sdk-3.41.9-${{ runner.os }}
+
+ - name: Setup gradle
+ uses: gradle/actions/setup-gradle@v3
+ with:
+ cache-read-only: false
+
+ - name: Restore pub packages
+ id: pub-cache
+ uses: actions/cache/restore@v4
+ with:
+ path: ~/.pub-cache
+ key: pub-cache-${{ runner.os }}-${{ hashFiles('pubspec_base.yaml', 'cw_*/pubspec.yaml') }}
+ restore-keys: pub-cache-${{ runner.os }}-
+
+ - name: Add rust android targets
+ run: |
+ rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Add secrets
+ run: |
+ touch cw_bitcoin/lib/.secrets.g.dart
+ touch cw_evm/lib/.secrets.g.dart
+ touch cw_solana/lib/.secrets.g.dart
+ touch cw_core/lib/.secrets.g.dart
+ touch cw_nano/lib/.secrets.g.dart
+ touch cw_tron/lib/.secrets.g.dart
+
+ echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
+ echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
+ echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
+ echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
+ echo "const blinkApiKey = '${{ secrets.BLINK_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
+ echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
+ echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
+ echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
+
+ echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
+ echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
+
+ echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
+ echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
+
+ echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart
+
+ echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart
+ - name: Add funded wallet secrets
+ if: ${{ inputs.write_funds_secrets }}
+ run: |
+ echo "${{ secrets.FUNDS_SECRETS_FILE }}" | base64 --decode > integration_test/core/funded_wallets.dart
+
+ - name: Build dependencies utilizing ghcr cache
+ run: |
+ env \
+ CW_DOCKER_REGISTRY=ghcr.io/cake-tech/cake_wallet \
+ CW_DOCKER_USE_CLOUD=true \
+ ./scripts/android/docker/build.sh
+
+ - name: Reclaim the space the dependency images took
+ run: |
+ docker system prune -af --volumes > /dev/null
+ df -h /
+
+ - name: Regenerate mweb bindings
+ run: |
+ git checkout -- cw_mweb/pubspec.yaml
+ rm -f cw_mweb/pubspec.lock
+ rm -rf cw_mweb/.dart_tool
+
+ # Hosted runners ship libclang, only install when it is actually missing
+ if ! ldconfig -p | grep -q libclang; then
+ sudo apt-get install -y --no-install-recommends libclang-dev > /dev/null
+ fi
+
+ if ! command -v protoc > /dev/null ||
+ [[ ! -f /usr/include/google/protobuf/descriptor.proto ]]; then
+ sudo apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev > /dev/null
+ fi
+
+ pushd cw_mweb
+ flutter pub get
+ dart run ffigen --config ffigen_config.yaml
+ popd
+
+ # Fail here in seconds rather than minutes into the gradle build
+ if ! grep -q "final class GoSlice" cw_mweb/lib/generated_bindings.g.dart; then
+ echo "::error::mweb bindings regenerated without class modifiers, ffigen resolved too old"
+ exit 1
+ fi
+
+ - name: Execute Build and Setup Commands
+ run: |
+ pushd scripts/android
+ source ./app_env.sh cakewallet
+ ./app_config.sh
+ popd
+
+ - name: Install Flutter dependencies
+ run: |
+ flutter pub get
+
+ - name: Save pub packages
+ if: steps.pub-cache.outputs.cache-hit != 'true'
+ continue-on-error: true
+ uses: actions/cache/save@v4
+ with:
+ path: ~/.pub-cache
+ key: pub-cache-${{ runner.os }}-${{ hashFiles('pubspec_base.yaml', 'cw_*/pubspec.yaml') }}
+
+ - name: Build generated code
+ run: |
+ flutter --version
+ ./model_generator.sh
+
+ - name: Generate KeyStore
+ run: |
+ keytool -genkey -v -keystore android/app/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS
+
+ - name: Generate key properties
+ run: |
+ dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS
+
+ - name: Generate localization
+ run: |
+ dart run tool/generate_localization.dart
+
+ - name: Compile graphics
+ run: |
+ ./compile_graphics.sh
+
+ - name: Compile assets/images graphics
+ run: |
+ while read -r dir; do
+ dart run vector_graphics_compiler --input-dir "$dir" --out-dir "$dir" > /dev/null &
+ done < <(find assets/images -type d)
+ wait
+
+ - name: Rename app
+ run: |
+ sanitized_branch_name=${BRANCH_NAME#origin/} # Remove 'origin/' prefix if it exists
+ sanitized_branch_name=${sanitized_branch_name:0:16} # Take only the first 16 characters
+ sanitized_branch_name=$(echo "$sanitized_branch_name" | tr '[:upper:]' '[:lower:]') # Convert to lowercase
+ sanitized_branch_name=$(echo "$sanitized_branch_name" | sed 's/[^a-z0-9]//g') # Remove all special characters
+
+ echo -e "id=com.cakewallet.test_${sanitized_branch_name}\nname=${BRANCH_NAME}" > android/app.properties
+
+ - name: Prime test build
+ env:
+ SUITE_DIR_INPUT: ${{ inputs.suite_dir }}
+ TEST_TIER_INPUT: ${{ inputs.test_tier }}
+ run: |
+ search_dir="$SUITE_DIR_INPUT"
+ if [[ "$TEST_TIER_INPUT" != "all" ]]; then
+ search_dir="$search_dir/$TEST_TIER_INPUT"
+ fi
+ first_suite=$(find "$search_dir" -name "*_test.dart" -type f | sort | head -1)
+ echo "Priming build with $first_suite"
+ flutter build apk --debug --target-platform android-x64 --target="$first_suite" --dart-define=CI_BUILD=true
+
+ - name: Cap the gradle heap for the test phase
+ run: |
+ sed -i 's/^org.gradle.jvmargs=.*/org.gradle.jvmargs=-Xmx3072M/' android/gradle.properties
+ grep '^org.gradle.jvmargs' android/gradle.properties
+ pkill -f GradleDaemon || echo "No gradle daemon to stop"
+ free -h || true
+ df -h / || true
+
+ - name: Integration tests on Android Emulator
+ uses: reactivecircus/android-emulator-runner@v2
+ env:
+ RECORD_SCREEN: ${{ inputs.record_screen }}
+ ENCRYPT_LOGS: ${{ inputs.write_funds_secrets }}
+ SUITE_DIR_INPUT: ${{ inputs.suite_dir }}
+ EXTRA_DART_DEFINES_INPUT: ${{ inputs.extra_dart_defines }}
+ with:
+ api-level: 29
+ arch: x86_64
+ profile: pixel_5
+ force-avd-creation: false
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 3072 -cores 4 -no-snapshot-save
+ disable-animations: false
+ working-directory: ${{ github.workspace }}
+ script: bash scripts/ci/run_integration_tests.sh ${{ inputs.test_tier }}
+
+ - name: Upload test logs
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: integration-test-logs
+ path: |
+ ${{ github.workspace }}/${{ inputs.test_tier }}_run.log
+ ${{ github.workspace }}/${{ inputs.test_tier }}_run.log.gpg
+ ${{ github.workspace }}/${{ inputs.test_tier }}_logcat.txt
+ ${{ github.workspace }}/${{ inputs.test_tier }}_logcat.txt.gpg
+ ${{ github.workspace }}/recordings/*.gpg
+
+ - name: Integration tests on Android Emulator (tier1, non-blocking)
+ if: ${{ inputs.test_tier == 'tier0' }}
+ continue-on-error: true
+ uses: reactivecircus/android-emulator-runner@v2
+ env:
+ RECORD_SCREEN: ${{ inputs.record_screen }}
+ ENCRYPT_LOGS: ${{ inputs.write_funds_secrets }}
+ SUITE_DIR_INPUT: ${{ inputs.suite_dir }}
+ EXTRA_DART_DEFINES_INPUT: ${{ inputs.extra_dart_defines }}
+ with:
+ api-level: 29
+ arch: x86_64
+ profile: pixel_5
+ force-avd-creation: false
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -read-only -memory 3072 -cores 4 -no-snapshot-save
+ disable-animations: false
+ working-directory: ${{ github.workspace }}
+ script: bash scripts/ci/run_integration_tests.sh tier1
+
+ - name: Upload tier1 logs
+ if: ${{ always() && inputs.test_tier == 'tier0' }}
+ continue-on-error: true
+ uses: actions/upload-artifact@v4
+ with:
+ name: integration-test-logs-tier1
+ path: |
+ ${{ github.workspace }}/tier1_run.log
+ ${{ github.workspace }}/tier1_run.log.gpg
+ ${{ github.workspace }}/tier1_logcat.txt
+ ${{ github.workspace }}/tier1_logcat.txt.gpg
+
+ # Superseded runs are cancelled on purpose, reporting those would be noise
+ - name: Report the run to slack
+ if: ${{ !cancelled() && inputs.slack_notify }}
+ continue-on-error: true
+ env:
+ SLACK_TOKEN: ${{ secrets.SLACK_TESTS_TOKEN }}
+ SLACK_CHANNEL: ${{ secrets.SLACK_TESTS_CHANNEL }}
+ BRANCH: ${{ github.head_ref || github.ref_name }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
+ ACTOR: ${{ github.actor }}
+ REPO_URL: ${{ github.server_url }}/${{ github.repository }}
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ DART_DEFINES: ${{ inputs.extra_dart_defines }}
+ run: |
+ if [[ -z "$SLACK_TOKEN" || -z "$SLACK_CHANNEL" ]]; then
+ echo "::notice::No slack token or channel configured, skipping the report"
+ exit 0
+ fi
+
+ fmt() {
+ printf '%dm %02ds' $(( $1 / 60 )) $(( $1 % 60 ))
+ }
+
+ field() {
+ awk -F= -v key="$2" '$1 == key { print $2; exit }' "$1"
+ }
+
+ tier_line() {
+ local file="$1" label="$2" note="$3"
+
+ if [[ ! -f "$file" ]]; then
+ printf '*%s*\n%s' "$label" "$note"
+ return
+ fi
+
+ printf '*%s*\n%s/%s passed in %s' "$label" \
+ "$(field "$file" passed)" "$(field "$file" total)" \
+ "$(fmt "$(field "$file" duration)")"
+ }
+
+ # For scenarios where the suite never ran because of driver issues,
+ # we can't call it a failing test, so we just log it
+ failure_lines() {
+ local file="$1" label="$2"
+
+ [[ -f "$file" ]] || return 0
+
+ while IFS= read -r entry; do
+ [[ -n "$entry" ]] || continue
+
+ local rest="${entry#*|}"
+ local name="${entry%%|*}"
+ local duration="${rest%%|*}"
+ local reason="${rest#*|}"
+
+ if [[ "$reason" == "void" ]]; then
+ printf '\n• `%s` (%s) _%s, driver never attached, the suite did not run_' \
+ "$name" "$(fmt "$duration")" "$label"
+ else
+ printf '\n• `%s` (%s) _%s_' "$name" "$(fmt "$duration")" "$label"
+ fi
+ done < <(sed -n 's/^fail=//p' "$file")
+ }
+
+ # Adding the results for the funds suites to the report too, so we know if we need to topup
+ chain_lines() {
+ local status_wanted="$1" icon="$2" file
+ local seen=""
+
+ for file in *_summary.txt; do
+ [[ -f "$file" ]] || continue
+
+ while IFS= read -r entry; do
+ [[ -n "$entry" ]] || continue
+
+ local chain="${entry%%|*}"
+ local rest="${entry#*|}"
+ local status="${rest%%|*}"
+ local detail="${rest#*|}"
+
+ [[ "$status" == "$status_wanted" ]] || continue
+ [[ "$seen" == *"[$chain]"* ]] && continue
+ seen="$seen[$chain]"
+
+ printf '\n%s *%s* _%s_' "$icon" "$chain" "$detail"
+ done < <(sed -n 's/^chain=//p' "$file")
+ done
+ }
+
+ pass_names() {
+ local file="$1" label="$2"
+
+ [[ -f "$file" ]] || return 0
+
+ printf '*%s* (%s)' "$label" "$(field "$file" passed)"
+
+ while IFS= read -r entry; do
+ [[ -n "$entry" ]] || continue
+ printf '\n• %s _%s_' "${entry%%|*}" "$(fmt "${entry##*|}")"
+ done < <(sed -n 's/^pass=//p' "$file")
+ }
+
+ tier0_failed=1
+ tier1_failed=0
+ total_passed=0
+
+ if [[ -f all_summary.txt ]]; then
+ tier0_failed=$(field all_summary.txt failed)
+ else
+ [[ -f tier0_summary.txt ]] && tier0_failed=$(field tier0_summary.txt failed)
+ [[ -f tier1_summary.txt ]] && tier1_failed=$(field tier1_summary.txt failed)
+ fi
+
+ for file in tier0_summary.txt tier1_summary.txt; do
+ [[ -f "$file" ]] || continue
+ total_passed=$(( total_passed + $(field "$file" passed) ))
+ done
+
+ if (( tier0_failed > 0 )); then
+ headline=":x: Integration tests failed"
+ elif (( tier1_failed > 0 )); then
+ headline=":warning: Gate passed, tier1 has failures"
+ else
+ headline=":white_check_mark: Integration tests passed"
+ fi
+
+ if (( tier0_failed > 0 )); then
+ tier1_note="_skipped, tier0 failed_"
+ else
+ tier1_note="_did not run_"
+ fi
+
+ where="<$REPO_URL/tree/$BRANCH|$BRANCH>"
+ if [[ -n "$PR_NUMBER" ]]; then
+ where="$where · <$REPO_URL/pull/$PR_NUMBER|#$PR_NUMBER ${PR_TITLE:-}>"
+ fi
+
+ failures=$(printf '%s%s%s' \
+ "$(failure_lines tier0_summary.txt tier0)" \
+ "$(failure_lines tier1_summary.txt tier1)" \
+ "$(failure_lines all_summary.txt suites)")
+
+ if [[ -f all_summary.txt ]]; then
+ primary=$(tier_line all_summary.txt suites '')
+ secondary=""
+ else
+ primary=$(tier_line tier0_summary.txt tier0 '_did not run_')
+ secondary=$(tier_line tier1_summary.txt tier1 "$tier1_note")
+ fi
+
+ chains=$(printf '%s%s%s%s' \
+ "$(chain_lines ok ':white_check_mark:')" \
+ "$(chain_lines empty ':moneybag:')" \
+ "$(chain_lines waiting ':hourglass:')" \
+ "$(chain_lines failed ':x:')")
+
+ needs_funding=$(chain_lines empty ':moneybag:')
+ too_slow=$(chain_lines waiting ':hourglass:')
+
+ blocks=$(jq -n \
+ --arg headline "$headline" \
+ --arg where "$where" \
+ --arg primary "$primary" \
+ --arg secondary "$secondary" \
+ --arg failures "$failures" \
+ --arg chains "$chains" \
+ --arg funding "$needs_funding" \
+ --arg slow "$too_slow" \
+ --arg defines "$(printf '%s' "$DART_DEFINES" | tr ';' ' ')" \
+ --arg commit "${COMMIT:0:8}" \
+ --arg actor "$ACTOR" \
+ --arg repo "$REPO_URL" \
+ --arg run "$RUN_URL" \
+ '[
+ { type: "header", text: { type: "plain_text", text: $headline, emoji: true } },
+ { type: "section", text: { type: "mrkdwn", text: $where } },
+ { type: "section", fields: (
+ [ { type: "mrkdwn", text: $primary } ]
+ + (if $secondary == "" then [] else
+ [ { type: "mrkdwn", text: $secondary } ] end)
+ ) }
+ ]
+ + (if $failures == "" then [] else
+ [ { type: "section", text: { type: "mrkdwn",
+ text: ("*Did not pass*" + $failures) } } ]
+ end)
+ + (if $chains == "" then [] else
+ [ { type: "divider" },
+ { type: "section", text: { type: "mrkdwn",
+ text: ("*Chains*" + $chains) } } ]
+ end)
+ + (if $funding == "" then [] else
+ [ { type: "context", elements: [ { type: "mrkdwn",
+ text: ":moneybag: The chains above could not cover a send. Top those wallets up and re run the funds workflow, nothing there needs debugging :)." } ] } ]
+ end)
+ + (if $slow == "" then [] else
+ [ { type: "context", elements: [ { type: "mrkdwn",
+ text: ":hourglass: Still rescanning when the clock ran out, so nothing was proven either way and the run is not red for them." } ] } ]
+ end)
+ + [
+ { type: "context", elements: [
+ { type: "mrkdwn",
+ text: ("<" + $repo + "/commit/" + $commit + "|" + $commit + "> · " + $actor
+ + (if $defines == "" then "" else " · `" + $defines + "`" end)) }
+ ] },
+ { type: "actions", elements: [
+ { type: "button", url: $run,
+ text: { type: "plain_text", text: "View run", emoji: false } }
+ ] }
+ ]')
+
+ response=$(curl -sS -X POST https://slack.com/api/chat.postMessage \
+ -H "Authorization: Bearer $SLACK_TOKEN" \
+ -H "Content-type: application/json; charset=utf-8" \
+ --data "$(jq -n --arg channel "$SLACK_CHANNEL" --arg text "$headline" \
+ --argjson blocks "$blocks" \
+ '{channel: $channel, text: $text, blocks: $blocks}')")
+
+ if [[ "$(jq -r '.ok' <<< "$response")" != "true" ]]; then
+ error=$(jq -r '.error // "unknown"' <<< "$response")
+ needed=$(jq -r '.needed // empty' <<< "$response")
+
+ if [[ -n "$needed" ]]; then
+ echo "::warning::Slack rejected the report: $error, it wants the $needed scope"
+ else
+ echo "::warning::Slack rejected the report: $error"
+ fi
+
+ exit 0
+ fi
+
+ thread_ts=$(jq -r '.ts // empty' <<< "$response")
+
+ if [[ -z "$thread_ts" ]] || (( total_passed == 0 )); then
+ exit 0
+ fi
+
+ # So that the channel is not clutter (and uglyyyy), we put everything that passed in the thread
+ # and the short version is the one that shows up in the channel
+ passed=$(printf '%s\n\n%s' \
+ "$(pass_names tier0_summary.txt tier0)" "$(pass_names tier1_summary.txt tier1)")
+
+ curl -sS -X POST https://slack.com/api/chat.postMessage \
+ -H "Authorization: Bearer $SLACK_TOKEN" \
+ -H "Content-type: application/json; charset=utf-8" \
+ --data "$(jq -n --arg channel "$SLACK_CHANNEL" --arg ts "$thread_ts" --arg text "$passed" \
+ '{channel: $channel, thread_ts: $ts, text: $text}')" > /dev/null
+
+ - name: Post-test cleanup
+ if: always()
+ run: |
+ pkill -f emulator || echo "No emulator processes to kill"
+ pkill -f adb || echo "No adb processes to kill"
### docs/INTEGRATION_TESTS.md
@@ -0,0 +1,306 @@
+# Integration tests
+
+End to end tests that drive the real app on a device. They run on an Android emulator in CI
+for every pull request into `dev`, and the funds suites run manually before releases.
+
+## Architecture
+
+Four layers, each one only talks to the layer below it:
+
+```
+suites/ the runnable tests, one file per user journey
+ tier0/ offline capable, no network assertions, the hard PR gate
+ tier1/ needs live network (nodes, swap providers), non blocking on PRs
+funds_suites/ moves real funds, only ever run by the manual funds workflow
+flows/ multi screen journeys shared between suites (onboarding, auth, wallet list)
+robots/ one class per screen, knows that screen's keys and assertions
+core/ the harness: BaseRobot primitives, AppLauncher, TestConfig, TestWallets
+```
+
+New robots extend `BaseRobot`. The older onboarding robots still hold a `CommonTestCases`
+instead, which is the same idea with a weaker waiting story, migrate one over whenever you
+touch it.
+
+Supporting pieces:
+
+- `test_driver/integration_test.dart` is the standard flutter drive driver.
+- `integration_test_runner.sh` discovers and runs suites, see Running locally.
+- `.github/workflows/integration_tests.yml` is the PR gate,
+ `reusable-integration-test.yml` holds the shared build and emulator pipeline.
+
+## Running locally
+
+Prerequisites, one time:
+
+1. A generated `lib/.secrets.g.dart` with the `*TestWalletSeeds` values populated.
+2. App config for the cakewallet flavor:
+ `cd scripts/android && source ./app_env.sh cakewallet && ./app_config.sh`
+3. Rename the app, every run wipes its data between suites:
+
+ ```
+ printf 'id=com.cakewallet.test_local\nname=local\n' > android/app.properties
+ ```
+
+ Step 2 writes the real `com.cakewallet.cake_wallet` id, and clearing that package takes
+ your own wallets with it. CI does this same rename after app_config for the same reason.
+ The runner refuses to clear a package that is not named for testing.
+4. Compiled vector assets, both passes:
+
+ ```
+ ./compile_graphics.sh
+ while read -r dir; do
+ dart run vector_graphics_compiler --input-dir "$dir" --out-dir "$dir"
+ done < <(find assets/images -type d)
+ ```
+
+ `compile_graphics.sh` does not cover `assets/images`, and those `.svg.vec` files are
+ neither committed nor ignored, so a fresh checkout has none of them. Without this the
+ app throws "Unable to load asset" on the welcome screen and the first suite fails.
+5. Native libs in `android/app/src/main/jniLibs/` for monero, wownero and zano
+ (prebuilts from the pinned monero_c release).
+6. `~/.cargo/bin` on PATH so gradle can build the breez rust crates, and `protoc` installed
+ (`brew install protobuf`), the spark crate generates its protobuf bindings at build time.
+
+If the build sits at 0% CPU for minutes, close Android Studio or stop its Gradle daemon,
+two daemons on this project deadlock on the same locks.
+
+Re-run steps 2 to 4 after every merge from dev, then `flutter pub get` at the root and the
+codegen for `lib/`. The chain proxies in `lib/<chain>/<chain>.dart` are generated and
+gitignored, so a merge that adds a call like `bitcoin!.hasSelectedLightning` leaves them
+behind and the build fails on a method that looks like it should exist. Step 2 also
+rewrites `android/app.properties` with the real app id, so redo step 3 alongside it, and it
+finishes by rewriting `pubspec.yaml` after its own last pub get, which is why the root pub
+get has to come after it. Skipping step 4 shows up as "Unable to load asset" on whichever
+screen dev added an icon to, one suite in, rather than at build time.
+
+Run one suite against a booted emulator:
+
+```
+flutter drive --driver=test_driver/integration_test.dart \
+ --target=integration_test/suites/tier0/onboarding_create_test.dart \
+ --dart-define=CI_BUILD=true -d <device-id>
+```
+
+Run a whole tier through the runner:
+
+```
+TEST_TIER=tier0 FLUTTER_DEVICE=<device-id> ./integration_test_runner.sh
+```
+
+`SUITE_DIR` also takes a single file, which is what you want while iterating on one suite:
+
+```bash
+SUITE_DIR=integration_test/suites/tier0/fiat_currency_test.dart ./integration_test_runner.sh
+```
+
+Runner knobs (environment variables): `SUITE_DIR`, `TEST_TIER` (tier0, tier1, all),
+`PLATFORM` (android, linux, macos, auto), `FLUTTER_DEVICE`, `RETRY_COUNT`,
+`EXTRA_DART_DEFINES`, `REMOVE_DATA_DIRECTORY=N` to keep app data between suites,
+`REMOVE_DESKTOP_DATA=Y` to let a desktop run wipe the wallet directories it shares with the
+installed app, `VOID_GRACE` for how long a driver that cannot attach is given to recover.
+
+Test knobs (dart defines): `TEST_WALLET_TYPES=all` runs every available wallet type,
+a comma separated list of type names runs just those, unset runs the representative set
+(solana, ethereum, bitcoin, monero).
+
+## Which tier does my test go in
+
+- Creates wallets, navigates screens, checks local state: **tier0**.
+- Needs a node, a provider API or any live network response: **tier1**.
+- Signs and broadcasts a real transaction: **funds_suites**, never anywhere else.
+
+tier0 blocks merges. tier1 runs on every PR but reports without blocking until it has a
+stable history. funds_suites run only from the manual funds workflow.
+
+## What is covered today
+
+Twenty seven suites. Add a line here when you add one.
+
+### tier0, no network needed
+
+| Suite | What it proves |
+| --- | --- |
+| `onboarding_create_test` | A wallet can be created for every configured type, and the app lands on the dashboard with that wallet open |
+| `onboarding_restore_test` | A wallet restores from seed for every configured type, and the restored wallet reports the seed it was given |
+| `invalid_seed_test` | A seed the wallet cannot parse leaves the user on the restore screen with nothing created |
+| `duplicate_wallet_name_test` | A name already taken is refused instead of quietly overwriting the wallet holding it |
+| `wallet_group_test` | A wallet added to an existing seed shares that seed, and both wallets stay reachable through the group |
+| `wallet_rename_test` | A wallet can be renamed, and cannot be renamed onto a name another wallet already holds |
+| `wallet_delete_test` | Deleting a wallet takes it off the list and out of storage, and leaves the others alone |
+| `receive_address_test` | The receive sheet shows an address the opened wallet owns, not a leftover from the previous one |
+| `seed_confirmation_test` | The seed and keys the app displays are the ones the opened wallet holds |
+| `show_keys_auth_test` | The seed and keys page turns away a wrong pin and opens for the right one |
+| `change_pin_test` | A changed pin is the one that opens what the old pin used to guard |
+| `send_validation_test` | The send screen refuses an empty address, a malformed one, an empty amount and more than the balance |
+| `address_book_test` | A saved contact keeps the address it was given |
+| `send_from_address_book_test` | A contact picked on the send screen fills in the address that was saved for it |
+| `settings_nav_test` | Every settings row opens its page and backs out of it |
+| `language_test` | Changing the language changes what the settings screen shows |
+| `fiat_currency_test` | Turning the fiat api off hides the currency setting, turning it back on returns it and the currency can be changed |
+
+### tier1, needs a node or a provider
+
+| Suite | What it proves |
+| --- | --- |
+| `sync_status_test` | A restored wallet reaches a node and starts syncing rather than sitting at connecting |
+| `node_switching_test` | Confirming another node repoints the wallet at it |
+| `transaction_history_test` | A wallet with known transactions renders them, both in the home preview and the full list |
+| `transaction_details_test` | Tapping a transaction opens the details of that transaction and not another one |
+| `swap_quote_test` | The swap sheet reaches a provider and returns a real quote, no trade is created |
+| `monero_legacy_seed_test` | A 25 word monero seed restores through the legacy path, which takes a seed type and a restore height the polyseed path never asks for |
+
+### funds_suites, manual workflow only
+
+| Suite | What it proves |
+| --- | --- |
+| `send_dry_run_test` | Every funded chain builds and signs a transaction, then throws it away without broadcasting |
+| `send_funds_test` | A real self transaction is broadcast on every funded chain |
+| `swap_dry_run_test` | A real deposit is priced end to end, stopping before the trade is created |
+| `swap_funds_test` | A swap is created and its deposit broadcast |
+
+The two dry runs need funded seeds because a quote and a signed transaction both depend on a
+real balance, but neither spends anything. `SPEND=true` is what lets the two that do spend
+past their guard.
+
+## Adding a test for your feature
+
+1. Give the widgets your test touches stable keys, inline in the widget:
+ `key: ValueKey('<page>_<element>_key')`. Keys go on the widget the test taps or reads,
+ never on AnimatedSwitcher children where the key is the animation identity.
+2. Create or extend the robot for the screen in `robots/`. One robot per screen, it
+ extends `BaseRobot` and implements `isDisplayed()`. Robots own every finder for their
+ screen, suites never call `find` directly.
+3. If the journey spans several screens, add a flow to `flows/` composing the robots.
+4. Copy `integration_test/templates/example_feature_test.dart.example` into the right
+ tier directory, rename it to `<feature>_test.dart` and fill it in. The runner only
+ picks up files ending in `_test.dart`.
+5. Run it twice locally against an emulator before pushing.
+6. Add it to the table above.
+
+## Harness rules
+
+- Suites use `integrationTest` from `core/app_launcher.dart`, not `testWidgets`. It contains
+ background async errors from app networking that would otherwise fail the test, awaited
+ assertion failures still fail the test normally.
+- No `sleep` or fixed delays, use `pumpUntilFound` and `pumpUntil` with a timeout.
+- No `pumpAndSettle` in new code, screens with endless animations never settle,
+ use the bounded `settle()` from BaseRobot.
+- No `find.text` on localized strings, keys only. Wallet names and addresses are data,
+ matching on those is fine.
+- `.secrets.g.dart` is only imported by `core/test_wallets.dart`.
+- Every suite starts from a fresh install, the runner wipes app data between suites.
+- Reading a view model through the page widget for a state assertion is fine, driving
+ the app through view models instead of the UI is not.
+
+## Secrets
+
+Test wallet seeds and receive addresses live in `lib/.secrets.g.dart` under names like
+`solanaTestWalletSeeds`, defined in `tool/utils/secret_key.dart` and reachable through
+`TestWallets`. CI injects the real values from the `MAIN_SECRETS_FILE` repository secret.
+
+Adding a new test secret means adding it in `tool/utils/secret_key.dart`, mapping it in
+`TestWallets` and asking the team lead to add the value to the CI secret.
+
+The slack report uses two more repository secrets, `SLACK_TESTS_TOKEN` and
+`SLACK_TESTS_CHANNEL`, which is a channel id rather than a webhook. Both the PR gate and
+the funds workflow report through them, so every test result lands in one channel.
+
+That is a separate slack app from the one behind `SLACK_APP_TOKEN`, which uploads apks to
+the builds channel. They are split so each token carries only what it needs, `chat:write`
+for this one and the file scopes for that one, and so the name on a message says which of
+the two sent it. A post coming back `missing_scope` means the app is short the
+`chat:write` scope or was never reinstalled after it was added.
+
+## Funds suites
+
+`funds_suites/` restore funded wallets and move real funds: a small self send on every
+funded chain, and one real swap with its deposit broadcast. They only run from the manual
+`Funds Integration Tests` workflow, which the team lead dispatches from the Actions tab
+before a release. The job runs in the `funds` environment, so add required reviewers to that
+environment under Settings, Environments, or a dispatch still starts without an approval.
+
+Funded seeds live in the `FUNDS_SECRETS_FILE` secret, a base64 encoded replacement for
+`integration_test/core/funded_wallets.dart` mapping wallet type names to that chain's
+funded wallets, at least two per chain. A wallet with a passphrase carries it and the restore
+flow ticks the passphrase box and enters it. A wallet without one leaves it out:
+
+```dart
+import "funded_wallet.dart";
+
+const Map<String, List<FundedWallet>> fundedWallets = {
+ "solana": [
+ FundedWallet(seed: "first wallet seed words ...", passphrase: "first passphrase"),
+ FundedWallet(seed: "second wallet seed words ...", passphrase: "second passphrase"),
+ ],
+ "ethereum": [
+ FundedWallet(seed: "seed words ..."),
+ ],
+};
+```
+
+A wrong passphrase restores an empty derivation, so it shows up the same way a drained
+wallet does, as "no spendable balance".
+
+The suites restore a chain's funded wallets one by one and use the first that shows a
+spendable balance, a wallet that finishes syncing while still empty counts as drained.
+A chain where every funded wallet is empty fails with a message asking for a top up.
+
+The checked in default is an empty map and must stay empty. Only the funds workflow turns
+on `write_funds_secrets`, and that input is the one thing keeping funded seeds out of a PR
+build now that no environment scopes the secret. In auto mode the suites discover every
+chain present in the map, so funding a new chain only means adding its entry to the
+secret. The dispatch inputs narrow a run: `flows` picks send, swap or both, `chains` lists
+the chains the send suites run on, `swap_from` picks the wallet type the swap deposits from
+(empty takes the first funded chain) and `swap_to` the currency it receives (empty leaves
+the swap page's own pairing, which is XMR for everything but a monero wallet). Whatever
+`swap_to` names must have a fixture address in `TestWallets.receiveAddressFor`, that is
+where the payout goes. Per chain send amounts are tuned in `TestConfig._fundsSendAmounts`.
+The swap suite enters an amount just above the provider minimum, the deposit wallet's
+balance has to cover that minimum plus fees.
+
+## How CI runs them
+
+The PR gate runs on GitHub hosted runners, the self-hosted android builders have no
+usable KVM so the emulator cannot start there. Moving back is a one line change to
+`runs-on` in `reusable-integration-test.yml` once a KVM capable builder exists.
+
+tier0 and tier1 each get their own emulator step and upload their logs separately, so a
+runner that dies during tier1 cannot take the gate's own results with it. The gradle heap
+is capped before the test phase because the daemon and the emulator together were
+exhausting the runner.
+
+Every run posts to slack. The message names each tier's counts, its duration and any suite
+that failed, and the full list of what passed goes in a reply on the same message so the
+channel keeps the short version. A suite that failed because the driver never attached is
+labelled as such rather than reported as a failing test, since in that case the suite never
+ran at all. It needs `SLACK_TESTS_TOKEN` and `SLACK_TESTS_CHANNEL`, and
+without either the step logs a notice and skips rather than failing the gate. Cancelled
+runs stay quiet, since superseded runs are cancelled on purpose. The report is built from
+the `SUMMARY_FILE` each tier writes, so anything the runner counts is available to it.
+
+Two environment quirks are worth knowing when a build fails in CI but not locally: the
+deps docker image regenerates the mweb ffi bindings with an ffigen too old for the current
+compiler and its rsync overwrites `cw_mweb/pubspec.yaml`, so the workflow restores that
+file and regenerates the bindings itself. And `assets/images` svgs need compiling to
+`.vec`, which `compile_graphics.sh` does not cover.
+
+## Flakiness playbook
+
+- A timeout in `pumpUntilFound` throws with the finder description. Diagnose through the
+ `integration-test-logs` artifact in CI, it holds the run log and a final logcat dump.
+ The timeout also takes a screenshot, it lands in `build/integration_test_screenshots/`
+ on the machine that ran the suite, named after the robot and the helper that timed out.
+ CI does not upload them.
+- The app's own error handling ignores missing `.svg.vec` assets by design, the harness
+ filters those too. Any other FlutterError fails the test, that is intentional.
+- Monero and wownero load native libraries on first open, give their steps generous
+ timeouts rather than retries.
+- A driver that cannot attach is watched rather than waited out. Once flutter drive says
+ it is struggling, the attempt gets `VOID_GRACE` seconds (60 by default) to come back and
+ is then ended so the retry can start, instead of sitting there for the rest of
+ `TEST_TIMEOUT`. Run 31364715120 lost 20 minutes to two attempts that did exactly that.
+ 60 is well clear of a healthy attach, which took under a second on all 19 attempts that
+ worked in that same run, against 5 seconds for the warning to appear on the two that
+ did not.
+- A suite that fails once in CI retries once with wiped app data. A red gate means the
+ same suite failed twice in a row.
### integration_test/components/common_test_cases.dart
@@ -1,260 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-class CommonTestCases {
- WidgetTester tester;
- CommonTestCases(this.tester);
-
- Future<void> isSpecificPage<T>() async {
- await tester.pumpAndSettle();
- hasType<T>();
- }
-
- Future<void> tapItemByKey(
- String key, {
- bool shouldPumpAndSettle = true,
- int pumpDuration = 100,
- }) async {
- await tester.pump(Duration(milliseconds: 500));
-
- final widgetFinder = find.byKey(ValueKey(key));
-
- expect(tester.any(widgetFinder), true, reason: 'Widget with key "$key" should be visible');
-
- final widget = widgetFinder.first;
- await tester.tap(widget, warnIfMissed: false);
- shouldPumpAndSettle
- ? await tester.pumpAndSettle(Duration(milliseconds: pumpDuration))
- : await tester.pump();
- }
-
- Future<void> tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async {
- await tester.tap(finder);
- shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
- }
-
- void hasText(String text, {bool hasWidget = true}) {
- final textWidget = find.text(text);
- expect(textWidget, hasWidget ? findsOneWidget : findsNothing);
- }
-
- void hasTextAtLestOnce(String text, {bool hasWidget = true}) {
- final textWidget = find.text(text);
- expect(textWidget, hasWidget ? findsAny : findsNothing);
- }
-
- void hasType<T>() {
- final typeWidget = find.byType(T);
- expect(typeWidget, findsOneWidget);
- }
-
- bool isKeyPresent(String key) {
- final typeWidget = find.byKey(ValueKey(key));
- return typeWidget.tryEvaluate();
- }
-
- void hasValueKey(String key) {
- final typeWidget = find.byKey(ValueKey(key));
- expect(typeWidget, findsOneWidget);
- }
-
- Future<void> swipePage({bool swipeRight = true}) async {
- await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0));
- await tester.pumpAndSettle();
- }
-
- Future<void> swipeByPageKey({required String key, bool swipeRight = true}) async {
- await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0));
- await tester.pumpAndSettle();
- }
-
- Future<void> goBack() async {
- tester.printToConsole('Routing back to previous screen');
- final NavigatorState navigator = tester.state(find.byType(Navigator));
- navigator.pop();
- await tester.pumpAndSettle();
- }
-
- Future<void> startGesture(String key, Offset gestureOffset) async {
- await tester.pumpAndSettle();
-
- final hasKey = isKeyPresent(key);
-
- tester.printToConsole("Has gestureKey: $hasKey");
-
- if (!hasKey) return;
-
- final gesture = await tester.startGesture(tester.getCenter(find.byKey(ValueKey(key))));
-
- // Drag to the left
- await gesture.moveBy(gestureOffset);
-
- // End the gesture
- await gesture.up();
-
- await tester.pump();
- }
-
- Future<void> dragUntilVisible(
- String childKey,
- String parentKey, {
- int maxScrolls = 100,
- double scrollStep = 50.0,
- int maxReverseScrolls = 50,
- }) async {
- await tester.pumpAndSettle();
-
- final itemFinder = find.byKey(ValueKey(childKey));
- final listFinder = find.byKey(ValueKey(parentKey));
-
- // Check if the widget is already in the widget tree
- if (tester.any(itemFinder)) {
- // Widget is already built and in the tree
- tester.printToConsole('Child is already present');
- return;
- }
-
- // Find the Scrollable associated with the Parent Ad
- final scrollableFinder = find.descendant(
- of: listFinder,
- matching: find.byType(Scrollable),
- );
-
- // Ensure that the Scrollable is found
- expect(
- scrollableFinder,
- findsOneWidget,
- reason: 'Scrollable descendant of the Parent Widget not found.',
- );
-
- // Get the initial scroll position
- final scrollableState = tester.state<ScrollableState>(scrollableFinder);
- double previousScrollPosition = scrollableState.position.pixels;
- bool scrollDown = true;
- bool reversedDirection = false;
- bool found = false;
-
- int reverseScrollCount = 0;
-
- for (int scrolls = 0; scrolls < maxScrolls; scrolls++) {
- await tester.pumpAndSettle();
-
- // Check if the widget is visible
- if (tester.any(itemFinder)) {
- found = true;
- break;
- }
-
- // Log current state for debugging
- tester.printToConsole('Scrolling ${scrollDown ? 'down' : 'up'}, attempt $scrolls');
-
- // Stop if reverse scroll limit is exceeded
- if (!scrollDown && reverseScrollCount >= maxReverseScrolls) {
- tester.printToConsole('Maximum reverse scrolls reached. Widget not found.');
- break;
- }
-
- // Perform scrolling in the current direction
- await tester.drag(
- scrollableFinder,
- scrollDown ? Offset(0, -scrollStep) : Offset(0, scrollStep),
- );
- await tester.pumpAndSettle();
-
- // Update the scroll position after the drag
- final currentScrollPosition = scrollableState.position.pixels;
-
- if (currentScrollPosition == previousScrollPosition) {
- // Cannot scroll further in the current direction
- if (reversedDirection) {
- // We've already tried both directions
- tester.printToConsole('Reached the scroll limit in both directions. Widget not found.');
- break;
- } else {
- // Reverse the scroll direction and reset reverse scroll count
- scrollDown = !scrollDown;
- reversedDirection = true;
- reverseScrollCount = 0;
- tester.printToConsole('Reached the end, reversing direction');
- }
- } else {
- // Update scroll position and reverse scroll count, incrementing only for reverse scrolling
- previousScrollPosition = currentScrollPosition;
- if (!scrollDown) reverseScrollCount++;
- }
- }
-
- if (!found) {
- tester.printToConsole('Widget not found after $maxScrolls scrolls.');
- }
- }
-
- Future<void> scrollItemIntoView(
- String itemKeyId,
- double scrollPixels,
- String scrollableFinderKey,
- ) async {
- final Finder itemFinder = find.byKey(ValueKey(itemKeyId));
-
- final scrollableFinder = find.descendant(
- of: find.byKey(ValueKey(scrollableFinderKey)),
- matching: find.byType(Scrollable),
- );
-
- // Check if the item is already visible
- if (tester.any(itemFinder)) {
- tester.printToConsole('Item $itemKeyId is already visible');
- return;
- }
-
- // Check if scrollable exists
- if (!tester.any(scrollableFinder)) {
- tester.printToConsole('Scrollable not found for $itemKeyId');
- return;
- }
-
- try {
- await tester.scrollUntilVisible(
- itemFinder,
- scrollPixels,
- scrollable: scrollableFinder,
- maxScrolls: 10,
- );
-
- // Wait for the scroll to complete
- await tester.pumpAndSettle(Duration(milliseconds: 500));
-
- // Verify the item is now visible
- if (!tester.any(itemFinder)) {
- tester.printToConsole('Item $itemKeyId not found after scrolling');
- }
- } catch (e) {
- tester.printToConsole('Could not scroll to $itemKeyId: $e');
- }
- }
-
- Future<void> enterText(String text, String editableTextKey) async {
- final editableTextWidget = find.byKey(ValueKey((editableTextKey)));
-
- await tester.enterText(editableTextWidget, text);
-
- await tester.pumpAndSettle();
- }
-
- void findWidgetViaDescendant({
- required FinderBase<Element> of,
- required FinderBase<Element> matching,
- }) {
- final textWidget = find.descendant(of: of, matching: matching);
-
- expect(textWidget, findsOneWidget);
- }
-
- Future<void> defaultSleepTime({int seconds = 2}) async =>
- await Future.delayed(Duration(seconds: seconds));
-
- Future<void> takeScreenshots(String screenshotName) async {
- // Pausing this for now
- // await (tester.binding as IntegrationTestWidgetsFlutterBinding).takeScreenshot(screenshotName);
- }
-}
### integration_test/components/common_test_constants.dart
@@ -1,15 +0,0 @@
-import 'package:cw_core/crypto_currency.dart';
-import 'package:cw_core/wallet_type.dart';
-
-class CommonTestConstants {
- static final pin = [0, 8, 0, 1];
- static final String sendTestAmount = '0.00008';
- static final String sendTestFiatAmount = '1.00';
- static final String exchangeTestAmount = '0.01';
- static final WalletType testWalletType = WalletType.solana;
- static final String testWalletName = 'Integrated Testing Wallet';
- static final CryptoCurrency sendTestReceiveCurrency = CryptoCurrency.sol;
- static final CryptoCurrency exchangeTestReceiveCurrency = CryptoCurrency.usdtSol;
- static final CryptoCurrency exchangeTestDepositCurrency = CryptoCurrency.sol;
- static final String testWalletAddress = '7TSTXnQZxQDA4JoNqs4DqVqE7oNWk2kmHkMT6DD6vw2S';
-}
### integration_test/components/common_test_flows.dart
@@ -1,414 +0,0 @@
-import 'dart:io';
-
-import 'package:cake_wallet/entities/seed_type.dart';
-import 'package:cake_wallet/reactions/wallet_utils.dart';
-import 'package:cake_wallet/wallet_types.g.dart';
-import 'package:cw_core/currency_for_wallet_type.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:cake_wallet/main.dart' as app;
-
-import '../robots/create_pin_welcome_page_robot.dart';
-import '../robots/new_wallet_page_robot.dart';
-import '../robots/new_wallet_type_page_robot.dart';
-import '../robots/pre_seed_page_robot.dart';
-import '../robots/restore_from_seed_or_key_robot.dart';
-import '../robots/restore_options_page_robot.dart';
-import '../robots/seed_verification_page_robot.dart';
-import '../robots/setup_pin_code_robot.dart';
-import '../robots/wallet_group_description_page_robot.dart';
-import '../robots/wallet_list_page_robot.dart';
-import '../robots/wallet_seed_page_robot.dart';
-import '../robots/welcome_page_robot.dart';
-import 'common_test_cases.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-import 'common_test_constants.dart';
-
-class CommonTestFlows {
- CommonTestFlows(this._tester)
- : _commonTestCases = CommonTestCases(_tester),
- _welcomePageRobot = WelcomePageRobot(_tester),
- _preSeedPageRobot = PreSeedPageRobot(_tester),
- _setupPinCodeRobot = SetupPinCodeRobot(_tester),
- _newWalletPageRobot = NewWalletPageRobot(_tester),
- _walletSeedPageRobot = WalletSeedPageRobot(_tester),
- _walletListPageRobot = WalletListPageRobot(_tester),
- _newWalletTypePageRobot = NewWalletTypePageRobot(_tester),
- _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester),
- _seedVerificationPageRobot = SeedVerificationPageRobot(_tester),
- _createPinWelcomePageRobot = CreatePinWelcomePageRobot(_tester),
- _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester),
- _walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(_tester);
-
- final WidgetTester _tester;
- final CommonTestCases _commonTestCases;
-
- final WelcomePageRobot _welcomePageRobot;
- final PreSeedPageRobot _preSeedPageRobot;
- final SetupPinCodeRobot _setupPinCodeRobot;
- final NewWalletPageRobot _newWalletPageRobot;
- final WalletSeedPageRobot _walletSeedPageRobot;
- final WalletListPageRobot _walletListPageRobot;
- final NewWalletTypePageRobot _newWalletTypePageRobot;
- final RestoreOptionsPageRobot _restoreOptionsPageRobot;
- final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
- final SeedVerificationPageRobot _seedVerificationPageRobot;
- final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
- final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
-
- //* ========== Handles flow to start the app afresh and accept disclaimer =============
- Future<void> startAppFlow(Key key) async {
- await app.main(topLevelKey: key);
-
- await _tester.pump(Duration(seconds: 2));
-
- // // --------- Disclaimer Page ------------
- // // Tap checkbox to accept disclaimer
- // await _disclaimerPageRobot.tapDisclaimerCheckbox();
-
- // // Tap accept button
- // await _disclaimerPageRobot.tapAcceptButton();
- }
-
- //* ========== Handles flow from welcome to creating a new wallet ===============
- Future<void> welcomePageToCreateNewWalletFlow(
- WalletType walletTypeToCreate,
- List<int> walletPin,
- ) async {
- await _welcomeToCreateWalletPath(walletTypeToCreate, walletPin);
-
- await _generateNewWalletDetails();
-
- await _confirmPreSeedInfo();
-
- await _confirmWalletDetails();
-
- await _verifyWalletSeed();
- }
-
- //* ========== Handles flow from welcome to restoring wallet from seeds ===============
- Future<void> welcomePageToRestoreWalletThroughSeedsFlow(
- WalletType walletTypeToRestore,
- String walletSeed,
- List<int> walletPin,
- ) async {
- await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin);
- await _restoreFromSeeds(walletTypeToRestore, walletSeed);
- }
-
- //* ========== Handles flow from welcome to restoring wallet from keys ===============
- Future<void> welcomePageToRestoreWalletThroughKeysFlow(
- WalletType walletTypeToRestore,
- List<int> walletPin,
- ) async {
- await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin);
- await _restoreFromKeys();
- }
-
- void confirmAllAvailableWalletTypeIconsDisplayCorrectly() {
- for (var walletType in availableWalletTypes) {
- final imageUrl = walletTypeToCryptoCurrency(walletType).iconPath;
-
- final walletIconFinder = find.image(
- Image.asset(
- imageUrl!,
- width: 32,
- height: 32,
- ).image,
- );
-
- expect(walletIconFinder, findsAny);
- }
- }
-
- //* ========== Handles creating new wallet flow from wallet list/menu ===============
- Future<void> createNewWalletFromWalletMenu(WalletType walletTypeToCreate) async {
- _tester.printToConsole('Creating ${walletTypeToCreate.name} Wallet');
-
- await _tester.pumpAndSettle(Duration(milliseconds: 1000));
-
- await _walletListPageRobot.navigateToCreateNewWalletPage();
- await _commonTestCases.defaultSleepTime();
-
- await _selectWalletTypeForWallet(walletTypeToCreate);
- await _commonTestCases.defaultSleepTime();
-
- // ---- Wallet Group/New Seed Implementation Comes here
- await _walletGroupDescriptionPageFlow(true, walletTypeToCreate);
-
- await _generateNewWalletDetails();
-
- await _confirmPreSeedInfo();
-
- await _confirmWalletDetails();
-
- await _verifyWalletSeed();
-
- await _commonTestCases.defaultSleepTime();
- }
-
- Future<void> _walletGroupDescriptionPageFlow(bool isNewSeed, WalletType walletType) async {
- if (!isBIP39Wallet(walletType)) return;
-
- await _walletGroupDescriptionPageRobot.isWalletGroupDescriptionPage();
-
- if (isNewSeed) {
- await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage();
- } else {
- await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup();
- }
- }
-
- //* ========== Handles restore wallet flow from wallet list/menu ===============
- Future<void> restoreWalletFromWalletMenu(WalletType walletType, String walletSeed) async {
- _tester.printToConsole('Restoring ${walletType.name} Wallet');
- await _walletListPageRobot.navigateToRestoreWalletOptionsPage();
- await _commonTestCases.defaultSleepTime();
-
- if (!Platform.isLinux) await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
- if (!Platform.isLinux) await _commonTestCases.defaultSleepTime();
-
- await _selectWalletTypeForWallet(walletType);
- await _commonTestCases.defaultSleepTime();
-
- await _restoreFromSeeds(walletType, walletSeed);
- await _commonTestCases.defaultSleepTime();
- }
-
- //* ========== Handles setting up pin code for wallet on first install ===============
- Future<void> setupPinCodeForWallet(List<int> pin) async {
- if (Platform.isLinux) return;
- // ----------- SetupPinCode Page -------------
- // Confirm initial defaults - Widgets to be displayed etc
- await _setupPinCodeRobot.isSetupPinCodePage();
-
- await _setupPinCodeRobot.enterPinCode(pin);
- await _setupPinCodeRobot.enterPinCode(pin);
- await _setupPinCodeRobot.tapSuccessButton();
- }
-
- Future<void> _welcomeToCreateWalletPath(
- WalletType walletTypeToCreate,
- List<int> pin,
- ) async {
- await _createPinWelcomePageRobot.tapSetAPinButton();
-
- await setupPinCodeForWallet(pin);
-
- await _welcomePageRobot.navigateToCreateNewWalletPage();
-
- await _selectWalletTypeForWallet(walletTypeToCreate);
-
- if (_welcomePageRobot.hasNewSingleSeedButton()) {
- await _welcomePageRobot.tapNewSingleSeed();
- }
- }
-
- Future<void> _welcomeToRestoreFromSeedsOrKeysPath(
- WalletType walletTypeToRestore,
- List<int> pin,
- ) async {
- await _createPinWelcomePageRobot.tapSetAPinButton();
-
- await setupPinCodeForWallet(pin);
-
- await _welcomePageRobot.navigateToRestoreWalletPage();
-
- if (!Platform.isLinux) await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
-
- await _selectWalletTypeForWallet(walletTypeToRestore);
- }
-
- //* ============ Handles New Wallet Type Page ==================
- Future<void> _selectWalletTypeForWallet(WalletType type) async {
- // ----------- NewWalletType Page -------------
- _tester.printToConsole('Selecting wallet type: ${type.name}');
-
- await _tester.pumpAndSettle(Duration(milliseconds: 1000));
-
- await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
-
- // Tapping the wallet type navigates directly (no separate Next button).
- _tester.printToConsole('Tapping wallet type: ${type.name}');
- await _newWalletTypePageRobot.selectWalletType(type);
-
- await _tester.pumpAndSettle(Duration(milliseconds: 2000));
-
- _tester.printToConsole('Wallet type selection completed for: ${type.name}');
- }
-
- //* ============ Handles New Wallet Page ==================
- Future<void> _generateNewWalletDetails() async {
- _tester.printToConsole('Attempting to find NewWalletPage...');
-
- await _tester.pumpAndSettle(Duration(milliseconds: 1000));
-
- await _newWalletPageRobot.isNewWalletPage();
- _tester.printToConsole('NewWalletPage found successfully');
-
- await _newWalletPageRobot.generateWalletName();
-
- if (Platform.isLinux) {
- // manual pin input
- await _restoreFromSeedOrKeysPageRobot
- .enterPasswordForWalletRestore(CommonTestConstants.pin.join(""));
- await _restoreFromSeedOrKeysPageRobot
- .enterPasswordRepeatForWalletRestore(CommonTestConstants.pin.join(""));
- }
-
- await _newWalletPageRobot.onNextButtonPressed();
- }
-
- //* ============ Handles Pre Seed Page =====================
- Future<void> _confirmPreSeedInfo() async {
- await _preSeedPageRobot.isPreSeedPage();
-
- await _preSeedPageRobot.onConfirmButtonPressed();
- }
-
- //* ============ Handles Wallet Seed Page ==================
- Future<void> _confirmWalletDetails() async {
- await _walletSeedPageRobot.isWalletSeedPage();
-
- _walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly();
-
- _walletSeedPageRobot.confirmWalletSeedReminderDisplays();
-
- // await _walletSeedPageRobot.onCopySeedsButtonPressed();
-
- await _walletSeedPageRobot.onVerifySeedButtonPressed();
- }
-
- //* ============ Handles Wallet Seed Verification Page ==================
-
- Future<void> _verifyWalletSeed() async {
- await _seedVerificationPageRobot.isSeedVerificationPage();
-
- _seedVerificationPageRobot.hasTitle();
-
- await _seedVerificationPageRobot.verifyWalletSeeds();
- }
-
- //* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Seeds Action
- Future<void> _restoreFromSeeds(WalletType type, String walletSeed) async {
- // ----------- RestoreFromSeedOrKeys Page -------------
-
- await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions();
- await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(walletSeed);
-
- final numberOfWords = walletSeed.split(' ').length;
-
- if (numberOfWords == 25 && (type == WalletType.monero)) {
- await _restoreFromSeedOrKeysPageRobot
- .chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy);
-
- // Using a constant value of 2831400 for the blockheight as its the restore blockheight for our testing wallet
- await _restoreFromSeedOrKeysPageRobot
- .enterBlockHeightForWalletRestore(secrets.moneroTestWalletBlockHeight);
- }
-
- if (Platform.isLinux) {
- // manual pin input
- await _restoreFromSeedOrKeysPageRobot
- .enterPasswordForWalletRestore(CommonTestConstants.pin.join(""));
- await _restoreFromSeedOrKeysPageRobot
- .enterPasswordRepeatForWalletRestore(CommonTestConstants.pin.join(""));
- }
-
- await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
- }
-
- //* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Keys Action
- Future<void> _restoreFromKeys() async {
- await _commonTestCases.swipePage();
- await _commonTestCases.defaultSleepTime();
-
- await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions(
- isSeedFormEntry: false,
- );
-
- await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore('');
- await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
- }
-
- //* ====== Utility Function to get test wallet seeds for each wallet type ========
- String getWalletSeedsByWalletType(WalletType walletType) {
- switch (walletType) {
- case WalletType.monero:
- return secrets.moneroTestWalletSeeds;
- case WalletType.bitcoin:
- return secrets.bitcoinTestWalletSeeds;
- case WalletType.ethereum:
- return secrets.ethereumTestWalletSeeds;
- case WalletType.litecoin:
- return secrets.litecoinTestWalletSeeds;
- case WalletType.bitcoinCash:
- return secrets.bitcoinCashTestWalletSeeds;
- case WalletType.polygon:
- return secrets.polygonTestWalletSeeds;
- case WalletType.solana:
- return secrets.solanaTestWalletSeeds;
- case WalletType.base:
- return secrets.baseTestWalletSeeds;
- case WalletType.arbitrum:
- return secrets.arbitrumTestWalletSeeds;
- case WalletType.bsc:
- return secrets.bscTestWalletSeeds;
- case WalletType.tron:
- return secrets.tronTestWalletSeeds;
- case WalletType.nano:
- return secrets.nanoTestWalletSeeds;
- case WalletType.wownero:
- return secrets.wowneroTestWalletSeeds;
- case WalletType.zano:
- return secrets.zanoTestWalletSeeds;
- case WalletType.decred:
- return secrets.decredTestWalletSeeds;
- case WalletType.dogecoin:
- return secrets.dogeTestWalletSeeds;
- case WalletType.zcash:
- return secrets.zcashTestWalletSeeds;
- case WalletType.none:
- case WalletType.haven:
- case WalletType.banano:
- throw Exception("Unable to get seeds for ${walletType}");
- }
- }
-
- //* ====== Utility Function to get test receive address for each wallet type ========
- String getReceiveAddressByWalletType(WalletType walletType) {
- switch (walletType) {
- case WalletType.monero:
- return secrets.moneroTestWalletReceiveAddress;
- case WalletType.bitcoin:
- return secrets.bitcoinTestWalletReceiveAddress;
- case WalletType.ethereum:
- return secrets.ethereumTestWalletReceiveAddress;
- case WalletType.litecoin:
- return secrets.litecoinTestWalletReceiveAddress;
- case WalletType.bitcoinCash:
- return secrets.bitcoinCashTestWalletReceiveAddress;
- case WalletType.polygon:
- return secrets.polygonTestWalletReceiveAddress;
- case WalletType.base:
- return secrets.baseTestWalletReceiveAddress;
- case WalletType.arbitrum:
- return secrets.arbitrumTestWalletReceiveAddress;
- case WalletType.bsc:
- return secrets.bscTestWalletReceiveAddress;
- case WalletType.solana:
- return secrets.solanaTestWalletReceiveAddress;
- case WalletType.tron:
- return secrets.tronTestWalletReceiveAddress;
- case WalletType.nano:
- return secrets.nanoTestWalletReceiveAddress;
- case WalletType.wownero:
- return secrets.wowneroTestWalletReceiveAddress;
- default:
- return '';
- }
- }
-}
### integration_test/core/app_launcher.dart
@@ -0,0 +1,75 @@
+import "dart:async";
+
+import "package:cake_wallet/main.dart" as app;
+import "package:flutter/foundation.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "benign_errors.dart";
+
+void integrationTest(String description, Future<void> Function(WidgetTester tester) body) {
+ testWidgets(description, (tester) async {
+ final completer = Completer<void>();
+
+ final run = runZonedGuarded(() async {
+ try {
+ await body(tester);
+
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ } catch (error, stack) {
+ if (!completer.isCompleted) {
+ completer.completeError(error, stack);
+ }
+ }
+ }, (error, stack) {
+ final kind = benignErrorKind(error.toString());
+
+ if (kind != null) {
+ debugPrint("Ignoring benign $kind background error: $error");
+ return;
+ }
+
+ if (completer.isCompleted) {
+ debugPrint("Background error after the test finished: $error");
+ return;
+ }
+
+ completer.completeError(error, stack);
+ });
+
+ unawaited(run);
+
+ await completer.future;
+ });
+}
+
+class AppLauncher {
+ AppLauncher(this.tester);
+
+ final WidgetTester tester;
+
+ Future<void> launchApp({required String testKey}) async {
+ final bindingHandler = FlutterError.onError;
+
+ await app.main(topLevelKey: ValueKey(testKey));
+
+ _installTestErrorHandler(bindingHandler);
+
+ await tester.pump(const Duration(seconds: 2));
+ }
+
+ void _installTestErrorHandler(FlutterExceptionHandler? bindingHandler) {
+ FlutterError.onError = (details) {
+ final reportedByTestFramework = details.library == "Flutter test framework";
+ final kind = reportedByTestFramework ? null : benignErrorKind(details.exceptionAsString());
+
+ if (kind != null) {
+ debugPrint("Ignoring benign $kind error: ${details.exceptionAsString()}");
+ return;
+ }
+
+ bindingHandler?.call(details);
+ };
+ }
+}
### integration_test/core/base_robot.dart
@@ -0,0 +1,238 @@
+import "dart:async";
+
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "benign_errors.dart";
+
+abstract class BaseRobot {
+ BaseRobot(this.tester);
+
+ final WidgetTester tester;
+
+ Future<void> isDisplayed();
+
+ void drainBackgroundErrors() {
+ final Object? pending = tester.takeException();
+
+ if (pending == null) {
+ return;
+ }
+
+ final kind = benignErrorKind(pending.toString());
+
+ if (kind != null) {
+ tester.printToConsole("Ignoring benign $kind background error: $pending");
+ return;
+ }
+
+ throw TestFailure("Unhandled background error: $pending");
+ }
+
+ Future<void> _pumpStep(Duration step) async {
+ await tester.pump(step);
+
+ drainBackgroundErrors();
+ }
+
+ Future<void> pumpUntilFound(
+ Finder finder, {
+ Duration timeout = const Duration(seconds: 30),
+ Duration step = const Duration(milliseconds: 100),
+ }) async {
+ final endTime = DateTime.now().add(timeout);
+
+ while (DateTime.now().isBefore(endTime)) {
+ await _pumpStep(step);
+
+ if (tester.any(finder)) {
+ return;
+ }
+ }
+
+ await takeScreenshot("${runtimeType}_pump_until_found_timeout");
+ throw TestFailure("Widget not found after ${timeout.inSeconds}s: $finder");
+ }
+
+ Future<void> pumpUntilGone(
+ Finder finder, {
+ Duration timeout = const Duration(seconds: 30),
+ Duration step = const Duration(milliseconds: 100),
+ }) async {
+ final endTime = DateTime.now().add(timeout);
+
+ while (DateTime.now().isBefore(endTime)) {
+ await _pumpStep(step);
+
+ if (!tester.any(finder)) {
+ return;
+ }
+ }
+
+ await takeScreenshot("${runtimeType}_pump_until_gone_timeout");
+ throw TestFailure("Widget still present after ${timeout.inSeconds}s: $finder");
+ }
+
+ Future<void> tapByKey(String key, {Duration timeout = const Duration(seconds: 30)}) async {
+ final finder = find.byKey(ValueKey(key));
+
+ await pumpUntilFound(finder, timeout: timeout);
+
+ // A tap during a route transition lands on the animating overlay instead of the widget
+ await settle(max: const Duration(seconds: 2));
+
+ await tester.tap(finder.first, warnIfMissed: false);
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+
+ Future<void> tapWhenVisible(
+ Finder finder, {
+ Duration timeout = const Duration(seconds: 30),
+ }) async {
+ await pumpUntilFound(finder, timeout: timeout);
+
+ await settle(max: const Duration(seconds: 2));
+
+ await tester.tap(finder.first, warnIfMissed: false);
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+
+ Future<void> enterTextByKey(String key, String text) async {
+ final finder = find.byKey(ValueKey(key));
+
+ await pumpUntilFound(finder);
+
+ await tester.enterText(finder.first, text);
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+
+ Future<bool> pumpUntil(
+ bool Function() condition, {
+ Duration timeout = const Duration(seconds: 30),
+ Duration step = const Duration(milliseconds: 100),
+ }) async {
+ final endTime = DateTime.now().add(timeout);
+
+ while (DateTime.now().isBefore(endTime)) {
+ await _pumpStep(step);
+
+ if (condition()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ Future<void> goBack() async {
+ final navigator = tester.state<NavigatorState>(find.byType(Navigator).last);
+ navigator.pop();
+
+ await tester.pump(const Duration(milliseconds: 500));
+ }
+
+ Future<void> dismissModal() async {
+ final navigator = tester.state<NavigatorState>(find.byType(Navigator).first);
+ navigator.pop();
+
+ await tester.pump(const Duration(milliseconds: 500));
+ }
+
+ // We make this bounded instead of pumpAndSettle
+ Future<void> settle({Duration max = const Duration(seconds: 3)}) async {
+ final endTime = DateTime.now().add(max);
+
+ while (DateTime.now().isBefore(endTime)) {
+ await _pumpStep(const Duration(milliseconds: 100));
+
+ if (!tester.binding.hasScheduledFrame) {
+ return;
+ }
+ }
+ }
+
+ Future<void> isSpecificPage<T>({Duration timeout = const Duration(seconds: 30)}) async {
+ await pumpUntilFound(find.byType(T), timeout: timeout);
+
+ await settle();
+
+ hasType<T>();
+ }
+
+ bool isKeyPresent(String key) => tester.any(find.byKey(ValueKey(key)));
+
+ String describeScreen() {
+ final names = tester.allWidgets
+ .map((widget) => widget.runtimeType.toString())
+ .where((name) => name.endsWith("Page") || name.endsWith("Sheet") || name.endsWith("Screen"))
+ .toSet()
+ .toList()
+ ..sort();
+
+ return names.join(", ");
+ }
+
+ void hasType<T>() {
+ expect(find.byType(T), findsOneWidget);
+ }
+
+ void hasText(String text) {
+ expect(find.text(text), findsOneWidget);
+ }
+
+ void hasTextAtLeastOnce(String text) {
+ expect(find.text(text), findsAny);
+ }
+
+ String? textByKey(String key) {
+ final finder = find.byKey(ValueKey(key));
+
+ if (!tester.any(finder)) {
+ return null;
+ }
+
+ return tester.widget<Text>(finder.first).data;
+ }
+
+ static bool _surfaceConverted = false;
+
+ Future<void> takeScreenshot(String name) async {
+ try {
+ final binding = tester.binding;
+
+ if (binding is! IntegrationTestWidgetsFlutterBinding) {
+ return;
+ }
+
+ if (!_surfaceConverted) {
+ await binding.convertFlutterSurfaceToImage();
+ _surfaceConverted = true;
+ }
+
+ var finished = false;
+
+ unawaited(
+ binding.takeScreenshot("${name}_${DateTime.now().millisecondsSinceEpoch}").then(
+ (_) => finished = true,
+ onError: (Object error) {
+ finished = true;
+ tester.printToConsole("Screenshot $name failed: $error");
+ },
+ ),
+ );
+
+ final deadline = DateTime.now().add(const Duration(seconds: 10));
+
+ while (!finished && DateTime.now().isBefore(deadline)) {
+ await tester.pump(const Duration(milliseconds: 100));
+ }
+
+ if (!finished) {
+ tester.printToConsole("Screenshot $name gave up after 10s");
+ }
+ } catch (e) {
+ tester.printToConsole("Screenshot $name failed: $e");
+ }
+ }
+}
### integration_test/core/benign_errors.dart
@@ -0,0 +1,51 @@
+String? benignErrorKind(String message) {
+ if (_toleratedAssertions.any(message.contains)) {
+ return "assertion";
+ }
+
+ if (_toleratedAssets.any(message.contains)) {
+ return "asset";
+ }
+
+ if (_toleratedNetworkNoise.any(message.contains)) {
+ return "network";
+ }
+
+ return null;
+}
+
+// Debug build assertions. The semantics ones are real accessibility bugs that already
+// exist on dev, take them off this list once they are fixed.
+const List<String> _toleratedAssertions = [
+ "overflowed by",
+ "minValue, and maxValue must be valid numbers",
+ "node.parent?._dirty",
+ "RenderBox was not laid out",
+];
+
+// CakeImageWidget reports these before falling back to the svg
+const List<String> _toleratedAssets = [
+ "Unable to load asset",
+ "NetworkImageLoadException",
+ "Failed to load network image",
+];
+
+// The same failures exception_handler.dart ignores
+const List<String> _toleratedNetworkNoise = [
+ "SocketException",
+ "HttpException",
+ "ClientException",
+ "HandshakeException",
+ "TimeoutException",
+ "Failed host lookup",
+ "Connection closed",
+ "Connection reset by peer",
+ "Connection refused",
+ "Connection timed out",
+ "Operation timed out",
+ "Network is unreachable",
+ "No route to host",
+ "Broken pipe",
+ "Software caused connection abort",
+ "Connection closed before full header",
+];
### integration_test/core/funded_wallet.dart
@@ -0,0 +1,6 @@
+class FundedWallet {
+ const FundedWallet({required this.seed, this.passphrase = ""});
+
+ final String seed;
+ final String passphrase;
+}
### integration_test/core/funded_wallets.dart
@@ -0,0 +1,5 @@
+import "funded_wallet.dart";
+
+// Empty on purpose and it has to stay empty in git. The manual funds workflow overwrites
+// this file from the FUNDS_SECRETS_FILE secret, keyed by WalletType name.
+const Map<String, List<FundedWallet>> fundedWallets = {};
### integration_test/core/funds_outcome.dart
@@ -0,0 +1,46 @@
+import "package:flutter_test/flutter_test.dart";
+
+class FundsOutcome {
+ static const marker = "FUNDS_RESULT";
+
+ static void ok(WidgetTester tester, String chain, String detail) =>
+ _write(tester, chain, "ok", detail);
+
+ static void empty(WidgetTester tester, String chain, String detail) =>
+ _write(tester, chain, "empty", detail);
+
+ static void waiting(WidgetTester tester, String chain, String detail) =>
+ _write(tester, chain, "waiting", detail);
+
+ static void failed(WidgetTester tester, String chain, String detail) =>
+ _write(tester, chain, statusFor(detail), detail);
+
+ static String statusFor(String detail) {
+ if (_isWalletBalanceLow(detail)) {
+ return "empty";
+ }
+
+ return _isChainTooSlowToSync(detail) ? "waiting" : "failed";
+ }
+
+ static bool needsFurtherReview(String detail) => statusFor(detail) == "failed";
+
+ static bool _isChainTooSlowToSync(String detail) =>
+ detail.toLowerCase().contains("never became ready to send");
+
+ // Low wallet balance is not a failure, we just need to add fundsss
+ static bool _isWalletBalanceLow(String detail) {
+ final wording = detail.toLowerCase();
+
+ return wording.contains("not enough") ||
+ wording.contains("wrong balance") ||
+ wording.contains("insufficient") ||
+ wording.contains("no spendable");
+ }
+
+ static void _write(WidgetTester tester, String chain, String status, String detail) {
+ final clean = detail.replaceAll("\n", " ").replaceAll("|", "/").trim();
+
+ tester.printToConsole("$marker|$chain|$status|$clean");
+ }
+}
### integration_test/core/test_config.dart
@@ -0,0 +1,112 @@
+import "package:cake_wallet/wallet_types.g.dart";
+import "package:cw_core/wallet_type.dart";
+
+import "test_wallets.dart";
+
+class TestConfig {
+ static final List<int> pin = [0, 8, 0, 1];
+
+ static const bool isCIBuild = bool.fromEnvironment("CI_BUILD");
+
+ static const String _walletTypesOverride = String.fromEnvironment("TEST_WALLET_TYPES");
+
+ // Solana and ethereum cover the cheap key-derivation chains,
+ // bitcoin covers the electrum family and monero covers the native-library chains.
+ static final List<WalletType> _representativeWalletTypes = [
+ WalletType.solana,
+ WalletType.ethereum,
+ WalletType.bitcoin,
+ WalletType.monero,
+ ];
+
+ static List<WalletType> get walletTypesUnderTest {
+ if (_walletTypesOverride == "all") {
+ return availableWalletTypes;
+ }
+
+ if (_walletTypesOverride.isNotEmpty) {
+ return _walletTypesOverride
+ .split(",")
+ .map((name) => WalletType.values.byName(name.trim()))
+ .where(availableWalletTypes.contains)
+ .toList();
+ }
+
+ return _representativeWalletTypes.where(availableWalletTypes.contains).toList();
+ }
+
+ static const String _fundsFlows = String.fromEnvironment("FLOWS", defaultValue: "all");
+
+ static const String _spend = String.fromEnvironment("SPEND", defaultValue: "false");
+
+ static const String _swapFrom = String.fromEnvironment("SWAP_FROM");
+
+ static const String swapReceive = String.fromEnvironment("SWAP_RECEIVE");
+
+ static WalletType swapDepositType(List<WalletType> funded) =>
+ _swapFrom.isEmpty ? funded.first : _walletTypeByName(_swapFrom, "SWAP_FROM");
+
+ static bool shouldRunFundsFlow(String flow) => _fundsFlows == "all" || _fundsFlows == flow;
+
+ static bool shouldDryRun(String flow) => shouldRunFundsFlow(flow);
+
+ static bool shouldSpend(String flow) => _spend == "true" && shouldRunFundsFlow(flow);
+
+ static const String _fundedChainsOverride = String.fromEnvironment(
+ "CHAINS",
+ defaultValue: "auto",
+ );
+
+ static List<WalletType> get fundedWalletTypesUnderTest {
+ if (_fundedChainsOverride == "auto") {
+ return TestWallets.fundedWalletTypes;
+ }
+
+ return _fundedChainsOverride
+ .split(",")
+ .map((name) => _walletTypeByName(name, "CHAINS"))
+ .where((type) => TestWallets.fundedWalletsFor(type).isNotEmpty)
+ .toList();
+ }
+
+ static const Map<String, String> _fundsSendAmounts = {
+ "solana": "0.0001",
+ "ethereum": "0.00001",
+ "polygon": "0.01",
+ "base": "0.00001",
+ "arbitrum": "0.00001",
+ "bsc": "0.000001",
+ "tron": "1",
+ "bitcoin": "0.00001",
+ "litecoin": "0.001",
+ "bitcoinCash": "0.0002",
+ "dogecoin": "2",
+ "monero": "0.0001",
+ "wownero": "0.1",
+ "zcash": "0.0001",
+ "nano": "0.000001",
+ "decred": "0.001",
+ "zano": "0.001",
+ };
+
+ static String fundsSendAmountFor(WalletType type) => _fundsSendAmounts[type.name] ?? "0.0001";
+
+ // Making this two hours to accommodate monero that takes a longgg time syncing
+ // also, this would only affect the funded tests which need actual funds to test
+ static const Duration walletSyncBudget = Duration(hours: 2);
+
+ static WalletType _walletTypeByName(String name, String input) {
+ final trimmed = name.trim();
+
+ final type = WalletType.values.where((value) => value.name == trimmed).toList();
+
+ if (type.isEmpty) {
+ throw ArgumentError(
+ 'Unknown wallet type "$trimmed" in $input, '
+ "valid names: ${WalletType.values.map((value) => value.name).join(", ")}",
+ );
+ }
+
+ return type.first;
+ }
+}
### integration_test/core/test_wallets.dart
@@ -0,0 +1,102 @@
+import "package:cake_wallet/.secrets.g.dart" as secrets;
+import "package:cake_wallet/wallet_types.g.dart";
+import "package:cw_core/wallet_type.dart";
+
+import "funded_wallet.dart";
+import "funded_wallets.dart";
+
+class TestWallets {
+ static List<WalletType> get fundedWalletTypes =>
+ availableWalletTypes.where((type) => fundedWalletsFor(type).isNotEmpty).toList();
+
+ static List<FundedWallet> fundedWalletsFor(WalletType type) =>
+ (fundedWallets[type.name] ?? []).where((wallet) => wallet.seed.trim().isNotEmpty).toList();
+
+ static String seedFor(WalletType type) {
+ switch (type) {
+ case WalletType.monero:
+ return secrets.moneroTestWalletSeeds;
+ case WalletType.bitcoin:
+ return secrets.bitcoinTestWalletSeeds;
+ case WalletType.ethereum:
+ return secrets.ethereumTestWalletSeeds;
+ case WalletType.litecoin:
+ return secrets.litecoinTestWalletSeeds;
+ case WalletType.bitcoinCash:
+ return secrets.bitcoinCashTestWalletSeeds;
+ case WalletType.polygon:
+ return secrets.polygonTestWalletSeeds;
+ case WalletType.solana:
+ return secrets.solanaTestWalletSeeds;
+ case WalletType.base:
+ return secrets.baseTestWalletSeeds;
+ case WalletType.arbitrum:
+ return secrets.arbitrumTestWalletSeeds;
+ case WalletType.bsc:
+ return secrets.bscTestWalletSeeds;
+ case WalletType.tron:
+ return secrets.tronTestWalletSeeds;
+ case WalletType.nano:
+ return secrets.nanoTestWalletSeeds;
+ case WalletType.wownero:
+ return secrets.wowneroTestWalletSeeds;
+ case WalletType.zano:
+ return secrets.zanoTestWalletSeeds;
+ case WalletType.decred:
+ return secrets.decredTestWalletSeeds;
+ case WalletType.dogecoin:
+ return secrets.dogeTestWalletSeeds;
+ case WalletType.zcash:
+ return secrets.zcashTestWalletSeeds;
+ case WalletType.none:
+ case WalletType.haven:
+ case WalletType.banano:
+ throw Exception("No test wallet seed available for ${type.name}");
+ }
+ }
+
+ static String receiveAddressFor(WalletType type) {
+ switch (type) {
+ case WalletType.monero:
+ return secrets.moneroTestWalletReceiveAddress;
+ case WalletType.bitcoin:
+ return secrets.bitcoinTestWalletReceiveAddress;
+ case WalletType.ethereum:
+ return secrets.ethereumTestWalletReceiveAddress;
+ case WalletType.litecoin:
+ return secrets.litecoinTestWalletReceiveAddress;
+ case WalletType.bitcoinCash:
+ return secrets.bitcoinCashTestWalletReceiveAddress;
+ case WalletType.polygon:
+ return secrets.polygonTestWalletReceiveAddress;
+ case WalletType.base:
+ return secrets.baseTestWalletReceiveAddress;
+ case WalletType.arbitrum:
+ return secrets.arbitrumTestWalletReceiveAddress;
+ case WalletType.bsc:
+ return secrets.bscTestWalletReceiveAddress;
+ case WalletType.solana:
+ return secrets.solanaTestWalletReceiveAddress;
+ case WalletType.tron:
+ return secrets.tronTestWalletReceiveAddress;
+ case WalletType.nano:
+ return secrets.nanoTestWalletReceiveAddress;
+ case WalletType.wownero:
+ return secrets.wowneroTestWalletReceiveAddress;
+ case WalletType.zano:
+ case WalletType.decred:
+ case WalletType.dogecoin:
+ case WalletType.zcash:
+ case WalletType.none:
+ case WalletType.haven:
+ case WalletType.banano:
+ return "";
+ }
+ }
+
+ static String get moneroLegacySeed => secrets.moneroLegacyTestWalletSeeds;
+
+ static String get moneroRestoreBlockHeight => secrets.moneroTestWalletBlockHeight;
+
+ static String get zcashRestoreHeight => secrets.zcashTestWalletRestoreHeight;
+}
### integration_test/flows/auth_flows.dart
@@ -0,0 +1,41 @@
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/test_config.dart";
+import "../robots/auth_page_robot.dart";
+
+class AuthFlows {
+ AuthFlows(this.tester) : _authPageRobot = AuthPageRobot(tester);
+
+ final WidgetTester tester;
+ final AuthPageRobot _authPageRobot;
+
+ Future<void> authenticateWithPin({List<int>? pin, bool required = true}) async {
+ final prompted = await _waitForPinWidget(
+ timeout: required ? const Duration(seconds: 30) : const Duration(seconds: 5),
+ );
+
+ if (!prompted) {
+ expect(required, false, reason: "The pin was never asked for");
+
+ return;
+ }
+
+ await _authPageRobot.enterPinCode(pin ?? TestConfig.pin);
+ }
+
+ bool isPinWidgetShown() => _authPageRobot.onAuthPage();
+
+ Future<bool> _waitForPinWidget({Duration timeout = const Duration(seconds: 30)}) async {
+ final endTime = DateTime.now().add(timeout);
+
+ while (DateTime.now().isBefore(endTime)) {
+ await tester.pump(const Duration(milliseconds: 100));
+
+ if (_authPageRobot.onAuthPage()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
### integration_test/flows/funds_flows.dart
@@ -0,0 +1,155 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/sync_status.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/test_config.dart";
+import "../core/test_wallets.dart";
+import "../robots/home_page_robot.dart";
+import "../robots/new_dashboard_robot.dart";
+import "../robots/new_send_page_robot.dart";
+import "onboarding_flows.dart";
+
+class FundsFlows {
+ FundsFlows(this.tester)
+ : _onboardingFlows = OnboardingFlows(tester),
+ _dashboardRobot = NewDashboardRobot(tester),
+ _homePageRobot = HomePageRobot(tester),
+ _sendRobot = NewSendPageRobot(tester);
+
+ final WidgetTester tester;
+ final OnboardingFlows _onboardingFlows;
+ final NewDashboardRobot _dashboardRobot;
+ final HomePageRobot _homePageRobot;
+ final NewSendPageRobot _sendRobot;
+
+ bool _hasRestoredAnyWallet = false;
+
+ Future<bool> openFundedWallet(WalletType type) async {
+ final wallets = TestWallets.fundedWalletsFor(type);
+
+ for (int index = 0; index < wallets.length; index++) {
+ final wallet = wallets[index];
+
+ tester.printToConsole("Trying funded wallet ${index + 1}/${wallets.length} for ${type.name}");
+
+ if (_hasRestoredAnyWallet) {
+ await _dashboardRobot.openWalletsTab();
+
+ final isWalletListReady = await _dashboardRobot.pumpUntil(
+ () =>
+ tester.any(find.byKey(const ValueKey("wallet_list_page_restore_wallet_button_key"))),
+ timeout: const Duration(seconds: 60),
+ );
+
+ expect(
+ isWalletListReady,
+ true,
+ reason: "The wallet list restore button didn't show before ${type.name}, "
+ "Currently have: ${_dashboardRobot.describeScreen()} on screen",
+ );
+
+ await _onboardingFlows.restoreAdditionalWalletFromWalletList(
+ type,
+ seed: wallet.seed,
+ passphrase: wallet.passphrase,
+ );
+ } else {
+ await _onboardingFlows.restoreFirstWalletFromSeed(
+ type,
+ seed: wallet.seed,
+ passphrase: wallet.passphrase,
+ );
+ }
+
+ _hasRestoredAnyWallet = true;
+
+ await _waitForDashboard(type);
+ await _homePageRobot.isDisplayed();
+
+ if (await _hasSpendableBalance()) {
+ return true;
+ }
+
+ tester.printToConsole("Wallet ${index + 1} for ${type.name} has no spendable balance");
+ }
+
+ return false;
+ }
+
+ Future<bool> returnToHome({int maxPops = 6}) async {
+ for (int attempt = 0; attempt <= maxPops; attempt++) {
+ if (await _homePageRobot.pumpUntil(
+ () => _isHomeInFront,
+ timeout: const Duration(seconds: 3),
+ )) {
+ return true;
+ }
+
+ try {
+ await _homePageRobot.dismissModal();
+ } catch (e) {
+ tester.printToConsole("Nothing left to pop while returning home: $e");
+ break;
+ }
+ }
+
+ return _isHomeInFront;
+ }
+
+ Future<void> _waitForDashboard(WalletType type) async {
+ final reached = await _dashboardRobot.pumpUntil(
+ () => tester.any(find.byKey(const ValueKey("new_dashboard_page_key"))),
+ timeout: TestConfig.walletSyncBudget,
+ );
+
+ if (reached) {
+ return;
+ }
+
+ final status = getIt.get<AppStore>().wallet?.syncStatus;
+ final stillSyncing = status is SyncingSyncStatus || status is SyncronizingSyncStatus;
+
+ throw TestFailure(
+ stillSyncing
+ ? "${type.name} never became ready to send, it is still syncing (${status.runtimeType})"
+ : "${type.name} never reached the dashboard, its status was ${status.runtimeType}",
+ );
+ }
+
+ bool get _isHomeInFront => _homePageRobot.isInFront && !_sendRobot.isSendFlowOpen;
+
+ Future<bool> _hasSpendableBalance({Duration timeout = TestConfig.walletSyncBudget}) async {
+ final appStore = getIt.get<AppStore>();
+ final endTime = DateTime.now().add(timeout);
+ DateTime? syncedSince;
+
+ while (DateTime.now().isBefore(endTime)) {
+ await tester.pump(const Duration(milliseconds: 250));
+
+ final wallet = appStore.wallet;
+
+ if (wallet == null) {
+ continue;
+ }
+
+ if (wallet.balance.values.any((balance) => balance.available.sign > 0)) {
+ return true;
+ }
+
+ if (wallet.syncStatus is SyncedSyncStatus) {
+ syncedSince ??= DateTime.now();
+
+ if (DateTime.now().difference(syncedSince) > const Duration(seconds: 15)) {
+ return false;
+ }
+ } else {
+ syncedSince = null;
+ }
+ }
+
+ return false;
+ }
+}
### integration_test/flows/onboarding_flows.dart
@@ -0,0 +1,267 @@
+import "dart:io";
+
+import "package:cake_wallet/entities/seed_type.dart";
+import "package:cake_wallet/reactions/wallet_utils.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/test_config.dart";
+import "../core/test_wallets.dart";
+import "../robots/create_pin_welcome_page_robot.dart";
+import "../robots/lightning_username_page_robot.dart";
+import "../robots/new_wallet_page_robot.dart";
+import "../robots/new_wallet_type_page_robot.dart";
+import "../robots/pre_seed_page_robot.dart";
+import "../robots/restore_from_seed_or_key_robot.dart";
+import "../robots/restore_options_page_robot.dart";
+import "../robots/seed_verification_page_robot.dart";
+import "../robots/setup_pin_code_robot.dart";
+import "../robots/wallet_group_description_page_robot.dart";
+import "../robots/wallet_group_existing_seed_page_robot.dart";
+import "../robots/wallet_groups_display_page_robot.dart";
+import "../robots/wallet_list_page_robot.dart";
+import "../robots/wallet_seed_page_robot.dart";
+import "../robots/welcome_page_robot.dart";
+
+class OnboardingFlows {
+ OnboardingFlows(this.tester)
+ : _welcomePageRobot = WelcomePageRobot(tester),
+ _preSeedPageRobot = PreSeedPageRobot(tester),
+ _setupPinCodeRobot = SetupPinCodeRobot(tester),
+ _newWalletPageRobot = NewWalletPageRobot(tester),
+ _walletSeedPageRobot = WalletSeedPageRobot(tester),
+ _walletListPageRobot = WalletListPageRobot(tester),
+ _newWalletTypePageRobot = NewWalletTypePageRobot(tester),
+ _seedVerificationPageRobot = SeedVerificationPageRobot(tester),
+ _createPinWelcomePageRobot = CreatePinWelcomePageRobot(tester),
+ _restoreOptionsPageRobot = RestoreOptionsPageRobot(tester),
+ _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(tester),
+ _lightningUsernamePageRobot = LightningUsernamePageRobot(tester),
+ _walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(tester),
+ _walletGroupsDisplayPageRobot = WalletGroupsDisplayPageRobot(tester),
+ _walletGroupExistingSeedPageRobot = WalletGroupExistingSeedPageRobot(tester);
+
+ final WidgetTester tester;
+
+ final WelcomePageRobot _welcomePageRobot;
+ final PreSeedPageRobot _preSeedPageRobot;
+ final SetupPinCodeRobot _setupPinCodeRobot;
+ final NewWalletPageRobot _newWalletPageRobot;
+ final WalletSeedPageRobot _walletSeedPageRobot;
+ final WalletListPageRobot _walletListPageRobot;
+ final NewWalletTypePageRobot _newWalletTypePageRobot;
+ final SeedVerificationPageRobot _seedVerificationPageRobot;
+ final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
+ final RestoreOptionsPageRobot _restoreOptionsPageRobot;
+ final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
+ final LightningUsernamePageRobot _lightningUsernamePageRobot;
+ final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
+ final WalletGroupsDisplayPageRobot _walletGroupsDisplayPageRobot;
+ final WalletGroupExistingSeedPageRobot _walletGroupExistingSeedPageRobot;
+
+ Future<void> createFirstWallet(WalletType type, {List<int>? pin}) async {
+ await _createPinWelcomePageRobot.tapSetAPinButton();
+
+ await setupPinCode(pin ?? TestConfig.pin);
+
+ await _welcomePageRobot.navigateToCreateNewWalletPage();
+
+ await _selectWalletType(type);
+
+ if (_welcomePageRobot.hasNewSingleSeedButton()) {
+ await _welcomePageRobot.tapNewSingleSeed();
+ }
+
+ await _completeWalletCreationSteps(type);
+ }
+
+ Future<void> createAdditionalWalletFromWalletList(WalletType type) async {
+ tester.printToConsole("Creating ${type.name} wallet");
+
+ await startCreatingWalletFromWalletList(type);
+
+ await _completeWalletCreationSteps(type);
+ }
+
+ Future<void> createWalletInGroupOf(WalletType type, String existingWalletName) async {
+ tester.printToConsole("Adding a ${type.name} wallet to the seed of $existingWalletName");
+
+ await _walletListPageRobot.navigateToCreateNewWalletPage();
+
+ await _selectWalletType(type);
+
+ await _walletGroupDescriptionPageRobot.isDisplayed();
+ await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup();
+
+ await _walletGroupsDisplayPageRobot.isDisplayed();
+ await _walletGroupsDisplayPageRobot.selectWallet(existingWalletName);
+ await _walletGroupsDisplayPageRobot.continueWithSelectedSeed();
+
+ await _newWalletPageRobot.isDisplayed();
+ await _newWalletPageRobot.generateWalletName();
+ await _newWalletPageRobot.onNextButtonPressed();
+
+ await _walletGroupExistingSeedPageRobot.isDisplayed();
+ await _walletGroupExistingSeedPageRobot.openWallet();
+ }
+
+ Future<void> startCreatingWalletFromWalletList(WalletType type) async {
+ await _walletListPageRobot.navigateToCreateNewWalletPage();
+
+ await _selectWalletType(type);
+
+ if (isBIP39Wallet(type)) {
+ await _walletGroupDescriptionPageRobot.isDisplayed();
+ await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage();
+ }
+
+ await _newWalletPageRobot.isDisplayed();
+ }
+
+ Future<void> restoreFirstWalletFromSeed(
+ WalletType type, {
+ String? seed,
+ String passphrase = "",
+ List<int>? pin,
+ }) async {
+ await startRestoringFirstWallet(type, pin: pin);
+
+ await _restoreFromSeed(type, seed ?? TestWallets.seedFor(type), passphrase);
+ }
+
+ Future<void> startRestoringFirstWallet(WalletType type, {List<int>? pin}) async {
+ await _createPinWelcomePageRobot.tapSetAPinButton();
+
+ await setupPinCode(pin ?? TestConfig.pin);
+
+ await _welcomePageRobot.navigateToRestoreWalletPage();
+
+ // Desktop skips the restore options page and goes straight to the type list.
+ if (!Platform.isLinux) {
+ await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
+ }
+
+ await _selectWalletType(type);
+
+ await _restoreFromSeedOrKeysPageRobot.isDisplayed();
+ }
+
+ Future<void> restoreAdditionalWalletFromWalletList(
+ WalletType type, {
+ String? seed,
+ String passphrase = "",
+ }) async {
+ tester.printToConsole("Restoring ${type.name} wallet");
+
+ await _walletListPageRobot.navigateToRestoreWalletOptionsPage();
+
+ if (!Platform.isLinux) {
+ await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
+ }
+
+ await _selectWalletType(type);
+
+ await _restoreFromSeed(type, seed ?? TestWallets.seedFor(type), passphrase);
+ }
+
+ Future<void> _restoreFromSeed(WalletType type, String seed, String passphrase) async {
+ await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions();
+ await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(seed);
+
+ if (passphrase.isNotEmpty) {
+ await _restoreFromSeedOrKeysPageRobot.enablePassphrase();
+ }
+
+ // 25 word monero seeds are legacy seeds and need their restore block height entered.
+ if (seed.split(" ").length == 25 && type == WalletType.monero) {
+ await _restoreFromSeedOrKeysPageRobot
+ .chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy);
+ await _restoreFromSeedOrKeysPageRobot
+ .enterBlockHeightForWalletRestore(TestWallets.moneroRestoreBlockHeight);
+ }
+
+ if (type == WalletType.zcash && TestWallets.zcashRestoreHeight.isNotEmpty) {
+ await _restoreFromSeedOrKeysPageRobot
+ .enterBlockHeightForWalletRestore(TestWallets.zcashRestoreHeight);
+ }
+
+ if (Platform.isLinux) {
+ final password = TestConfig.pin.join("");
+ await _restoreFromSeedOrKeysPageRobot.enterPasswordForWalletRestore(password);
+ await _restoreFromSeedOrKeysPageRobot.enterPasswordRepeatForWalletRestore(password);
+ }
+
+ await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
+
+ if (passphrase.isNotEmpty) {
+ await _restoreFromSeedOrKeysPageRobot.enterPassphrase(passphrase);
+ }
+ }
+
+ Future<void> setupPinCode(List<int> pin) async {
+ // Desktop builds take a wallet password on the new wallet form instead of a pin.
+ if (Platform.isLinux) {
+ return;
+ }
+
+ await _setupPinCodeRobot.isDisplayed();
+
+ await _setupPinCodeRobot.enterPinCode(pin);
+ await _setupPinCodeRobot.enterPinCode(pin);
+ await _setupPinCodeRobot.tapSuccessButton();
+ }
+
+ Future<void> _selectWalletType(WalletType type) async {
+ await _newWalletTypePageRobot.isDisplayed();
+
+ await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
+ await _newWalletTypePageRobot.selectWalletType(type);
+ }
+
+ Future<void> _completeWalletCreationSteps(WalletType type) async {
+ await _generateNewWalletDetails();
+
+ await _preSeedPageRobot.isDisplayed();
+ await _preSeedPageRobot.onConfirmButtonPressed();
+
+ await _confirmWalletSeedDetails();
+
+ await _verifyWalletSeed();
+
+ if (type == WalletType.bitcoin) {
+ await _lightningUsernamePageRobot.isDisplayed();
+ await _lightningUsernamePageRobot.tapSkip();
+ }
+ }
+
+ Future<void> _generateNewWalletDetails() async {
+ await _newWalletPageRobot.isDisplayed();
+
+ await _newWalletPageRobot.generateWalletName();
+
+ if (Platform.isLinux) {
+ final password = TestConfig.pin.join("");
+ await _restoreFromSeedOrKeysPageRobot.enterPasswordForWalletRestore(password);
+ await _restoreFromSeedOrKeysPageRobot.enterPasswordRepeatForWalletRestore(password);
+ }
+
+ await _newWalletPageRobot.onNextButtonPressed();
+ }
+
+ Future<void> _confirmWalletSeedDetails() async {
+ await _walletSeedPageRobot.isDisplayed();
+
+ _walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly();
+ _walletSeedPageRobot.confirmWalletSeedReminderDisplays();
+
+ await _walletSeedPageRobot.onVerifySeedButtonPressed();
+ }
+
+ Future<void> _verifyWalletSeed() async {
+ await _seedVerificationPageRobot.isDisplayed();
+
+ _seedVerificationPageRobot.hasTitle();
+
+ await _seedVerificationPageRobot.verifyWalletSeeds();
+ }
+}
### integration_test/flows/wallet_flows.dart
@@ -0,0 +1,59 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../robots/new_dashboard_robot.dart";
+import "auth_flows.dart";
+
+class WalletFlows {
+ WalletFlows(this.tester)
+ : _authFlows = AuthFlows(tester),
+ _dashboardRobot = NewDashboardRobot(tester);
+
+ final WidgetTester tester;
+ final AuthFlows _authFlows;
+ final NewDashboardRobot _dashboardRobot;
+
+ Future<void> _expandWalletGroups({int maxGroups = 10}) async {
+ for (int index = 0; index < maxGroups; index++) {
+ final tile = find.byKey(ValueKey("group_wallets_expansion_tile_widget_$index"));
+
+ if (!tester.any(tile)) {
+ return;
+ }
+
+ await tester.tap(tile, warnIfMissed: false);
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+ }
+
+ Future<void> switchToWallet(String name) async {
+ await _dashboardRobot.openWalletsTab();
+
+ final walletFinder = find.text(name);
+
+ // Wallets sharing a seed are listed under their group's name, so the wallet itself is
+ // not on screen until that group is opened.
+ if (!tester.any(walletFinder)) {
+ await _expandWalletGroups();
+ }
+
+ await _dashboardRobot.pumpUntilFound(walletFinder);
+
+ await tester.tap(walletFinder.first, warnIfMissed: false);
+ await tester.pump(const Duration(milliseconds: 500));
+
+ await _authFlows.authenticateWithPin(required: false);
+
+ // The row does nothing at all when that wallet is already the open one, and the load
+ // itself runs behind a progress overlay, so the store is the only thing that tells us
+ // the switch happened.
+ final switched = await _dashboardRobot.pumpUntil(
+ () => getIt.get<AppStore>().wallet?.name == name,
+ timeout: const Duration(minutes: 2),
+ );
+
+ expect(switched, true, reason: "Wallet never became $name after tapping its row");
+ }
+}
### integration_test/funds_related_tests.dart
@@ -1,96 +0,0 @@
-import 'package:flutter/foundation.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import 'components/common_test_constants.dart';
-import 'components/common_test_flows.dart';
-import 'robots/auth_page_robot.dart';
-import 'robots/dashboard_page_robot.dart';
-import 'robots/exchange_confirm_page_robot.dart';
-import 'robots/exchange_page_robot.dart';
-import 'robots/exchange_trade_page_robot.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- DashboardPageRobot dashboardPageRobot;
- ExchangePageRobot exchangePageRobot;
- ExchangeConfirmPageRobot exchangeConfirmPageRobot;
- AuthPageRobot authPageRobot;
- ExchangeTradePageRobot exchangeTradePageRobot;
- CommonTestFlows commonTestFlows;
-
- group('Startup Test', () {
- testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL',
- (tester) async {
- authPageRobot = AuthPageRobot(tester);
- exchangePageRobot = ExchangePageRobot(tester);
- dashboardPageRobot = DashboardPageRobot(tester);
- exchangeTradePageRobot = ExchangeTradePageRobot(tester);
- exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
- commonTestFlows = CommonTestFlows(tester);
-
- await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key'));
-
- await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
- CommonTestConstants.testWalletType,
- secrets.solanaTestWalletSeeds,
- CommonTestConstants.pin,
- );
-
- // ----------- RestoreFromSeedOrKeys Page -------------
- await dashboardPageRobot.navigateToExchangePage();
-
- // ----------- Exchange Page -------------
- await exchangePageRobot.isExchangePage();
- exchangePageRobot.hasResetButton();
- await exchangePageRobot.displayBothExchangeCards();
- exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards();
- exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards();
-
- await exchangePageRobot
- .selectDepositCurrency(CommonTestConstants.exchangeTestDepositCurrency);
- await exchangePageRobot
- .selectReceiveCurrency(CommonTestConstants.exchangeTestReceiveCurrency);
-
- await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
- await exchangePageRobot.enterDepositRefundAddress(
- depositAddress: CommonTestConstants.testWalletAddress);
-
- await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
-
- await exchangePageRobot.onExchangeButtonPressed();
-
- await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
-
- final onAuthPage = authPageRobot.onAuthPage();
- if (onAuthPage) {
- await authPageRobot.enterPinCode(CommonTestConstants.pin);
- }
-
- final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
- if (onAuthPageDesktop) {
- await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
- }
-
- // ----------- Exchange Confirm Page -------------
- await exchangeConfirmPageRobot.isExchangeConfirmPage();
-
- exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly();
- await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly();
- await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
-
- // ----------- Exchange Trade Page -------------
- await exchangeTradePageRobot.isExchangeTradePage();
- exchangeTradePageRobot.hasInformationDialog();
- await exchangeTradePageRobot.onGotItButtonPressed();
-
- await exchangeTradePageRobot.onSendFromCakeButtonPressed();
-
- await exchangeTradePageRobot.handleConfirmSendResult();
-
- await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed();
- });
- });
-}
### integration_test/funds_suites/send_dry_run_test.dart
@@ -0,0 +1,107 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../core/app_launcher.dart";
+import "../core/funds_outcome.dart";
+import "../core/test_config.dart";
+import "../flows/auth_flows.dart";
+import "../flows/funds_flows.dart";
+import "../robots/home_page_robot.dart";
+import "../robots/new_send_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Every funded chain can build a transaction without sending it", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final authFlows = AuthFlows(tester);
+ final fundsFlows = FundsFlows(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final sendRobot = NewSendPageRobot(tester);
+
+ if (!TestConfig.shouldDryRun("send")) {
+ tester.printToConsole("FLOWS excludes send, skipping");
+ return;
+ }
+
+ final walletTypes = TestConfig.fundedWalletTypesUnderTest;
+
+ expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
+
+ await appLauncher.launchApp(testKey: "send_dry_run_test_app_key");
+
+ final appStore = getIt.get<AppStore>();
+ final failures = <String>[];
+
+ for (final type in walletTypes) {
+ tester.printToConsole("Send dry run starting for ${type.name}");
+
+ try {
+ final opened = await fundsFlows.openFundedWallet(type);
+
+ if (!opened) {
+ tester.printToConsole("${type.name} has nothing spendable, top it up");
+ FundsOutcome.empty(tester, type.name, "every funded wallet is empty");
+ continue;
+ }
+
+ expect(appStore.wallet?.type, type);
+
+ final ownAddress = appStore.wallet!.walletAddresses.address;
+
+ await homePageRobot.openSendSheet();
+ await sendRobot.isDisplayed();
+
+ await sendRobot.enterAddress(ownAddress);
+ await sendRobot.enterAmount(TestConfig.fundsSendAmountFor(type));
+ await sendRobot.tapSendButtonWhenReady();
+
+ await authFlows.authenticateWithPin(required: false);
+
+ await sendRobot.confirmTransactionBuilt();
+
+ await sendRobot.confirmSendButtonReplacesSwiper();
+
+ tester.printToConsole("${type.name} built a transaction, leaving it unsent");
+ FundsOutcome.ok(tester, type.name, "built and signed, not broadcast");
+
+ expect(
+ await fundsFlows.returnToHome(),
+ true,
+ reason: "${type.name} finished but the send flow would not close",
+ );
+ } catch (e) {
+ if (FundsOutcome.needsFurtherReview(e.toString())) {
+ failures.add("${type.name}: $e");
+ } else {
+ tester.printToConsole("${type.name} needs attention rather than a fix: $e");
+ }
+
+ FundsOutcome.failed(tester, type.name, e.toString());
+
+ if (!await fundsFlows.returnToHome()) {
+ // The app is hanging or found itself somewhere we don't know,
+ // no need to continue running anything else
+ for (final skipped in walletTypes.skip(walletTypes.indexOf(type) + 1)) {
+ FundsOutcome.waiting(
+ tester,
+ skipped.name,
+ "never ran, ${type.name} left the app somewhere it could not come back from",
+ );
+ }
+
+ if (FundsOutcome.needsFurtherReview(e.toString())) {
+ failures.add("${type.name}: could not get back to the home page, "
+ "the chains after this one never ran");
+ }
+
+ break;
+ }
+ }
+ }
+
+ expect(failures, isEmpty, reason: "Failed chains:\n${failures.join("\n")}");
+ });
+}
### integration_test/funds_suites/send_funds_test.dart
@@ -0,0 +1,109 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../core/app_launcher.dart";
+import "../core/funds_outcome.dart";
+import "../core/test_config.dart";
+import "../flows/auth_flows.dart";
+import "../flows/funds_flows.dart";
+import "../robots/home_page_robot.dart";
+import "../robots/new_send_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ // tests everything up to the point of swiping
+ integrationTest("Send a real self transaction on every funded chain", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final fundsFlows = FundsFlows(tester);
+ final authFlows = AuthFlows(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final sendRobot = NewSendPageRobot(tester);
+
+ if (!TestConfig.shouldSpend("send")) {
+ tester.printToConsole("FLOWS excludes send, skipping");
+ return;
+ }
+
+ final walletTypes = TestConfig.fundedWalletTypesUnderTest;
+
+ expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
+
+ await appLauncher.launchApp(testKey: "send_funds_test_app_key");
+
+ final appStore = getIt.get<AppStore>();
+ final failures = <String>[];
+
+ for (final type in walletTypes) {
+ tester.printToConsole("Funds send starting for ${type.name}");
+
+ try {
+ final opened = await fundsFlows.openFundedWallet(type);
+
+ if (!opened) {
+ tester.printToConsole("${type.name} has nothing spendable, top it up");
+ FundsOutcome.empty(tester, type.name, "every funded wallet is empty");
+ continue;
+ }
+
+ expect(appStore.wallet?.type, type);
+
+ // Making it a self send so the funds remain inside the funded wallet, only fees are spent.
+ final ownAddress = appStore.wallet!.walletAddresses.address;
+
+ await homePageRobot.openSendSheet();
+ await sendRobot.isDisplayed();
+
+ await sendRobot.enterAddress(ownAddress);
+ await sendRobot.enterAmount(TestConfig.fundsSendAmountFor(type));
+ await sendRobot.tapSendButtonWhenReady();
+
+ await authFlows.authenticateWithPin(required: false);
+
+ await sendRobot.swipeToConfirm();
+ await sendRobot.confirmTransactionCommitted();
+
+ // The send page sits under the confirm sheet, so one pop leaves it up and the next
+ // chain would try to restore from behind it.
+ expect(
+ await fundsFlows.returnToHome(),
+ true,
+ reason: "${type.name} broadcast but the send flow would not close",
+ );
+
+ tester.printToConsole("FUNDS_SEND_OK: ${type.name}");
+ FundsOutcome.ok(tester, type.name, "self send broadcast");
+ } catch (e) {
+ if (FundsOutcome.needsFurtherReview(e.toString())) {
+ failures.add("${type.name}: $e");
+ } else {
+ tester.printToConsole("${type.name} needs attention rather than a fix: $e");
+ }
+
+ FundsOutcome.failed(tester, type.name, e.toString());
+ tester.printToConsole("FUNDS_SEND_FAIL: ${type.name}: $e");
+
+ if (!await fundsFlows.returnToHome()) {
+ for (final skipped in walletTypes.skip(walletTypes.indexOf(type) + 1)) {
+ FundsOutcome.waiting(
+ tester,
+ skipped.name,
+ "never ran, ${type.name} left the app somewhere it could not come back from",
+ );
+ }
+
+ if (FundsOutcome.needsFurtherReview(e.toString())) {
+ failures.add("${type.name}: could not get back to the home page, "
+ "the chains after this one never ran");
+ }
+
+ break;
+ }
+ }
+ }
+
+ expect(failures, isEmpty, reason: "Failed chains:\n${failures.join("\n")}");
+ });
+}
### integration_test/funds_suites/swap_dry_run_test.dart
@@ -0,0 +1,69 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/crypto_currency.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../core/app_launcher.dart";
+import "../core/funds_outcome.dart";
+import "../core/test_config.dart";
+import "../flows/funds_flows.dart";
+import "../robots/home_page_robot.dart";
+import "../robots/new_swap_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ // tests everything up to the point of creating the trade
+ integrationTest("Swap prices a real deposit without creating a trade", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final fundsFlows = FundsFlows(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final swapRobot = NewSwapPageRobot(tester);
+
+ if (!TestConfig.shouldDryRun("swap")) {
+ tester.printToConsole("FLOWS excludes swap, skipping");
+ return;
+ }
+
+ final walletTypes = TestConfig.fundedWalletTypesUnderTest;
+
+ expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
+
+ await appLauncher.launchApp(testKey: "swap_dry_run_test_app_key");
+
+ final appStore = getIt.get<AppStore>();
+
+ final type = TestConfig.swapDepositType(walletTypes);
+
+ expect(
+ walletTypes,
+ contains(type),
+ reason: "SWAP_FROM=${type.name} is not among the funded wallets under test",
+ );
+
+ final opened = await fundsFlows.openFundedWallet(type);
+
+ expect(opened, true, reason: "Every funded wallet for ${type.name} came up empty");
+ expect(appStore.wallet?.type, type);
+
+ await homePageRobot.openSwapSheet();
+ await swapRobot.isDisplayed();
+
+ if (TestConfig.swapReceive.isNotEmpty) {
+ await swapRobot.chooseReceiveCurrency(CryptoCurrency.fromString(TestConfig.swapReceive));
+ }
+
+ await swapRobot.enterMinimumViableDepositAmount();
+ await swapRobot.enterTestReceiveAddress();
+
+ await swapRobot.confirmQuoteReceived();
+ await swapRobot.confirmSwapButtonEnabled();
+
+ tester.printToConsole("${type.name} got a quote and the swap button enabled, no trade created");
+ FundsOutcome.ok(tester, type.name, "quoted, swap button enabled, no trade created");
+
+ await swapRobot.dismissModal();
+ await homePageRobot.isDisplayed();
+ });
+}
### integration_test/funds_suites/swap_funds_test.dart
@@ -0,0 +1,86 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/crypto_currency.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../core/app_launcher.dart";
+import "../core/funds_outcome.dart";
+import "../core/test_config.dart";
+import "../core/test_wallets.dart";
+import "../flows/auth_flows.dart";
+import "../flows/funds_flows.dart";
+import "../robots/home_page_robot.dart";
+import "../robots/new_send_page_robot.dart";
+import "../robots/new_swap_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Create a real swap and broadcast its deposit", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final fundsFlows = FundsFlows(tester);
+ final authFlows = AuthFlows(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final sendRobot = NewSendPageRobot(tester);
+ final swapRobot = NewSwapPageRobot(tester);
+
+ if (!TestConfig.shouldSpend("swap")) {
+ tester.printToConsole("FLOWS excludes swap, skipping");
+ return;
+ }
+
+ final walletTypes = TestConfig.fundedWalletTypesUnderTest;
+
+ expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
+
+ final type = TestConfig.swapDepositType(walletTypes);
+
+ expect(
+ walletTypes,
+ contains(type),
+ reason: "SWAP_FROM=${type.name} is not among the funded wallets under test",
+ );
+
+ await appLauncher.launchApp(testKey: "swap_funds_test_app_key");
+
+ final opened = await fundsFlows.openFundedWallet(type);
+
+ expect(
+ opened,
+ true,
+ reason: "${type.name}: none of the ${TestWallets.fundedWalletsFor(type).length} funded "
+ "wallets has a spendable balance, top them up before the next run",
+ );
+
+ final appStore = getIt.get<AppStore>();
+ expect(appStore.wallet?.type, type);
+
+ await homePageRobot.openSwapSheet();
+ await swapRobot.isDisplayed();
+
+ if (TestConfig.swapReceive.isNotEmpty) {
+ await swapRobot.chooseReceiveCurrency(CryptoCurrency.fromString(TestConfig.swapReceive));
+ }
+
+ await swapRobot.enterMinimumViableDepositAmount();
+ final payoutAddress = await swapRobot.enterTestReceiveAddress();
+ await swapRobot.confirmQuoteReceived();
+ await swapRobot.confirmSwapButtonEnabled();
+
+ await swapRobot.tapSwapButton();
+
+ await authFlows.authenticateWithPin(required: false);
+
+ await swapRobot.confirmTradeCreated();
+ swapRobot.confirmPayoutAddress(payoutAddress);
+
+ await sendRobot.swipeToConfirm();
+ await swapRobot.confirmDepositCommitted();
+
+ await homePageRobot.isDisplayed();
+
+ tester.printToConsole("FUNDS_SWAP_OK: ${type.name}");
+ FundsOutcome.ok(tester, type.name, "trade created and deposit broadcast");
+ });
+}
### integration_test/helpers/mocks.dart
@@ -1,27 +0,0 @@
-import 'package:cake_wallet/core/auth_service.dart';
-import 'package:cake_wallet/core/secure_storage.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:cake_wallet/store/authentication_store.dart';
-import 'package:cake_wallet/store/settings_store.dart';
-import 'package:cake_wallet/store/wallet_list_store.dart';
-import 'package:cake_wallet/view_model/link_view_model.dart';
-import 'package:hive/hive.dart';
-import 'package:mocktail/mocktail.dart';
-
-class MockAppStore extends Mock implements AppStore {}
-
-class MockAuthService extends Mock implements AuthService {}
-
-class MockSettingsStore extends Mock implements SettingsStore {}
-
-class MockAuthenticationStore extends Mock implements AuthenticationStore {}
-
-class MockWalletListStore extends Mock implements WalletListStore {}
-
-class MockLinkViewModel extends Mock implements LinkViewModel {}
-
-class MockHiveInterface extends Mock implements HiveInterface {}
-
-class MockHiveBox extends Mock implements Box<dynamic> {}
-
-class MockSecureStorage extends Mock implements SecureStorage {}
### integration_test/helpers/test_helpers.dart
@@ -1,99 +0,0 @@
-import 'package:cake_wallet/core/auth_service.dart';
-import 'package:cake_wallet/core/secure_storage.dart';
-import 'package:cake_wallet/di.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:cake_wallet/store/authentication_store.dart';
-import 'package:cake_wallet/store/settings_store.dart';
-import 'package:cake_wallet/store/wallet_list_store.dart';
-import 'package:cake_wallet/view_model/link_view_model.dart';
-import 'package:hive/hive.dart';
-import 'package:mocktail/mocktail.dart';
-
-import 'mocks.dart';
-
-class TestHelpers {
- static void setup() {
- // Fallback values can also be declared here
- registerDependencies();
- }
-
- static void registerDependencies() {
- getAndRegisterAppStore();
- getAndRegisterAuthService();
- getAndRegisterSettingsStore();
- getAndRegisterAuthenticationStore();
- getAndRegisterWalletListStore();
-
- getAndRegisterLinkViewModel();
- getAndRegisterSecureStorage();
- getAndRegisterHiveInterface();
- }
-
- static MockSettingsStore getAndRegisterSettingsStore() {
- _removeRegistrationIfExists<SettingsStore>();
- final service = MockSettingsStore();
- getIt.registerSingleton<SettingsStore>(service);
- return service;
- }
-
- static MockAppStore getAndRegisterAppStore() {
- _removeRegistrationIfExists<AppStore>();
- final service = MockAppStore();
- final settingsStore = getAndRegisterSettingsStore();
-
- when(() => service.settingsStore).thenAnswer((invocation) => settingsStore);
- getIt.registerSingleton<AppStore>(service);
- return service;
- }
-
- static MockAuthService getAndRegisterAuthService() {
- _removeRegistrationIfExists<AuthService>();
- final service = MockAuthService();
- getIt.registerSingleton<AuthService>(service);
- return service;
- }
-
- static MockAuthenticationStore getAndRegisterAuthenticationStore() {
- _removeRegistrationIfExists<AuthenticationStore>();
- final service = MockAuthenticationStore();
- when(() => service.state).thenReturn(AuthenticationState.uninitialized);
- getIt.registerSingleton<AuthenticationStore>(service);
- return service;
- }
-
- static MockWalletListStore getAndRegisterWalletListStore() {
- _removeRegistrationIfExists<WalletListStore>();
- final service = MockWalletListStore();
- getIt.registerSingleton<WalletListStore>(service);
- return service;
- }
-
- static MockLinkViewModel getAndRegisterLinkViewModel() {
- _removeRegistrationIfExists<LinkViewModel>();
- final service = MockLinkViewModel();
- getIt.registerSingleton<LinkViewModel>(service);
- return service;
- }
-
- static MockHiveInterface getAndRegisterHiveInterface() {
- _removeRegistrationIfExists<HiveInterface>();
- final service = MockHiveInterface();
- getIt.registerSingleton<HiveInterface>(service);
- return service;
- }
-
- static MockSecureStorage getAndRegisterSecureStorage() {
- _removeRegistrationIfExists<SecureStorage>();
- final service = MockSecureStorage();
- getIt.registerSingleton<SecureStorage>(service);
- return service;
- }
-
- static void _removeRegistrationIfExists<T extends Object>() {
- if (getIt.isRegistered<T>()) {
- getIt.unregister<T>();
- }
- }
-
- static void tearDown() => getIt.reset();
-}
### integration_test/integration_response_data.json
@@ -1 +0,0 @@
-null
\ No newline at end of file
### integration_test/robots/auth_page_robot.dart
@@ -1,36 +1,20 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/auth/auth_page.dart';
-import 'package:flutter/widgets.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/auth/auth_page.dart";
+import "package:flutter/widgets.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
-import 'pin_code_widget_robot.dart';
+import "pin_code_widget_robot.dart";
class AuthPageRobot extends PinCodeWidgetRobot {
- AuthPageRobot(this.tester)
- : commonTestCases = CommonTestCases(tester),
- super(tester);
+ AuthPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<AuthPage>();
+ }
bool onAuthPage() {
- final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key'));
+ final hasPinButtons = find.byKey(const ValueKey("pin_code_button_3_key"));
final hasPin = hasPinButtons.tryEvaluate();
return hasPin;
}
-
- bool onAuthPageDesktop() {
- final hasWalletPasswordInput = find.byKey(ValueKey('enter_wallet_password'));
- return hasWalletPasswordInput.tryEvaluate();
- }
-
- Future<void> isAuthPage() async {
- await commonTestCases.isSpecificPage<AuthPage>();
- await commonTestCases.takeScreenshots('auth_page');
- }
-
- void hasTitle() {
- commonTestCases.hasText(S.current.setup_pin);
- }
}
### integration_test/robots/connection_sync_page_robot.dart
@@ -0,0 +1,40 @@
+import "package:cake_wallet/entities/fiat_api_mode.dart";
+import "package:cake_wallet/src/screens/settings/connection_sync_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class ConnectionSyncPageRobot extends BaseRobot {
+ ConnectionSyncPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<ConnectionSyncPage>();
+ }
+
+ Future<void> setFiatApiMode(FiatApiMode mode) async {
+ await tapByKey("fiat_api");
+
+ final unselected = find.byKey(ValueKey("picker_items_index_${mode.title}_button_key"));
+ final selected =
+ find.byKey(ValueKey("picker_items_index_${mode.title}_selected_item_button_key"));
+
+ await pumpUntil(() => tester.any(unselected) || tester.any(selected));
+
+ await tester.tap(tester.any(unselected) ? unselected : selected);
+
+ await settle();
+ }
+
+ void hasFiatApiMode(FiatApiMode mode) {
+ expect(
+ find.descendant(
+ of: find.byKey(const ValueKey("fiat_api")),
+ matching: find.text(mode.title),
+ ),
+ findsOneWidget,
+ reason: "The fiat api row does not show ${mode.title}",
+ );
+ }
+}
### integration_test/robots/contact_robot.dart
@@ -0,0 +1,66 @@
+import "package:cake_wallet/src/screens/contact/contact_list_page.dart";
+import "package:cake_wallet/src/screens/contact/contact_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class ContactRobot extends BaseRobot {
+ ContactRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await pumpUntilFound(find.byType(ContactListPage));
+ }
+
+ Future<void> openAddContact() async {
+ await tapByKey("contact_list_page_add_contact_button_key");
+
+ await pumpUntilFound(find.byType(ContactPage));
+ }
+
+ Future<void> enterName(String name) async {
+ await enterTextByKey("contact_page_name_textfield_key", name);
+ }
+
+ Future<void> chooseCurrency(String currencyName) async {
+ await tapByKey("contact_page_currency_picker_button_key");
+
+ await tapByKey("picker_items_index_${currencyName}_button_key");
+
+ await settle();
+ }
+
+ Future<void> enterAddress(String address) async {
+ final input = find.byKey(const ValueKey("contact_page_address_input_key"));
+
+ await pumpUntilFound(input);
+
+ final field = find.descendant(of: input, matching: find.byType(EditableText));
+
+ await tester.enterText(field.first, address);
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+
+ Future<void> save() async {
+ await tapByKey("contact_page_save_button_key");
+
+ await pumpUntilGone(find.byType(ContactPage));
+
+ await settle();
+ }
+
+ void expectContactSaved(String name, String address) {
+ final page = tester.widget<ContactListPage>(find.byType(ContactListPage));
+
+ final saved = page.contactListViewModel.contacts.where((c) => c.name == name).toList();
+
+ expect(saved, isNotEmpty, reason: "The contact $name was never saved");
+
+ expect(
+ saved.first.address,
+ address,
+ reason: "The address book stored a different address than it was given",
+ );
+ }
+}
### integration_test/robots/create_pin_welcome_page_robot.dart
@@ -1,46 +1,16 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/welcome/create_pin_welcome_page.dart';
-import 'package:cake_wallet/wallet_type_utils.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/welcome/create_pin_welcome_page.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class CreatePinWelcomePageRobot {
- CreatePinWelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class CreatePinWelcomePageRobot extends BaseRobot {
+ CreatePinWelcomePageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isCreatePinWelcomePage() async {
- await commonTestCases.isSpecificPage<CreatePinWelcomePage>();
- await commonTestCases.takeScreenshots('create_pin_welcome_page');
- }
-
- void hasTitle() {
- String title;
- if (isMoneroOnly) {
- title = S.current.monero_com;
- }
-
- title = S.current.cake_wallet;
-
- commonTestCases.hasText(title);
- }
-
- void hasDescription() {
- String description;
- if (isMoneroOnly) {
- description = S.current.monero_com_wallet_text;
- }
-
- description = S.current.new_first_wallet_text;
-
- commonTestCases.hasText(description);
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<CreatePinWelcomePage>();
}
Future<void> tapSetAPinButton() async {
- await commonTestCases.tapItemByKey('create_pin_welcome_page_create_a_pin_button_key');
-
- await commonTestCases.defaultSleepTime();
+ await tapByKey("create_pin_welcome_page_create_a_pin_button_key");
}
}
### integration_test/robots/dashboard_menu_widget_robot.dart
@@ -1,40 +0,0 @@
-import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-
-class DashboardMenuWidgetRobot {
- DashboardMenuWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> hasMenuWidget() async {
- await commonTestCases.takeScreenshots('menu_widget_page');
- commonTestCases.hasType<MenuWidget>();
- }
-
- void displaysTheCorrectWalletNameAndSubName() {
- final menuWidgetState = tester.state<MenuWidgetState>(find.byType(MenuWidget));
-
- final walletName = menuWidgetState.widget.dashboardViewModel.name;
- commonTestCases.hasText(walletName);
-
- final walletSubName = menuWidgetState.widget.dashboardViewModel.subname;
- if (walletSubName.isNotEmpty) {
- commonTestCases.hasText(walletSubName);
- }
- }
-
- Future<void> navigateToWalletMenu() async {
- await commonTestCases.tapItemByKey('dashboard_page_menu_widget_wallet_menu_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> navigateToSecurityAndBackupPage() async {
- await commonTestCases.tapItemByKey(
- 'dashboard_page_menu_widget_security_and_backup_button_key',
- );
- await commonTestCases.defaultSleepTime();
- }
-}
### integration_test/robots/dashboard_page_robot.dart
@@ -1,122 +0,0 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
-import 'package:cake_wallet/src/screens/dashboard/pages/balance/crypto_balance_widget.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-import 'dashboard_menu_widget_robot.dart';
-
-class DashboardPageRobot {
- DashboardPageRobot(this.tester)
- : commonTestCases = CommonTestCases(tester),
- dashboardMenuWidgetRobot = DashboardMenuWidgetRobot(tester);
-
- final WidgetTester tester;
- final DashboardMenuWidgetRobot dashboardMenuWidgetRobot;
- late CommonTestCases commonTestCases;
-
- Future<void> isDashboardPage() async {
- await commonTestCases.isSpecificPage<DashboardPage>();
- await commonTestCases.takeScreenshots('dashboard_page');
- }
-
- Future<void> confirmWalletTypeIsDisplayedCorrectly(
- WalletType type, {
- bool isHaven = false,
- }) async {
- final cryptoBalanceWidget =
- tester.widget<CryptoBalanceWidget>(find.byType(CryptoBalanceWidget));
- final hasAccounts = cryptoBalanceWidget.dashboardViewModel.balanceViewModel.hasAccounts;
-
- if (hasAccounts) {
- final walletName = cryptoBalanceWidget.dashboardViewModel.name;
- commonTestCases.hasText(walletName);
- } else {
- final walletName = walletTypeToString(type);
- final assetName = isHaven ? '$walletName Assets' : walletName;
- commonTestCases.hasText(assetName);
- }
- await commonTestCases.defaultSleepTime(seconds: 5);
- }
-
- void confirmServiceUpdateButtonDisplays() {
- commonTestCases.hasValueKey('dashboard_page_services_update_button_key');
- }
-
- void confirmSyncIndicatorButtonDisplays() {
- commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key');
- }
-
- void confirmMenuButtonDisplays() {
- commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key');
- }
-
- Future<void> confirmRightCryptoAssetTitleDisplaysPerPageView(
- WalletType type, {
- bool isHaven = false,
- }) async {
- //Balance Page
- await confirmWalletTypeIsDisplayedCorrectly(type, isHaven: isHaven);
-
- // Swipe to Cake features Page
- await swipeDashboardTab(false);
- commonTestCases.hasText('Cake ${S.current.features}');
-
- // Swipe back to balance
- await swipeDashboardTab(true);
-
- // Swipe to Transactions Page
- await swipeDashboardTab(true);
- commonTestCases.hasText(S.current.transactions);
-
- // Swipe back to balance
- await swipeDashboardTab(false);
- await commonTestCases.defaultSleepTime(seconds: 3);
- }
-
- Future<void> swipeDashboardTab(bool swipeRight) async {
- await commonTestCases.swipeByPageKey(
- key: 'dashboard_page_view_key',
- swipeRight: swipeRight,
- );
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> openDrawerMenu() async {
- await commonTestCases.tapItemByKey('dashboard_page_wallet_menu_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> navigateToBuyPage() async {
- await commonTestCases.tapItemByKey('dashboard_page_buy_action_button_key');
- }
-
- Future<void> navigateToWalletsListPage({bool isDrawer = false}) async {
- await tester.pumpAndSettle(Duration(milliseconds: 1000));
-
- await commonTestCases.tapItemByKey(
- isDrawer
- ? 'dashboard_page_menu_widget_wallet_menu_button_key'
- : 'dashboard_page__wallet_list_button_key',
- );
-
- await tester.pumpAndSettle(Duration(milliseconds: 2000));
- }
-
- Future<void> navigateToSendPage() async {
- await commonTestCases.tapItemByKey('dashboard_page_send_action_button_key');
- }
-
- Future<void> navigateToSellPage() async {
- await commonTestCases.tapItemByKey('dashboard_page_sell_action_button_key');
- }
-
- Future<void> navigateToReceivePage() async {
- await commonTestCases.tapItemByKey('dashboard_page_receive_action_button_key');
- }
-
- Future<void> navigateToExchangePage() async {
- await commonTestCases.tapItemByKey('dashboard_page_swap_action_button_key');
- }
-}
### integration_test/robots/disclaimer_page_robot.dart
@@ -1,40 +0,0 @@
-import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-
-class DisclaimerPageRobot {
- DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isDisclaimerPage() async {
- await commonTestCases.isSpecificPage<DisclaimerPage>();
- await commonTestCases.takeScreenshots('disclaimer_page');
- }
-
- void hasCheckIcon(bool hasBeenTapped) {
- // The checked Icon should not be available initially, until user taps the checkbox
- final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key'));
- expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing);
- }
-
- void hasDisclaimerCheckbox() {
- final checkBox = find.byKey(ValueKey('disclaimer_check_key'));
- expect(checkBox, findsOneWidget);
- }
-
- Future<void> tapDisclaimerCheckbox() async {
- await commonTestCases.tapItemByKey('disclaimer_check_key');
-
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> tapAcceptButton() async {
- await commonTestCases.tapItemByKey('disclaimer_accept_button_key');
-
- await commonTestCases.defaultSleepTime();
- }
-}
### integration_test/robots/display_settings_page_robot.dart
@@ -0,0 +1,78 @@
+import "package:cake_wallet/entities/fiat_currency.dart";
+import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
+import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
+import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart";
+import "package:cake_wallet/src/screens/settings/display_settings_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class DisplaySettingsPageRobot extends BaseRobot {
+ DisplaySettingsPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<DisplaySettingsPage>();
+ }
+
+ Future<void> openLanguagePicker() async {
+ await tapByKey("display_settings_language");
+ }
+
+ Future<void> chooseLanguage(String code) async {
+ await tapByKey("picker_items_index_${code}_button_key");
+
+ await settle();
+ }
+
+ bool hasFiatCurrencyRow() =>
+ tester.any(find.byKey(const ValueKey("display_settings_fiat_currency")));
+
+ Future<void> openFiatCurrencyPicker() async {
+ await tapByKey("display_settings_fiat_currency");
+
+ await pumpUntilFound(find.byType(FiatCurrencyPickerSheet));
+ }
+
+ Future<void> chooseFiatCurrency(FiatCurrency currency) async {
+ final searchField = find.descendant(
+ of: find.byType(CurrencyPickerSearchField),
+ matching: find.byType(TextFormField),
+ );
+
+ await tester.enterText(searchField, currency.fullName);
+
+ final row = find.byWidgetPredicate(
+ (widget) => widget is FiatCurrencyRow && widget.currency == currency,
+ );
+
+ await pumpUntilFound(row);
+
+ await tester.tap(row.first);
+
+ await pumpUntilGone(find.byType(FiatCurrencyPickerSheet));
+ }
+
+ void expectFiatCurrencyShown(FiatCurrency currency) {
+ expect(
+ find.descendant(
+ of: find.byKey(const ValueKey("display_settings_fiat_currency")),
+ matching: find.text(currency.title),
+ ),
+ findsOneWidget,
+ reason: "The currency row still does not show ${currency.title}",
+ );
+ }
+
+ void expectLanguageShown(String displayName) {
+ expect(
+ find.descendant(
+ of: find.byKey(const ValueKey("display_settings_language")),
+ matching: find.text(displayName),
+ ),
+ findsOneWidget,
+ reason: "The language row still does not show $displayName",
+ );
+ }
+}
### integration_test/robots/exchange_confirm_page_robot.dart
@@ -1,46 +0,0 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-
-class ExchangeConfirmPageRobot {
- ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isExchangeConfirmPage() async {
- await commonTestCases.isSpecificPage<ExchangeConfirmPage>();
- await commonTestCases.takeScreenshots('exchange_confirm_page');
- }
-
- void confirmComponentsOfTradeDisplayProperly() {
- final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
- final trade = exchangeConfirmPage.trade;
-
- commonTestCases.hasText(trade.id);
- commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}');
-
- commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key');
- commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key');
- }
-
- Future<void> confirmCopyTradeIdToClipBoardWorksProperly() async {
- final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
- final trade = exchangeConfirmPage.trade;
-
- await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key');
-
- ClipboardData? clipboardData = await Clipboard.getData('text/plain');
-
- expect(clipboardData?.text, trade.id);
- }
-
- Future<void> onSavedTradeIdButtonPressed() async {
- await tester.pumpAndSettle();
- await commonTestCases.defaultSleepTime();
- await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key');
- }
-}
### integration_test/robots/exchange_page_robot.dart
@@ -1,338 +0,0 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
-import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
-import 'package:cw_core/crypto_currency.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-import '../components/common_test_constants.dart';
-import 'auth_page_robot.dart';
-
-class ExchangePageRobot {
- ExchangePageRobot(this.tester)
- : commonTestCases = CommonTestCases(tester),
- authPageRobot = AuthPageRobot(tester);
-
- WidgetTester tester;
- AuthPageRobot authPageRobot;
- CommonTestCases commonTestCases;
-
- Future<void> isExchangePage() async {
- await commonTestCases.isSpecificPage<ExchangePage>();
- await commonTestCases.takeScreenshots('exchange_page');
- await commonTestCases.defaultSleepTime();
- }
-
- void hasResetButton() {
- commonTestCases.hasText(S.current.reset);
- }
-
- void displaysPresentProviderPicker() {
- commonTestCases.hasType<PresentProviderPicker>();
- }
-
- Future<void> displayBothExchangeCards() async {
- final ExchangePage exchangeCard = tester.widget<ExchangePage>(
- find.byType(ExchangePage),
- );
-
- final depositKey = exchangeCard.depositKey;
- final receiveKey = exchangeCard.receiveKey;
-
- final depositExchangeCard = find.byKey(depositKey);
- expect(depositExchangeCard, findsOneWidget);
-
- final receiveExchangeCard = find.byKey(receiveKey);
- expect(receiveExchangeCard, findsOneWidget);
- }
-
- void confirmRightComponentsDisplayOnDepositExchangeCards() {
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
- final depositCardPrefix = 'deposit_exchange_card';
-
- commonTestCases.hasValueKey('${depositCardPrefix}_title_key');
- commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key');
- commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key');
- commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key');
-
- exchangePage.depositKey.currentState!.changeLimits(min: '0.1');
-
- commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key');
-
- final initialCurrency = exchangeViewModel.depositCurrency;
- if (initialCurrency.tag != null) {
- commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key');
- }
-
- if (exchangeViewModel.hasAllAmount) {
- commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key');
- }
-
- if (exchangeViewModel.isMoneroWallet) {
- commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key');
- }
-
- if (exchangeViewModel.isDepositAddressEnabled) {
- commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key');
- } else {
- commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key');
- commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key');
- }
-
- // commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key');
- }
-
- void confirmRightComponentsDisplayOnReceiveExchangeCards() {
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
- final receiveCardPrefix = 'receive_exchange_card';
-
- commonTestCases.hasValueKey('${receiveCardPrefix}_title_key');
- commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key');
- commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key');
- commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key');
- commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key');
-
- final initialCurrency = exchangeViewModel.receiveCurrency;
- if (initialCurrency.tag != null) {
- commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key');
- }
-
- if (exchangeViewModel.hasAllAmount) {
- commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key');
- }
-
- if (exchangeViewModel.isMoneroWallet) {
- commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key');
- }
-
- commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key');
- }
-
- Future<void> selectDepositCurrency(CryptoCurrency depositCurrency) async {
- final depositPrefix = 'deposit_exchange_card';
- final currencyPickerKey = '${depositPrefix}_currency_picker_button_key';
- final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key';
-
- await commonTestCases.tapItemByKey(currencyPickerKey);
- commonTestCases.hasValueKey(currencyPickerDialogKey);
-
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
-
- if (depositCurrency == exchangeViewModel.depositCurrency) {
- await commonTestCases.defaultSleepTime();
- await commonTestCases
- .tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key');
- return;
- }
-
- await commonTestCases.enterText(depositCurrency.name, 'search_bar_widget_key');
-
- await commonTestCases.defaultSleepTime();
-
- await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key');
- }
-
- Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
- final receivePrefix = 'receive_exchange_card';
- final currencyPickerKey = '${receivePrefix}_currency_picker_button_key';
- final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key';
-
- await commonTestCases.tapItemByKey(currencyPickerKey);
- commonTestCases.hasValueKey(currencyPickerDialogKey);
-
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
-
- if (receiveCurrency == exchangeViewModel.receiveCurrency) {
- await commonTestCases
- .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
- return;
- }
-
- await commonTestCases.enterText(receiveCurrency.name, 'search_bar_widget_key');
-
- await commonTestCases.defaultSleepTime();
-
- await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
- }
-
- Future<void> enterDepositAmount(String amount) async {
- await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key');
- }
-
- Future<void> enterDepositRefundAddress({String? depositAddress}) async {
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
-
- if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) {
- await commonTestCases.enterText(
- depositAddress, 'deposit_exchange_card_editable_address_textfield_key');
- }
- }
-
- Future<void> enterReceiveAddress(String receiveAddress) async {
- await commonTestCases.enterText(
- receiveAddress,
- 'receive_exchange_card_editable_address_textfield_key',
- );
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onExchangeButtonPressed() async {
- await commonTestCases.tapItemByKey('exchange_page_exchange_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- bool hasMaxLimitError() {
- final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit);
-
- bool hasMaxError = maxErrorText.tryEvaluate();
-
- return hasMaxError;
- }
-
- bool hasMinLimitError() {
- final minErrorText = find.text(S.current.error_text_input_below_minimum_limit);
-
- bool hasMinError = minErrorText.tryEvaluate();
-
- return hasMinError;
- }
-
- bool hasTradeCreationFailureError() {
- final tradeCreationFailureDialogButton =
- find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key'));
-
- bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate();
- tester.printToConsole('Trade not created error: $hasTradeCreationFailure');
- return hasTradeCreationFailure;
- }
-
- Future<void> onTradeCreationFailureDialogButtonPressed() async {
- await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key');
- }
-
- /// Handling Trade Failure Errors or errors shown through the Failure Dialog.
- ///
- /// Simulating the user's flow and response when this error comes up.
- /// Examples are:
- /// - No provider can handle this trade error,
- /// - Trade amount below limit error.
- Future<void> _handleTradeCreationFailureErrors() async {
- tester.printToConsole('Inside trade creation failure handle');
- bool isTradeCreationFailure = false;
-
- isTradeCreationFailure = hasTradeCreationFailureError();
-
- int maxRetries = 3;
- int retries = 0;
-
- while (isTradeCreationFailure && retries < maxRetries) {
- await tester.pumpAndSettle();
-
- await onTradeCreationFailureDialogButtonPressed();
-
- await commonTestCases.defaultSleepTime(seconds: 5);
-
- await onExchangeButtonPressed();
-
- await _handleAuthPage();
-
- isTradeCreationFailure = hasTradeCreationFailureError();
- retries++;
- }
- }
-
- /// Handles the min limit error.
- ///
- /// Simulates the user's flow and response when it comes up.
- ///
- /// Has a max retry of 20 times.
- Future<void> _handleMinLimitError(String initialAmount) async {
- double amount;
-
- amount = double.parse(initialAmount);
-
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
-
- amount = (exchangeViewModel.limits.min ?? 0.0) + 1.0;
-
- await enterDepositAmount(amount.toString());
-
- await tester.pumpAndSettle();
-
- await onExchangeButtonPressed();
-
- await tester.pumpAndSettle();
- }
-
- /// Handles the max limit error.
- ///
- /// Simulates the user's flow and response when it comes up.
- ///
- /// Has a max retry of 20 times.
- Future<void> _handleMaxLimitError(String initialAmount) async {
- double amount;
-
- amount = double.parse(initialAmount);
-
- ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
- final exchangeViewModel = exchangePage.exchangeViewModel;
-
- amount = (exchangeViewModel.limits.max ?? 0.0) - 1.0;
-
- await enterDepositAmount(amount.toString());
-
- await tester.pumpAndSettle();
-
- await onExchangeButtonPressed();
-
- await tester.pumpAndSettle();
- }
-
- Future<void> handleErrors(String initialAmount) async {
- await tester.pumpAndSettle();
-
- bool isMinLimitError = hasMinLimitError();
-
- if (isMinLimitError) {
- tester.printToConsole('Has min limit error');
- await _handleMinLimitError(initialAmount);
- }
-
- bool isMaxLimitError = hasMaxLimitError();
-
- if (isMaxLimitError) {
- await _handleMaxLimitError(initialAmount);
- }
-
- tester.printToConsole('No limits error, proceeding with flow');
-
- await _handleAuthPage();
-
- await tester.pumpAndSettle();
-
- await _handleTradeCreationFailureErrors();
-
- await tester.pumpAndSettle();
-
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> _handleAuthPage() async {
- final onAuthPage = authPageRobot.onAuthPage();
- if (onAuthPage) {
- await authPageRobot.enterPinCode(CommonTestConstants.pin);
- }
-
- final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
- if (onAuthPageDesktop) {
- await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
- }
- }
-}
### integration_test/robots/exchange_trade_external_send_page_robot.dart
@@ -1,35 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
-
-import '../components/common_test_cases.dart';
-
-class ExchangeTradeExternalSendPageRobot {
- ExchangeTradeExternalSendPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isExchangeTradeExternalSendPage() async {
- await commonTestCases.isSpecificPage<ExchangeTradeExternalSendPage>();
- await commonTestCases.takeScreenshots('exchange_trade_external_send_page');
- }
-
- Future<void> verifySendDetailsItemsDisplayProperly() async {
- final widget =
- tester.widget<ExchangeTradeExternalSendPage>(find.byType(ExchangeTradeExternalSendPage));
- final exchangeTradeViewModel = widget.exchangeTradeViewModel;
- final items = exchangeTradeViewModel.items.where((item) => item.isExternalSendDetail).toList();
-
- for (var item in items) {
- commonTestCases.hasValueKey('exchange_trade_external_send_page_send_item_${item.title}_key');
- tester.printToConsole('${item.title} present on screen');
- }
-
- commonTestCases.defaultSleepTime();
- }
-
- Future<void> onContinueButtonPressed() async {
- await commonTestCases.tapItemByKey('exchange_trade_external_send_page_continue_button_key');
- }
-}
### integration_test/robots/exchange_trade_page_robot.dart
@@ -1,161 +0,0 @@
-import 'dart:async';
-
-import 'package:cake_wallet/core/execution_state.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-
-class ExchangeTradePageRobot {
- ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isExchangeTradePage() async {
- await commonTestCases.isSpecificPage<ExchangeTradePage>();
- await commonTestCases.takeScreenshots('exchange_trade_page');
- }
-
- void hasInformationDialog() {
- commonTestCases.hasValueKey('information_page_dialog_key');
- }
-
- Future<void> onGotItButtonPressed() async {
- await commonTestCases.tapItemByKey('information_page_got_it_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onSendFromExternalButtonPressed() async {
- tester.printToConsole('Routing to send from external details page');
-
- await commonTestCases.tapItemByKey(
- 'exchange_trade_page_send_from_external_button_key',
- );
- }
-
- Future<void> onSendFromCakeButtonPressed() async {
- tester.printToConsole('Now sending from cake');
-
- await commonTestCases.tapItemByKey(
- 'exchange_trade_page_send_from_cake_button_key',
- shouldPumpAndSettle: false,
- );
-
- final Completer<void> completer = Completer<void>();
-
- // Loop to wait for the async operation to complete
- while (true) {
- await Future.delayed(Duration(seconds: 1));
-
- final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm));
- final execState = state.widget.exchangeTradeViewModel.sendViewModel.state;
-
- bool isDone = execState is ExecutedSuccessfullyState;
- bool isFailed = execState is FailureState;
-
- tester.printToConsole('isDone: $isDone');
- tester.printToConsole('isFailed: $isFailed');
-
- if (isDone || isFailed) {
- tester.printToConsole(
- isDone ? 'Completer is done' : 'Completer is done though operation failed');
- completer.complete();
- await tester.pump();
- break;
- } else {
- tester.printToConsole('Completer is not done');
- await tester.pump();
- }
- }
-
- await expectLater(completer.future, completes);
-
- tester.printToConsole('Done confirming sending');
-
- await commonTestCases.defaultSleepTime(seconds: 4);
- }
-
- Future<void> onSendButtonOnConfirmSendingDialogPressed() async {
- tester.printToConsole('Send Button on Confirm Dialog Triggered');
- await commonTestCases.defaultSleepTime(seconds: 4);
-
- final sendText = find.text(S.current.send);
- bool hasText = sendText.tryEvaluate();
-
- if (hasText) {
- await commonTestCases.tapItemByFinder(sendText);
-
- await commonTestCases.defaultSleepTime(seconds: 4);
- }
- }
-
- Future<void> onCancelButtonOnConfirmSendingDialogPressed() async {
- tester.printToConsole('Cancel Button on Confirm Dialog Triggered');
-
- await commonTestCases.tapItemByKey(
- 'exchange_trade_page_confirm_sending_dialog_cancel_button_key',
- );
-
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onSendFailureDialogButtonPressed() async {
- await commonTestCases.defaultSleepTime(seconds: 6);
-
- tester.printToConsole('Send Button Failure Dialog Triggered');
-
- await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key');
- }
-
- Future<bool> hasErrorWhileSending() async {
- await tester.pump();
-
- tester.printToConsole('Checking if there is an error');
-
- final errorDialog = find.byKey(
- ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
- );
-
- bool hasError = errorDialog.tryEvaluate();
-
- tester.printToConsole('Has error: $hasError');
-
- return hasError;
- }
-
- Future<void> handleConfirmSendResult() async {
- bool hasError = false;
-
- hasError = await hasErrorWhileSending();
-
- int maxRetries = 20;
- int retries = 0;
-
- while (hasError && retries < maxRetries) {
- tester.printToConsole('hasErrorInLoop: $hasError');
- await tester.pump();
-
- await onSendFailureDialogButtonPressed();
- tester.printToConsole('Failure button tapped');
-
- await commonTestCases.defaultSleepTime();
-
- await onSendFromCakeButtonPressed();
- tester.printToConsole('Confirm sending button tapped');
-
- hasError = await hasErrorWhileSending();
-
- retries++;
- }
-
- if (!hasError) {
- tester.printToConsole('No error, proceeding with flow');
- await tester.pump();
- }
-
- await commonTestCases.defaultSleepTime();
- }
-}
### integration_test/robots/home_page_robot.dart
@@ -0,0 +1,227 @@
+import "package:cake_wallet/new-ui/pages/home_page.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart";
+import "package:cw_core/sync_status.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class HomePageRobot extends BaseRobot {
+ HomePageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ final shown = await pumpUntil(() => isInFront);
+
+ expect(shown, true, reason: "The home tab never came to the front");
+ }
+
+ bool get isInFront {
+ final stackFinder = find.byType(IndexedStack);
+
+ if (!tester.any(stackFinder)) {
+ return false;
+ }
+
+ final stack = tester.widget<IndexedStack>(stackFinder.first);
+ final index = stack.children.indexWhere((child) => child is NewHomePage);
+
+ return index >= 0 && stack.index == index;
+ }
+
+ Future<void> hasWalletName(String name) async {
+ final nameFinder = find.byKey(const ValueKey("home_page_wallet_name_text_key"));
+ final syncBarFinder = find.byType(SyncBar);
+
+ final shown = await pumpUntil(() => tester.any(nameFinder) || tester.any(syncBarFinder));
+
+ expect(shown, true, reason: "Top bar showed neither the wallet name nor the sync bar");
+
+ if (!tester.any(nameFinder)) {
+ tester.printToConsole("Sync bar is in place of the wallet name, not checking it on screen");
+ return;
+ }
+
+ expect(textByKey("home_page_wallet_name_text_key"), name);
+ }
+
+ Future<void> openSendSheet() async {
+ await tapByKey("home_page_send_button_key");
+ }
+
+ Future<void> openReceiveSheet() async {
+ await tapByKey("home_page_receive_button_key");
+ }
+
+ Future<void> openSwapSheet() async {
+ await tapByKey("home_page_swap_button_key");
+ }
+
+ Future<void> openSettingsSheet() async {
+ await tapByKey("home_page_settings_button_key");
+ }
+
+ Future<void> openHistoryTab() async {
+ final tabFinder = find.byKey(const ValueKey("line_tab_switcher_1_key"));
+
+ if (!tester.any(tabFinder)) {
+ return;
+ }
+
+ await tapWhenVisible(tabFinder);
+
+ await settle();
+ }
+
+ Future<void> confirmTransactionHistoryVisible({
+ Duration timeout = const Duration(minutes: 3),
+ }) async {
+ final hasTransactions = await pumpUntil(
+ () => _dashboardTransactionCount() > 0,
+ timeout: timeout,
+ );
+
+ expect(
+ hasTransactions,
+ true,
+ reason: "No transactions arrived within ${timeout.inSeconds}s",
+ );
+
+ await openHistoryTab();
+
+ await pumpUntilFound(find.byType(HistoryTile));
+
+ expect(
+ tester.widgetList(find.byType(HistoryTile)).length,
+ lessThanOrEqualTo(3),
+ reason: "The home page preview should never render more than the short history",
+ );
+ }
+
+ Future<void> openAllTransactions() async {
+ final actionButton = find.byKey(const ValueKey("assets_history_action_button_key"));
+ final historyBar = find.byKey(const ValueKey("history_top_bar_key"));
+
+ await tapWhenVisible(tester.any(actionButton) ? actionButton : historyBar);
+
+ await pumpUntilFound(find.byKey(const ValueKey("history_modal_key")));
+ }
+
+ Future<void> confirmAllTransactionsVisible() async {
+ await pumpUntilFound(_allViewTiles);
+
+ final rendered = tester.widgetList(_allViewTiles).length;
+
+ if (_dashboardTransactionCount() > 3) {
+ expect(
+ rendered,
+ greaterThan(3),
+ reason: "The All view rendered $rendered tiles, no more than the home preview does",
+ );
+
+ return;
+ }
+
+ expect(_allViewTiles, findsWidgets);
+ }
+
+ String firstTransactionIdInAllView() {
+ final tile = tester.widgetList<HistoryTile>(_allViewTiles).first;
+ final key = tile.key! as ValueKey<String>;
+
+ return key.value.substring("home_page_transaction_".length, key.value.length - "_key".length);
+ }
+
+ Future<void> openFirstTransactionDetails() async {
+ await pumpUntilFound(_allViewTiles);
+
+ await tester.tap(_allViewTiles.first);
+
+ await pumpUntilFound(find.byType(TransactionDetailsModal));
+
+ await settle();
+ }
+
+ String openedTransactionId() {
+ final modal = tester.widget<TransactionDetailsModal>(find.byType(TransactionDetailsModal));
+
+ return modal.transactionDetailsViewModel.transactionInfo.id;
+ }
+
+ void hasTransactionIdRow() {
+ expect(
+ find.byKey(const ValueKey("standard_list_item_transaction_details_id_key")),
+ findsOneWidget,
+ reason: "The details opened without showing the transaction id",
+ );
+ }
+
+ // The home preview stays mounted underneath the modal, so its tiles have to be left out.
+ Finder get _allViewTiles => find.descendant(
+ of: find.byKey(const ValueKey("history_modal_key")),
+ matching: find.byType(HistoryTile),
+ );
+
+ Future<void> confirmSyncIndicatorShown({Duration timeout = const Duration(minutes: 3)}) async {
+ final syncBar = find.byType(SyncBar);
+ final tick = find.descendant(of: syncBar, matching: find.byIcon(Icons.check));
+
+ SyncStatus? lastStatus;
+ var sawIndicator = false;
+
+ final synced = await pumpUntil(
+ () {
+ lastStatus = _dashboardStatus();
+
+ if (lastStatus == null) {
+ return false;
+ }
+
+ final keyed = find.descendant(
+ of: syncBar,
+ matching: find.byKey(ValueKey(lastStatus.runtimeType)),
+ );
+
+ sawIndicator = sawIndicator || tester.any(keyed) || tester.any(tick);
+
+ return lastStatus is SyncedSyncStatus;
+ },
+ timeout: timeout,
+ );
+
+ expect(
+ synced,
+ true,
+ reason: "The wallet never synced within ${timeout.inMinutes}m, "
+ "it reported ${lastStatus.runtimeType}",
+ );
+
+ expect(
+ sawIndicator,
+ true,
+ reason: "The sync bar never showed a status while the wallet synced",
+ );
+ }
+
+ SyncStatus? _dashboardStatus() {
+ final finder = find.byType(NewHomePage);
+
+ if (!tester.any(finder)) {
+ return null;
+ }
+
+ return tester.widget<NewHomePage>(finder.first).dashboardViewModel.status;
+ }
+
+ int _dashboardTransactionCount() {
+ final finder = find.byType(NewHomePage);
+
+ if (!tester.any(finder)) {
+ return 0;
+ }
+
+ return tester.widget<NewHomePage>(finder.first).dashboardViewModel.transactions.length;
+ }
+}
### integration_test/robots/lightning_username_page_robot.dart
@@ -0,0 +1,17 @@
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class LightningUsernamePageRobot extends BaseRobot {
+ LightningUsernamePageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await pumpUntilFound(find.byKey(const ValueKey("lightning_username_page_skip_button_key")));
+ }
+
+ Future<void> tapSkip() async {
+ await tapByKey("lightning_username_page_skip_button_key");
+ }
+}
### integration_test/robots/manage_nodes_page_robot.dart
@@ -0,0 +1,39 @@
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart";
+import "package:cake_wallet/src/screens/settings/manage_nodes_page.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class ManageNodesPageRobot extends BaseRobot {
+ ManageNodesPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<ManageNodesPage>();
+ }
+
+ String currentNodeUri() {
+ final page = tester.widget<ManageNodesPage>(find.byType(ManageNodesPage));
+
+ return page.nodeListViewModel.currentNode.uriRaw;
+ }
+
+ Future<void> switchToAnotherNode() async {
+ final rows = find.byType(NodeListRow);
+
+ await pumpUntilFound(rows.first);
+
+ expect(
+ tester.widgetList(rows).length,
+ greaterThan(1),
+ reason: "Only the current node is listed, there is nothing to switch to",
+ );
+
+ await tapWhenVisible(rows.at(1));
+
+ await tapWhenVisible(find.text(S.current.change));
+
+ await settle();
+ }
+}
### integration_test/robots/new_dashboard_robot.dart
@@ -0,0 +1,25 @@
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class NewDashboardRobot extends BaseRobot {
+ NewDashboardRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed({Duration timeout = const Duration(seconds: 30)}) async {
+ await pumpUntilFound(find.byKey(const ValueKey("new_dashboard_page_key")), timeout: timeout);
+ }
+
+ Future<void> openHomeTab() async {
+ await tapByKey("dashboard_page_home_action_button_key");
+ }
+
+ Future<void> openWalletsTab() async {
+ await tapByKey("dashboard_page_wallets_action_button_key");
+ }
+
+ Future<void> openContactsTab() async {
+ await tapByKey("dashboard_page_contacts_action_button_key");
+ }
+}
### integration_test/robots/new_receive_page_robot.dart
@@ -0,0 +1,57 @@
+import "package:cake_wallet/new-ui/pages/receive_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class NewReceivePageRobot extends BaseRobot {
+ NewReceivePageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await pumpUntilFound(find.byType(NewReceivePage));
+ }
+
+ Future<void> confirmAddressMatches(String expected) async {
+ final wanted = _normalise(expected);
+
+ await pumpUntilFound(find.byKey(const ValueKey("receive_page_address_key")));
+
+ final matched = await pumpUntil(() => _renderedAddress() == wanted);
+
+ expect(
+ matched,
+ true,
+ reason: "Receive page should show $wanted but showed ${_renderedAddress()}",
+ );
+ }
+
+ String _normalise(String address) =>
+ address.replaceAll("bitcoincash:", "").replaceAll(RegExp(r"\s"), "");
+
+ String? _renderedAddress() {
+ final root = find.byKey(const ValueKey("receive_page_address_key"));
+
+ if (!tester.any(root)) {
+ return null;
+ }
+
+ final rich = find.descendant(of: root, matching: find.byType(RichText));
+
+ if (tester.any(rich)) {
+ final text = _normalise(tester.widget<RichText>(rich.first).text.toPlainText());
+
+ if (text.isNotEmpty) {
+ return text;
+ }
+ }
+
+ final plain = find.descendant(of: root, matching: find.byType(Text));
+
+ if (tester.any(plain)) {
+ return _normalise(tester.widget<Text>(plain.first).data ?? "");
+ }
+
+ return null;
+ }
+}
### integration_test/robots/new_send_page_robot.dart
@@ -0,0 +1,205 @@
+import "package:cake_wallet/core/execution_state.dart";
+import "package:cake_wallet/new-ui/pages/send_page.dart";
+import "package:cake_wallet/new-ui/widgets/confirm_swiper.dart";
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
+import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart";
+import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart";
+import "package:cake_wallet/view_model/send/send_view_model.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+import "../core/test_config.dart";
+
+class NewSendPageRobot extends BaseRobot {
+ NewSendPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await pumpUntilFound(find.byType(NewSendPage));
+ }
+
+ Future<void> enterAddress(String address) async {
+ await _enterTextInInput("send_page_address_input_key", address);
+ }
+
+ Future<void> enterAmount(String amount) async {
+ await _enterTextInInput("send_page_amount_input_key", amount);
+ }
+
+ String enteredAddress() => _textInInput("send_page_address_input_key");
+
+ Future<void> pickContactFromAddressBook(String contactName) async {
+ await tapByKey("send_page_address_book_button_key");
+
+ final tabs = find.byType(Tab);
+
+ await pumpUntilFound(tabs);
+
+ await tester.tap(tabs.at(1));
+
+ await settle();
+
+ final contactFinder = find.byKey(ValueKey(contactName));
+
+ await pumpUntilFound(contactFinder);
+
+ await tester.tap(contactFinder.first);
+
+ await pumpUntilGone(contactFinder);
+ }
+
+ Future<void> tapSendButton() async {
+ await tapByKey("send_page_send_button_key");
+ }
+
+ Future<void> tapSendButtonWhenReady({Duration timeout = TestConfig.walletSyncBudget}) async {
+ final ready = await pumpUntil(() => _model()?.isReadyForSend ?? false, timeout: timeout);
+
+ expect(
+ ready,
+ true,
+ reason: "The wallet never became ready to send within ${timeout.inMinutes}m, "
+ "so the send button stayed disabled",
+ );
+
+ await tapSendButton();
+ }
+
+ bool get isSendFlowOpen =>
+ tester.any(find.byType(NewSendPage)) || tester.any(find.byType(SendConfirmSheet));
+
+ Future<void> swipeToConfirm({Duration timeout = const Duration(seconds: 90)}) async {
+ final finder = find.byKey(const ValueKey("send_page_confirm_swiper_key"));
+
+ await pumpUntilFound(finder, timeout: timeout);
+
+ await settle(max: const Duration(seconds: 2));
+
+ final width = tester.view.physicalSize.width / tester.view.devicePixelRatio;
+ await tester.drag(finder.first, Offset(width, 0));
+
+ await tester.pump(const Duration(milliseconds: 500));
+ }
+
+ Future<void> confirmSendButtonReplacesSwiper() async {
+ final swiper = find.byKey(const ValueKey("send_page_confirm_swiper_key"));
+
+ await pumpUntilFound(swiper);
+
+ tester.binding.platformDispatcher.accessibilityFeaturesTestValue =
+ const FakeAccessibilityFeatures(accessibleNavigation: true);
+ addTearDown(tester.binding.platformDispatcher.clearAccessibilityFeaturesTestValue);
+
+ await settle(max: const Duration(seconds: 2));
+
+ final button = find.descendant(of: swiper, matching: find.byType(NewPrimaryButton));
+
+ expect(
+ tester.any(button),
+ true,
+ reason: "Accessible navigation is on but the sheet still only offers the swiper",
+ );
+
+ expect(
+ tester.widget<NewPrimaryButton>(button.first).onPressed,
+ isNotNull,
+ reason: "The send button a screen reader gets is disabled",
+ );
+ }
+
+ Future<void> confirmTransactionBuilt({Duration timeout = const Duration(minutes: 3)}) async {
+ await pumpUntil(
+ () => tester.any(find.byType(ConfirmSwiper)) || _model()?.state is FailureState,
+ timeout: timeout,
+ );
+
+ if (tester.any(find.byType(ConfirmSwiper))) {
+ return;
+ }
+
+ fail("No confirm sheet after ${timeout.inSeconds}s. ${_describeSendState()}");
+ }
+
+ Future<void> confirmTransactionCommitted({
+ Duration timeout = const Duration(minutes: 3),
+ }) async {
+ await pumpUntilFound(find.byType(TransactionCommittedScreenActionButton), timeout: timeout);
+ }
+
+ Future<void> expectNoTransactionBuilt({
+ Duration window = const Duration(seconds: 15),
+ }) async {
+ final offered = await pumpUntil(
+ () => tester.any(find.byType(ConfirmSwiper)),
+ timeout: window,
+ );
+
+ expect(
+ offered,
+ false,
+ reason:
+ "The send screen showed the swiper, wanting to broadcast a transaction it should have refused",
+ );
+ }
+
+ SendViewModel? _model() {
+ final sheet = find.byType(SendConfirmSheet);
+
+ if (tester.any(sheet)) {
+ return tester.widget<SendConfirmSheet>(sheet.first).sendViewModel;
+ }
+
+ final page = find.byType(NewSendPage);
+
+ if (tester.any(page)) {
+ return tester.widget<NewSendPage>(page.first).sendViewModel;
+ }
+
+ return null;
+ }
+
+ String _describeSendState() {
+ final model = _model();
+
+ final available = model?.wallet.balance.values
+ .map((balance) => balance.available)
+ .where((amount) => amount.sign > 0)
+ .join(", ");
+
+ final errorFinder = find.byType(TransactionErrorActions);
+ final error = tester.any(errorFinder)
+ ? tester.widget<TransactionErrorActions>(errorFinder.first).errorText
+ : model?.state is FailureState
+ ? (model!.state as FailureState).error
+ : null;
+
+ return "state=${model?.state.runtimeType}, "
+ "wallet holds ${available == null || available.isEmpty ? "nothing" : available}"
+ "${error == null ? "" : ", the chain said: $error"}";
+ }
+
+ String _textInInput(String inputKey) {
+ final inputFinder = find.byKey(ValueKey(inputKey));
+
+ if (!tester.any(inputFinder)) {
+ return "unknown";
+ }
+
+ final field = find.descendant(of: inputFinder, matching: find.byType(EditableText));
+
+ return tester.widget<EditableText>(field.first).controller.text;
+ }
+
+ Future<void> _enterTextInInput(String inputKey, String text) async {
+ final inputFinder = find.byKey(ValueKey(inputKey));
+
+ await pumpUntilFound(inputFinder);
+
+ final fieldFinder = find.descendant(of: inputFinder, matching: find.byType(EditableText));
+
+ await tester.enterText(fieldFinder.first, text);
+
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+}
### integration_test/robots/new_settings_page_robot.dart
@@ -0,0 +1,45 @@
+import "package:cake_wallet/new-ui/pages/settings_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class NewSettingsPageRobot extends BaseRobot {
+ NewSettingsPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await pumpUntilFound(find.byType(SettingsMainPage));
+ }
+
+ Future<void> openRow(String route) async {
+ await scrollUntilRowVisible(route);
+ await tapByKey(route);
+ }
+
+ Future<void> scrollUntilRowVisible(String route) async {
+ final rowFinder = find.byKey(ValueKey(route));
+
+ if (tester.any(rowFinder)) {
+ return;
+ }
+
+ final scrollableFinder = find.descendant(
+ of: find.byType(SettingsMainPage),
+ matching: find.byType(Scrollable),
+ );
+
+ await tester.scrollUntilVisible(
+ rowFinder,
+ 200,
+ scrollable: scrollableFinder.first,
+ maxScrolls: 30,
+ );
+
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+
+ Future<void> confirmLeafPageDisplayed(Type pageType) async {
+ await pumpUntilFound(find.byType(pageType));
+ }
+}
### integration_test/robots/new_swap_page_robot.dart
@@ -0,0 +1,211 @@
+import "package:cake_wallet/core/execution_state.dart";
+import "package:cake_wallet/exchange/limits_state.dart";
+import "package:cake_wallet/new-ui/pages/swap_page.dart";
+import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart";
+import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_amount_box.dart";
+import "package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart";
+import "package:cake_wallet/src/widgets/primary_button.dart";
+import "package:cake_wallet/view_model/send/send_view_model_state.dart";
+import "package:cw_core/crypto_currency.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+import "../core/test_config.dart";
+import "../core/test_wallets.dart";
+
+class NewSwapPageRobot extends BaseRobot {
+ NewSwapPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await pumpUntilFound(find.byType(NewSwapPage));
+ }
+
+ Future<void> enterDepositAmount(String amount) async {
+ await enterTextByKey("swap_page_deposit_amount_field_key", amount);
+ }
+
+ Future<void> chooseReceiveCurrency(CryptoCurrency currency) async {
+ await tapByKey("swap_amount_box_receive_currency_button_key");
+
+ final search = find.descendant(
+ of: find.byType(CurrencyPickerSearchField),
+ matching: find.byType(EditableText),
+ );
+
+ await pumpUntilFound(search);
+
+ await tester.enterText(search.first, currency.title);
+ await settle(max: const Duration(seconds: 2));
+
+ await tapWhenVisible(
+ find.byWidgetPredicate(
+ (widget) => widget is CurrencyPickerRow && _isSameCurrency(widget.currency, currency),
+ ),
+ );
+
+ final chosen = await pumpUntil(
+ () => _isSameCurrency(
+ tester.widget<NewSwapPage>(find.byType(NewSwapPage)).exchangeViewModel.receiveCurrency,
+ currency,
+ ),
+ );
+
+ expect(
+ chosen,
+ true,
+ reason: "The picker did not switch the receive currency to ${currency.title}",
+ );
+ }
+
+ static bool _isSameCurrency(CryptoCurrency one, CryptoCurrency other) =>
+ one.title.toLowerCase() == other.title.toLowerCase() &&
+ (one.tag ?? "").toLowerCase() == (other.tag ?? "").toLowerCase();
+
+ Future<void> confirmQuoteReceived({Duration timeout = const Duration(seconds: 90)}) async {
+ final received = await pumpUntil(() => _bestRate() > 0, timeout: timeout);
+
+ expect(received, true, reason: "No provider quote arrived within ${timeout.inSeconds}s");
+ }
+
+ Future<void> enterMinimumViableDepositAmount() async {
+ final limitsLoaded = await pumpUntil(
+ () => _limitsState() is LimitsLoadedSuccessfully,
+ timeout: const Duration(seconds: 90),
+ );
+
+ expect(limitsLoaded, true, reason: "Provider limits never loaded");
+
+ final min = _limitsMin() ?? 0;
+
+ // 5 percent over the minimum, so a small rate move does not invalidate the trade.
+ final amount = min > 0 ? (min * 1.05).toStringAsFixed(8) : "0.01";
+ await enterDepositAmount(amount);
+ }
+
+ Future<String> enterTestReceiveAddress() async {
+ final exchangeViewModel =
+ tester.widget<NewSwapPage>(find.byType(NewSwapPage)).exchangeViewModel;
+ final receiveType = cryptoCurrencyOrTokenToWalletType(exchangeViewModel.receiveCurrency);
+ final address = receiveType == null ? "" : TestWallets.receiveAddressFor(receiveType);
+
+ expect(
+ address.isNotEmpty,
+ true,
+ reason: "No test receive address for ${exchangeViewModel.receiveCurrency.title}",
+ );
+
+ final receiveCard = find.byWidgetPredicate(
+ (widget) => widget is SwapAmountBox && widget.isReceiverCard,
+ );
+
+ tester.state<SwapAmountBoxState>(receiveCard).addressController.text = address;
+ await settle();
+
+ return address;
+ }
+
+ Future<void> confirmSwapButtonEnabled({Duration timeout = TestConfig.walletSyncBudget}) async {
+ final button = find.byKey(const ValueKey("exchange_page_exchange_button_key"));
+
+ final enabled = await pumpUntil(
+ () => tester.any(button) && !tester.widget<LoadingPrimaryButton>(button).isDisabled,
+ timeout: timeout,
+ );
+
+ expect(
+ enabled,
+ true,
+ reason: "The swap button stayed disabled with an amount, a quote and a receive address",
+ );
+ }
+
+ Future<void> tapSwapButton() async {
+ await tapByKey("exchange_page_exchange_button_key");
+ }
+
+ Future<void> confirmTradeCreated({Duration timeout = const Duration(minutes: 2)}) async {
+ await pumpUntilFound(find.byType(SwapConfirmSheet), timeout: timeout);
+ }
+
+ void confirmPayoutAddress(String expected) {
+ final sheet = tester.widget<SwapConfirmSheet>(find.byType(SwapConfirmSheet));
+ final payout = sheet.exchangeTradeViewModel.trade.payoutAddress ?? "";
+ final isHex = expected.startsWith("0x");
+
+ expect(
+ isHex ? payout.toLowerCase() : payout,
+ isHex ? expected.toLowerCase() : expected,
+ reason: "The trade pays out somewhere other than the test wallet",
+ );
+ }
+
+ Future<void> confirmDepositCommitted({Duration timeout = const Duration(minutes: 3)}) async {
+ ExecutionState? lastState;
+
+ final committed = await pumpUntil(
+ () {
+ final sheet = find.byType(SwapConfirmSheet);
+
+ if (!tester.any(sheet)) {
+ return true;
+ }
+
+ lastState =
+ tester.widget<SwapConfirmSheet>(sheet).exchangeTradeViewModel.sendViewModel.state;
+
+ return lastState is TransactionCommitted;
+ },
+ timeout: timeout,
+ );
+
+ expect(
+ committed,
+ true,
+ reason: "The deposit never committed within ${timeout.inMinutes}m, "
+ "the sheet reported ${lastState.runtimeType}",
+ );
+
+ expect(
+ lastState,
+ isA<TransactionCommitted>(),
+ reason: "The confirm sheet closed before the deposit committed, "
+ "it last reported ${lastState.runtimeType}",
+ );
+
+ await pumpUntilGone(find.byType(NewSwapPage));
+ }
+
+ double? _limitsMin() {
+ final finder = find.byType(NewSwapPage);
+
+ if (!tester.any(finder)) {
+ return null;
+ }
+
+ return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.limits.min;
+ }
+
+ LimitsState? _limitsState() {
+ final finder = find.byType(NewSwapPage);
+
+ if (!tester.any(finder)) {
+ return null;
+ }
+
+ return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.limitsState;
+ }
+
+ double _bestRate() {
+ final finder = find.byType(NewSwapPage);
+
+ if (!tester.any(finder)) {
+ return 0;
+ }
+
+ return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.bestRate;
+ }
+}
### integration_test/robots/new_wallet_page_robot.dart
@@ -1,36 +1,32 @@
-import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart";
+import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class NewWalletPageRobot {
- NewWalletPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class NewWalletPageRobot extends BaseRobot {
+ NewWalletPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isNewWalletPage() async {
- await commonTestCases.isSpecificPage<NewWalletPage>();
- await commonTestCases.takeScreenshots('new_wallet_page');
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<NewWalletPage>();
}
Future<void> enterWalletName(String walletName) async {
- await commonTestCases.enterText(
- walletName,
- 'new_wallet_page_wallet_name_textformfield_key',
- );
- await commonTestCases.defaultSleepTime();
+ await enterTextByKey("new_wallet_page_wallet_name_textformfield_key", walletName);
}
Future<void> generateWalletName() async {
- await commonTestCases.tapItemByKey(
- 'new_wallet_page_wallet_name_textformfield_generate_name_button_key',
- );
- await commonTestCases.defaultSleepTime();
+ await tapByKey("new_wallet_page_wallet_name_textformfield_generate_name_button_key");
+ }
+
+ Future<void> expectNameRejected() async {
+ await pumpUntilFound(find.byType(AlertWithOneAction));
+
+ hasType<NewWalletPage>();
}
Future<void> onNextButtonPressed() async {
- await commonTestCases.tapItemByKey('new_wallet_page_confirm_button_key');
- await commonTestCases.defaultSleepTime();
+ await tapByKey("new_wallet_page_confirm_button_key");
}
}
### integration_test/robots/new_wallet_type_page_robot.dart
@@ -1,55 +1,29 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
-import 'package:cake_wallet/themes/core/material_base_theme.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class NewWalletTypePageRobot {
- NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class NewWalletTypePageRobot extends BaseRobot {
+ NewWalletTypePageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isNewWalletTypePage() async {
- await commonTestCases.isSpecificPage<NewWalletTypePage>();
- await commonTestCases.takeScreenshots('new_wallet_type_page');
- }
-
- void displaysCorrectTitle(bool isCreate) {
- commonTestCases.hasText(
- isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet,
- );
- }
-
- void hasWalletTypeForm() {
- commonTestCases.hasType<WalletTypeForm>();
- }
-
- void displaysCorrectImage(ThemeType type) {
- final walletTypeImage = Image.asset('assets/images/wallet_type.png').image;
- final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image;
-
- find.image(
- type == ThemeType.dark ? walletTypeImage : walletTypeLightImage,
- );
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<NewWalletTypePage>();
}
Future<void> findParticularWalletTypeInScrollableList(WalletType type) async {
- await tester.pump(Duration(milliseconds: 1000));
-
final scrollableWidget = find.descendant(
- of: find.byKey(Key('new_wallet_type_scrollable_key')),
+ of: find.byKey(const Key("new_wallet_type_scrollable_key")),
matching: find.byType(Scrollable),
);
- final targetWidget = find.byKey(ValueKey('new_wallet_type_${type.name}_button_key'));
+ final targetWidget = find.byKey(ValueKey("new_wallet_type_${type.name}_button_key"));
- tester.printToConsole('Attempting to scroll to wallet type ${type.name}');
+ tester.printToConsole("Attempting to scroll to wallet type ${type.name}");
- await tester.pump(Duration(milliseconds: 500));
+ await pumpUntilFound(scrollableWidget.first);
await tester.scrollUntilVisible(
targetWidget,
@@ -58,28 +32,28 @@ class NewWalletTypePageRobot {
maxScrolls: 20,
);
- await tester.pumpAndSettle(Duration(milliseconds: 1000));
+ await settle();
- expect(tester.any(targetWidget), true,
- reason: 'Wallet type ${type.name} should be visible after scrolling');
+ expect(
+ tester.any(targetWidget),
+ true,
+ reason: "Wallet type ${type.name} should be visible after scrolling",
+ );
- // Additional check to ensure the widget is actually on screen
final widgetRect = tester.getRect(targetWidget);
final screenSize = tester.view.physicalSize;
final screenRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
expect(
screenRect.overlaps(widgetRect),
true,
- reason: 'Wallet type ${type.name} should be within screen bounds',
+ reason: "Wallet type ${type.name} should be within screen bounds",
);
- tester.printToConsole('Wallet type ${type.name} is now visible and tappable');
+ tester.printToConsole("Wallet type ${type.name} is now visible and tappable");
}
- // Tapping a wallet type now navigates directly (the page no longer has a
- // separate "Next" button — selection happens on tap).
Future<void> selectWalletType(WalletType type) async {
- await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key');
+ await tapByKey("new_wallet_type_${type.name}_button_key");
}
}
### integration_test/robots/pin_code_widget_robot.dart
@@ -1,52 +1,24 @@
-import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/pin_code/pin_code_widget.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class PinCodeWidgetRobot {
- PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class PinCodeWidgetRobot extends BaseRobot {
+ PinCodeWidgetRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
+ @override
+ Future<void> isDisplayed() async => hasPinCodeWidget();
void hasPinCodeWidget() {
final pinCodeWidget = find.bySubtype<PinCodeWidget>();
expect(pinCodeWidget, findsOneWidget);
}
- void hasNumberButtonsVisible() {
- // Confirmation for buttons 1-9
- for (var i = 1; i < 10; i++) {
- commonTestCases.hasValueKey('pin_code_button_${i}_key');
+ Future<void> enterPinCode(List<int> pinCode) async {
+ for (final pin in pinCode) {
+ await tapByKey("pin_code_button_${pin}_key");
}
- // Confirmation for 0 button
- commonTestCases.hasValueKey('pin_code_button_0_key');
- }
-
- Future<void> enterPassword(String password, {int pumpDuration = 100}) async {
- await commonTestCases.enterText(
- password,
- 'enter_wallet_password',
- );
- await tester.pumpAndSettle();
- await commonTestCases.tapItemByKey(
- 'unlock',
- );
- await tester.pumpAndSettle();
-
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> enterPinCode(List<int> pinCode, {int pumpDuration = 100}) async {
- for (int pin in pinCode) {
- await commonTestCases.tapItemByKey(
- 'pin_code_button_${pin}_key',
- pumpDuration: pumpDuration,
- );
- }
-
- await commonTestCases.takeScreenshots('pin_code_widget');
- await commonTestCases.defaultSleepTime();
+ await settle();
}
}
### integration_test/robots/pre_seed_page_robot.dart
@@ -1,24 +1,20 @@
import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
-import 'package:flutter_test/flutter_test.dart';
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class PreSeedPageRobot {
- PreSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class PreSeedPageRobot extends BaseRobot {
+ PreSeedPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isPreSeedPage() async {
- await commonTestCases.isSpecificPage<PreSeedPage>();
- await commonTestCases.takeScreenshots('pre_seed_page');
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<PreSeedPage>();
}
+ // The button is only built once all three boxes are ticked
Future<void> onConfirmButtonPressed() async {
- await commonTestCases.tapItemByKey("pre_seed_page_only_way_checkbox_key");
- await commonTestCases.tapItemByKey("pre_seed_page_write_down_checkbox_key");
- await commonTestCases.tapItemByKey("pre_seed_page_never_share_checkbox_key");
- await commonTestCases.tapItemByKey('pre_seed_page_button_key');
- await commonTestCases.defaultSleepTime();
+ await tapByKey("pre_seed_page_only_way_checkbox_key");
+ await tapByKey("pre_seed_page_write_down_checkbox_key");
+ await tapByKey("pre_seed_page_never_share_checkbox_key");
+ await tapByKey("pre_seed_page_button_key");
}
}
### integration_test/robots/restore_from_seed_or_key_robot.dart
@@ -1,123 +1,101 @@
-import 'package:cake_wallet/entities/seed_type.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
-import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/entities/seed_type.dart";
+import "package:cake_wallet/src/screens/restore/wallet_restore_page.dart";
+import "package:cake_wallet/src/widgets/primary_button.dart";
+import "package:cake_wallet/src/widgets/validable_annotated_editable_text.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class RestoreFromSeedOrKeysPageRobot {
- RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class RestoreFromSeedOrKeysPageRobot extends BaseRobot {
+ RestoreFromSeedOrKeysPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isRestoreFromSeedKeyPage() async {
- await commonTestCases.isSpecificPage<WalletRestorePage>();
- await commonTestCases.takeScreenshots('wallet_restore_page');
- }
-
- Future<void> confirmViewComponentsDisplayProperlyPerPageView() async {
- commonTestCases.hasText(S.current.wallet_name);
- commonTestCases.hasText(S.current.enter_seed_phrase);
- commonTestCases.hasText(S.current.restore_title_from_seed);
-
- commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key');
- commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key');
- commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
- commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key');
-
- commonTestCases.hasText(S.current.private_key, hasWidget: false);
- commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false);
-
- await commonTestCases.swipePage();
- await commonTestCases.defaultSleepTime();
-
- commonTestCases.hasText(S.current.wallet_name);
- commonTestCases.hasText(S.current.private_key);
- commonTestCases.hasText(S.current.restore_title_from_keys);
-
- commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false);
- commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false);
-
- await commonTestCases.swipePage(swipeRight: false);
- }
-
- void confirmRestoreButtonDisplays() {
- commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key');
- }
-
- void confirmAdvancedSettingButtonDisplays() {
- commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key');
- }
-
- Future<void> enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async {
- await commonTestCases.enterText(
- walletName,
- 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key',
- );
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletRestorePage>();
}
Future<void> selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async {
- await commonTestCases.tapItemByKey(
+ await tapByKey(
'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key',
);
}
Future<void> enterSeedPhraseForWalletRestore(String text) async {
- ValidatableAnnotatedEditableTextState seedTextState =
- await tester.state(find.byType(ValidatableAnnotatedEditableText));
+ final ValidatableAnnotatedEditableTextState seedTextState =
+ tester.state(find.byType(ValidatableAnnotatedEditableText));
seedTextState.widget.controller.text = text;
- await tester.pumpAndSettle();
+ await settle();
}
Future<void> enterPasswordForWalletRestore(String text) async {
- await commonTestCases.enterText(
- text,
- 'password',
- );
- await tester.pumpAndSettle();
+ await enterTextByKey("password", text);
+ await settle();
}
Future<void> enterPasswordRepeatForWalletRestore(String text) async {
- await commonTestCases.enterText(
- text,
- 'repeat_wallet_password',
- );
- await tester.pumpAndSettle();
+ await enterTextByKey("repeat_wallet_password", text);
+ await settle();
}
Future<void> enterBlockHeightForWalletRestore(String blockHeight) async {
- await commonTestCases.enterText(
- blockHeight,
- 'wallet_restore_from_seed_blockheight_textfield_key',
- );
- await tester.pumpAndSettle();
+ await enterTextByKey("wallet_restore_from_seed_blockheight_textfield_key", blockHeight);
+ await settle();
}
Future<void> chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType selectedType) async {
- await commonTestCases.tapItemByKey('wallet_restore_from_seed_seedtype_picker_button_key');
+ await tapByKey("wallet_restore_from_seed_seedtype_picker_button_key");
+
+ await tapByKey("picker_items_index_${selectedType.title}_button_key");
+ }
+
+ Future<void> expectRestoreRefused({Duration window = const Duration(seconds: 10)}) async {
+ final button = tester.widget<LoadingPrimaryButton>(
+ find.byKey(const ValueKey("wallet_restore_seed_or_key_restore_button_key")),
+ );
+
+ expect(
+ button.isDisabled,
+ true,
+ reason: "The restore button accepted a seed the wallet cannot parse",
+ );
- await commonTestCases.defaultSleepTime();
+ final left = await pumpUntil(
+ () => !tester.any(find.byType(WalletRestorePage)),
+ timeout: window,
+ );
- await commonTestCases.tapItemByKey('picker_items_index_${selectedType.title}_button_key');
+ expect(left, false, reason: "A seed the wallet cannot parse still restored something");
}
- Future<void> onPasteSeedPhraseButtonPressed() async {
- await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
+ Future<void> onRestoreWalletButtonPressed() async {
+ await tapByKey("wallet_restore_seed_or_key_restore_button_key");
}
- Future<void> enterPrivateKeyForWalletRestore(String privateKey) async {
- await commonTestCases.enterText(
- privateKey,
- 'wallet_restore_from_key_private_key_textfield_key',
+ Future<void> enablePassphrase() async {
+ await tapByKey("wallet_restore_has_passphrase_checkbox_key");
+ await settle();
+
+ final page = tester.widget<WalletRestorePage>(find.byType(WalletRestorePage));
+
+ expect(
+ page.walletRestoreViewModel.hasPassphrase,
+ true,
+ reason: "The passphrase box did not tick, the wallet would restore without it",
);
- await tester.pumpAndSettle();
}
- Future<void> onRestoreWalletButtonPressed() async {
- await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key');
- await commonTestCases.defaultSleepTime();
+ Future<void> enterPassphrase(String passphrase) async {
+ await enterTextByKey(
+ "add_passphrase_bottom_sheet_widget_passphrase_textfield_key",
+ passphrase,
+ );
+ await enterTextByKey(
+ "add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key",
+ passphrase,
+ );
+
+ await tapByKey("add_passphrase_bottom_sheet_widget_restore_button_key");
}
}
### integration_test/robots/restore_options_page_robot.dart
@@ -1,43 +1,16 @@
-import 'package:cake_wallet/src/screens/restore/restore_options_page.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/restore/restore_options_page.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class RestoreOptionsPageRobot {
- RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class RestoreOptionsPageRobot extends BaseRobot {
+ RestoreOptionsPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isRestoreOptionsPage() async {
- await commonTestCases.isSpecificPage<RestoreOptionsPage>();
- await commonTestCases.takeScreenshots('restore_options');
- }
-
- void hasRestoreOptionsButton() {
- commonTestCases.hasValueKey('restore_options_from_seeds_or_keys_button_key');
- commonTestCases.hasValueKey('restore_options_from_backup_button_key');
- commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key');
- commonTestCases.hasValueKey('restore_options_from_qr_button_key');
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<RestoreOptionsPage>();
}
Future<void> navigateToRestoreFromSeedsOrKeysPage() async {
- await commonTestCases.tapItemByKey('restore_options_from_seeds_or_keys_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> navigateToRestoreFromBackupPage() async {
- await commonTestCases.tapItemByKey('restore_options_from_backup_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> navigateToRestoreFromHardwareWalletPage() async {
- await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> backAndVerify() async {
- await commonTestCases.goBack();
- await isRestoreOptionsPage();
+ await tapByKey("restore_options_from_seeds_or_keys_button_key");
}
}
### integration_test/robots/security_and_backup_page_robot.dart
@@ -1,28 +0,0 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-
-class SecurityAndBackupPageRobot {
- SecurityAndBackupPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- final CommonTestCases commonTestCases;
-
- Future<void> isSecurityAndBackupPage() async {
- await commonTestCases.isSpecificPage<SecurityBackupPage>();
- await commonTestCases.takeScreenshots('security_backup_page');
- }
-
- void hasTitle() {
- commonTestCases.hasText(S.current.security_and_backup);
- }
-
- Future<void> navigateToShowKeysPage() async {
- await tester.pumpAndSettle(Duration(milliseconds: 1000));
-
- await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key');
- await tester.pumpAndSettle(Duration(milliseconds: 1000));
- }
-}
### integration_test/robots/security_backup_page_robot.dart
@@ -0,0 +1,16 @@
+import "package:cake_wallet/src/screens/settings/security_backup_page.dart";
+
+import "../core/base_robot.dart";
+
+class SecurityBackupPageRobot extends BaseRobot {
+ SecurityBackupPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<SecurityBackupPage>();
+ }
+
+ Future<void> openChangePin() async {
+ await tapByKey("security_backup_page_change_pin_button_key");
+ }
+}
### integration_test/robots/seed_verification_page_robot.dart
@@ -1,22 +1,19 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class SeedVerificationPageRobot {
- SeedVerificationPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class SeedVerificationPageRobot extends BaseRobot {
+ SeedVerificationPageRobot(super.tester);
- final WidgetTester tester;
- final CommonTestCases commonTestCases;
-
- Future<void> isSeedVerificationPage() async {
- await commonTestCases.isSpecificPage<SeedVerificationPage>();
- await commonTestCases.takeScreenshots('seed_verification_page');
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<SeedVerificationPage>();
}
void hasTitle() {
- commonTestCases.hasText(S.current.verify_seed);
+ hasText(S.current.verify_seed);
}
Future<void> verifyWalletSeeds() async {
@@ -28,18 +25,23 @@ class SeedVerificationPageRobot {
while (!walletSeedViewModel.isVerificationComplete &&
walletSeedViewModel.verificationWordCount != 0) {
final currentCorrectWord = walletSeedViewModel.currentCorrectWord;
+ final currentStep = walletSeedViewModel.currentStepIndex;
+
+ hasTextAtLeastOnce(currentCorrectWord);
- commonTestCases.hasTextAtLestOnce(currentCorrectWord);
+ await tapByKey("seed_verification_option_${currentCorrectWord}_button_key");
- await commonTestCases.tapItemByKey(
- 'seed_verification_option_${currentCorrectWord}_button_key',
+ final hasMovedOn = await pumpUntil(
+ () =>
+ walletSeedViewModel.isVerificationComplete ||
+ walletSeedViewModel.currentStepIndex != currentStep,
);
- await commonTestCases.defaultSleepTime(seconds: 1);
+ if (!hasMovedOn) {
+ throw TestFailure("Seed verification never moved past step $currentStep");
+ }
}
- await commonTestCases.tapItemByKey('wallet_seed_page_open_wallet_button_key');
-
- await commonTestCases.defaultSleepTime();
+ await tapByKey("wallet_seed_page_open_wallet_button_key");
}
}
### integration_test/robots/send_page_robot.dart
@@ -1,716 +0,0 @@
-import 'dart:async';
-
-import 'package:cake_wallet/core/execution_state.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/send/send_page.dart';
-import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
-import 'package:cake_wallet/src/widgets/primary_button.dart';
-import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
-import 'package:cw_core/crypto_currency.dart';
-import 'package:cw_core/transaction_priority.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-import '../components/common_test_constants.dart';
-import 'auth_page_robot.dart';
-import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
-import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
-
-class SendPageRobot {
- SendPageRobot({required this.tester})
- : commonTestCases = CommonTestCases(tester),
- authPageRobot = AuthPageRobot(tester);
-
- WidgetTester tester;
- CommonTestCases commonTestCases;
- AuthPageRobot authPageRobot;
-
- Future<void> isSendPage() async {
- await commonTestCases.isSpecificPage<SendPage>();
- await commonTestCases.takeScreenshots('send_page');
- }
-
- Future<void> waitForSendPage() async {
- tester.printToConsole('Waiting for SendPage to be available');
-
- final stopwatch = Stopwatch()..start();
- final maxDuration = Duration(seconds: 30);
-
- while (stopwatch.elapsed < maxDuration) {
- await tester.pump(Duration(milliseconds: 500));
-
- // Check if we're still on auth page
- if (authPageRobot.onAuthPage()) {
- tester.printToConsole('Still on auth page, waiting...');
- continue;
- }
-
- // Check if SendPage is available
- final sendPageFinder = find.byType(SendPage);
- if (sendPageFinder.tryEvaluate()) {
- tester.printToConsole('SendPage found!');
- return;
- }
-
- tester.printToConsole('SendPage not found yet, waiting...');
- }
-
- throw Exception('SendPage not found after ${maxDuration.inSeconds} seconds');
- }
-
- Future<void> checkIfSendPageIsVisible() async {
- tester.printToConsole('Confirming SendPage is visible');
-
- if (authPageRobot.onAuthPage()) {
- tester.printToConsole('Auth page is currently visible');
- await _handleAuthPage();
- await commonTestCases.defaultSleepTime();
- }
-
- await waitForSendPage();
- }
-
- void hasTitle() {
- commonTestCases.hasText(S.current.send);
- }
-
- void confirmViewComponentsDisplayProperly() {
- SendPage sendPage = tester.widget(find.byType(SendPage));
- final sendViewModel = sendPage.sendViewModel;
-
- commonTestCases.hasValueKey('send_page_address_textfield_key');
- commonTestCases.hasValueKey('send_page_note_textfield_key');
- commonTestCases.hasValueKey('send_page_amount_textfield_key');
- commonTestCases.hasValueKey('send_page_add_template_button_key');
-
- if (sendViewModel.hasMultipleTokens) {
- commonTestCases.hasValueKey('send_page_currency_picker_button_key');
- }
-
- if (!sendViewModel.isBatchSending) {
- commonTestCases.hasValueKey('send_page_send_all_button_key');
- }
-
- if (!sendViewModel.isFiatDisabled) {
- commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key');
- }
-
- if (sendViewModel.feesViewModel.hasFees) {
- commonTestCases.hasValueKey('send_page_select_fee_priority_button_key');
- }
-
- if (sendViewModel.hasCoinControl) {
- commonTestCases.hasValueKey('send_page_unspent_coin_button_key');
- }
-
- if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) {
- commonTestCases.hasValueKey('send_page_add_receiver_button_key');
- }
- }
-
- Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
- final currencyPickerKey = 'send_page_currency_picker_button_key';
- final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key';
-
- await commonTestCases.tapItemByKey(currencyPickerKey);
- await commonTestCases.defaultSleepTime();
-
- // Check if picker dialog is present
- if (!commonTestCases.isKeyPresent(currencyPickerDialogKey)) {
- tester.printToConsole('Currency picker dialog not found, may already be selected');
- return;
- }
-
- SendPage sendPage = tester.widget(find.byType(SendPage));
- final sendViewModel = sendPage.sendViewModel;
-
- if (receiveCurrency == sendViewModel.selectedCryptoCurrency) {
- await commonTestCases
- .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
- return;
- }
-
- await commonTestCases.enterText(receiveCurrency.title, 'search_bar_widget_key');
-
- await commonTestCases.defaultSleepTime();
-
- await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.fullName}_button_key');
- }
-
- Future<void> enterReceiveAddress(String receiveAddress) async {
- await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> enterSendAmount(String amount, {bool isFiat = false}) async {
- await commonTestCases.enterText(
- amount,
- isFiat ? 'send_page_fiat_amount_textfield_key' : 'send_page_amount_textfield_key',
- );
- }
-
- String _getTextFromField(ValueKey<String> key) {
- final field = find.byKey(key);
- if (!field.tryEvaluate()) return '';
- final baseTextFormField = tester.widget<BaseTextFormField>(field);
- return baseTextFormField.controller?.text ?? '';
- }
-
- /// Validates wallet balance for $1 send by checking against the converted crypto amount
- /// Returns true if wallet has sufficient balance, false otherwise
- Future<bool> validateWalletBalanceForOneDollarSend() async {
- SendPage sendPage = tester.widget(find.byType(SendPage));
- final sendViewModel = sendPage.sendViewModel;
-
- final balance = await sendViewModel.sendingBalance;
-
- await setupOneDollarSend();
-
- // Get the crypto amount that was set for the $1 send
- String cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
- if (cryptoAmount.isEmpty || cryptoAmount == '0' || cryptoAmount == '0.0') {
- cryptoAmount = '0.001'; // fallback amount
- }
-
- final amount = double.tryParse(cryptoAmount) ?? 0.0;
-
- tester.printToConsole(
- 'Wallet balance: $balance, sending amount for \$${CommonTestConstants.sendTestFiatAmount}: $cryptoAmount',
- );
-
- if (balance.isEmpty || double.tryParse(balance) == null) {
- tester.printToConsole('Invalid wallet balance: $balance');
- return false;
- }
-
- final balanceValue = double.parse(balance);
- if (balanceValue < amount) {
- tester.printToConsole(
- 'Insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send: $balanceValue < $amount',
- );
- return false;
- }
-
- tester.printToConsole(
- 'Wallet has sufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
- );
- return true;
- }
-
- Future<void> selectTransactionPriority({TransactionPriority? priority}) async {
- SendPage sendPage = tester.widget(find.byType(SendPage));
- final sendViewModel = sendPage.sendViewModel;
-
- if (!sendViewModel.feesViewModel.hasFees || priority == null) return;
-
- final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key';
- await commonTestCases.tapItemByKey(transactionPriorityPickerKey);
-
- if (priority == sendViewModel.feesViewModel.transactionPriority) {
- await commonTestCases
- .tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key');
- return;
- }
-
- await commonTestCases.dragUntilVisible(
- 'picker_items_index_${priority.title}_button_key',
- 'picker_scrollbar_key',
- );
- await commonTestCases.defaultSleepTime();
-
- await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key');
- }
-
- Future<void> onSendButtonPressed() async {
- tester.printToConsole('Pressing send');
-
- await checkIfSendPageIsVisible();
-
- await tester.pumpAndSettle();
- final sendPage = tester.widget<SendPage>(find.byType(SendPage));
-
- while (true) {
- bool isReadyForSend = sendPage.sendViewModel.isReadyForSend;
- await tester.pump();
- if (isReadyForSend) {
- tester.printToConsole('Is ready for send');
- break;
- } else {
- await commonTestCases.defaultSleepTime();
- await tester.pumpAndSettle();
- tester.printToConsole('not yet ready for send');
- }
- }
- await commonTestCases.tapItemByKey(
- 'send_page_send_button_key',
- shouldPumpAndSettle: false,
- );
-
- await _waitForSendTransactionCompletion();
-
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> _waitForSendTransactionCompletion() async {
- await tester.pump();
- final Completer<void> completer = Completer<void>();
-
- // Loop to wait for the async operation to complete
- while (true) {
- await Future.delayed(Duration(seconds: 1));
-
- tester.printToConsole('Before _handleAuth');
-
- await _handleAuthPage();
-
- await commonTestCases.defaultSleepTime();
-
- tester.printToConsole('After _handleAuth');
-
- await tester.pump();
-
- if (authPageRobot.onAuthPage()) {
- tester.printToConsole('Still on auth page, continuing to wait');
- continue;
- }
-
- final sendPageFinder = find.byType(SendPage);
- if (!sendPageFinder.tryEvaluate()) {
- tester.printToConsole('SendPage not found yet, continuing to wait');
- continue;
- }
-
- final sendPage = tester.widget<SendPage>(sendPageFinder);
- final state = sendPage.sendViewModel.state;
-
- await tester.pump();
-
- bool isDone = state is ExecutedSuccessfullyState || state is TransactionCommitted;
- bool isFailed = state is FailureState;
-
- tester.printToConsole('isDone: $isDone');
- tester.printToConsole('isFailed: $isFailed');
-
- if (isDone || isFailed) {
- tester.printToConsole(
- isDone ? 'Completer is done' : 'Completer is done though operation failed',
- );
- completer.complete();
- await tester.pump();
- break;
- } else {
- tester.printToConsole('Completer is not done');
- await tester.pump();
- }
- }
-
- await expectLater(completer.future, completes);
-
- tester.printToConsole('Done confirming sending operation');
- }
-
- Future<void> _handleAuthPage() async {
- tester.printToConsole('Inside _handleAuth');
-
- final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
- if (onAuthPageDesktop) {
- await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
- await commonTestCases.defaultSleepTime();
- return;
- }
-
- await tester.pump();
- tester.printToConsole('starting auth checks');
-
- final authPage = authPageRobot.onAuthPage();
-
- tester.printToConsole('hasAuth:$authPage');
-
- if (authPage) {
- await tester.pump();
- tester.printToConsole('Starting inner _handleAuth loop checks');
-
- try {
- await authPageRobot.enterPinCode(CommonTestConstants.pin, pumpDuration: 500);
- tester.printToConsole('Auth done');
-
- await tester.pump(Duration(seconds: 3));
-
- tester.printToConsole('Auth pump done');
-
- await commonTestCases.defaultSleepTime();
- } catch (e) {
- tester.printToConsole('Auth failed, retrying: $e');
- await tester.pump();
-
- await commonTestCases.defaultSleepTime(seconds: 1);
- await _handleAuthPage();
- }
- } else {
- tester.printToConsole('No auth page detected, proceeding');
- }
- await tester.pump();
- }
-
- //* ------ On Sending Failure ------------
- Future<bool> hasErrorWhileSending() async {
- await tester.pump();
-
- tester.printToConsole('Checking if there is an error');
-
- final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key'));
-
- bool hasError = errorDialog.tryEvaluate();
-
- tester.printToConsole('Has error: $hasError');
-
- return hasError;
- }
-
- Future<void> onSendFailureDialogButtonPressed() async {
- await commonTestCases.defaultSleepTime();
-
- tester.printToConsole('Send Button Failure Dialog Triggered');
-
- await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key');
- }
-
- //* ------ On Sending Success ------------
- Future<void> onSendSliderOnConfirmSendingBottomSheetDragged() async {
- await commonTestCases.defaultSleepTime();
- await tester.pump();
-
- if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
- tester.printToConsole('Found confirm sending bottom sheet, starting slider drag');
-
- final accessibleButton = find.byType(PrimaryButton);
- if (accessibleButton.tryEvaluate()) {
- tester.printToConsole('Found accessible navigation button, tapping it');
- await tester.tap(accessibleButton);
- await tester.pumpAndSettle();
- } else {
- await _performSendSliderDrag();
- }
-
- tester.printToConsole('Slider/button action completed, waiting for transaction completion');
-
- // Check if bottom sheet is dismissed
- if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
- tester.printToConsole('Bottom sheet still present, waiting a bit more for dismissal');
- await commonTestCases.defaultSleepTime(seconds: 3);
-
- // If still present, try one more manual dismissal attempt
- if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
- tester.printToConsole('Bottom sheet still present, attempting final manual dismissal');
- await tester.tapAt(Offset(200, 200)); // Tap outside
- await tester.pumpAndSettle();
- }
- }
-
- // Wait for transaction completion
- await _waitForCommitTransactionCompletion();
- await commonTestCases.defaultSleepTime(seconds: 2);
- } else {
- tester.printToConsole('Confirm sending bottom sheet not found, waiting and retrying');
- await commonTestCases.defaultSleepTime();
- await tester.pump();
- await onSendSliderOnConfirmSendingBottomSheetDragged();
- }
- }
-
- Future<void> _performSendSliderDrag() async {
- final sliderFinder = find.byKey(const ValueKey('standard_slide_button_widget_slider_key'));
- expect(sliderFinder, findsOneWidget, reason: 'Slider should be found');
-
- // Find the StandardSlideButton widget to get the main container
- final slideButtonFinder = find.byType(StandardSlideButton);
- expect(slideButtonFinder, findsOneWidget, reason: 'StandardSlideButton should be found');
-
- // Get the main container bounds (the outer container, not the slider container)
- final mainContainerRect = tester.getRect(slideButtonFinder);
- final containerWidth = mainContainerRect.width;
-
- final sideMargin = 4.0;
- final sliderWidth = 42.0;
- final effectiveMaxWidth = containerWidth - 2 * sideMargin;
- final threshold = effectiveMaxWidth - sliderWidth - 10;
-
- // Add a small buffer to ensure we exceed the threshold
- final dragDistance = threshold + 20;
-
- tester.printToConsole(
- 'Main container width: $containerWidth, Threshold: $threshold, Drag distance: $dragDistance',
- );
-
- // Start the drag
- await tester.drag(sliderFinder, Offset(dragDistance, 0));
-
- // Wait for the drag to complete and trigger onHorizontalDragEnd
- await tester.pump(Duration(milliseconds: 100));
-
- // Release the drag (this should trigger onHorizontalDragEnd)
- await tester.pump(Duration(seconds: 2));
-
- tester.printToConsole('Drag completed, waiting for callback');
-
- // Wait for the slide completion callback to trigger and bottom sheet to dismiss
- await _waitForBottomSheetDismissal();
- }
-
- Future<void> _waitForBottomSheetDismissal() async {
- tester.printToConsole('Waiting for bottom sheet dismissal');
-
- final stopwatch = Stopwatch()..start();
- final maxDuration = Duration(seconds: 10);
-
- while (stopwatch.elapsed < maxDuration) {
- await tester.pump(Duration(milliseconds: 500));
-
- // Check if the confirm bottom sheet is still present
- if (!commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
- tester.printToConsole('Bottom sheet dismissed successfully!');
- return;
- }
-
- // Check if transaction has started (this indicates slider was successful)
- if (_isTransactionStarted()) {
- tester.printToConsole(
- 'Transaction started, slider was successful even if bottom sheet is still visible');
- return;
- }
-
- tester.printToConsole('Bottom sheet still present, waiting');
- }
-
- tester.printToConsole('Bottom sheet dismissal timeout reached, trying manual dismissal');
-
- // If the bottom sheet is still present after timeout, try to manually dismiss it
- // This might happen if the slider drag didn't trigger the onSlideComplete properly
- if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
- tester.printToConsole('Attempting manual bottom sheet dismissal');
-
- // Try to find and tap a close button or back button
- final closeButton = find.byIcon(Icons.close);
- if (closeButton.tryEvaluate()) {
- await tester.tap(closeButton);
- await tester.pumpAndSettle();
- tester.printToConsole('Manual close button tapped');
- } else {
- // Try to pop the bottom sheet by tapping outside or using back gesture
- await tester.tapAt(Offset(100, 100)); // Tap outside the bottom sheet
- await tester.pumpAndSettle();
- tester.printToConsole('Tapped outside bottom sheet');
- }
- }
- }
-
- bool _isTransactionStarted() {
- try {
- // Check if SendPage is available and transaction state has changed
- final sendPageFinder = find.byType(SendPage);
- if (!sendPageFinder.tryEvaluate()) {
- return false;
- }
-
- final sendPage = tester.widget<SendPage>(sendPageFinder);
- final state = sendPage.sendViewModel.state;
-
- // Check if we're in a transaction-related state
- return state is TransactionCommitting ||
- state is IsExecutingState ||
- state is TransactionCommitted ||
- state is ExecutedSuccessfullyState;
- } catch (e) {
- return false;
- }
- }
-
- Future<void> _waitForCommitTransactionCompletion() async {
- tester.printToConsole('Starting to wait for transaction completion');
-
- final stopwatch = Stopwatch()..start();
- final maxDuration = Duration(seconds: 60);
-
- while (stopwatch.elapsed < maxDuration) {
- await Future.delayed(Duration(seconds: 1));
-
- // Check if the confirm bottom sheet is gone (transaction started)
- if (!commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
- tester.printToConsole('Confirm bottom sheet disappeared, transaction may be processing');
- }
-
- // Check if we've navigated back to the dashboard (for cases of successful transaction)
- final dashboardFinder = find.byType(DashboardPage);
- if (dashboardFinder.tryEvaluate()) {
- tester.printToConsole('Dashboard detected, transaction completed successfully!');
- await tester.pump();
- return;
- }
-
- // Check if SendPage is still available (transaction still processing)
- final sendPageFinder = find.byType(SendPage);
- if (sendPageFinder.tryEvaluate()) {
- final sendPage = tester.widget<SendPage>(sendPageFinder);
- final state = sendPage.sendViewModel.state;
-
- bool isDone = state is ExecutedSuccessfullyState || state is TransactionCommitted;
- bool isFailed = state is FailureState;
-
- tester.printToConsole('Transaction state: $state');
- tester.printToConsole('isDone: $isDone');
- tester.printToConsole('isFailed: $isFailed');
-
- if (isDone) {
- tester.printToConsole('Transaction committed successfully!');
- await tester.pump();
- return;
- } else if (isFailed) {
- tester.printToConsole('Transaction failed: $state');
- await tester.pump();
- return;
- } else {
- tester.printToConsole('Transaction still processing');
- await tester.pump();
- }
- } else {
- // SendPage not found, check if we're on dashboard or still processing
- if (dashboardFinder.tryEvaluate()) {
- tester.printToConsole('Dashboard detected - transaction completed successfully!');
- await tester.pump();
- return;
- } else {
- tester.printToConsole(
- 'SendPage not found, but dashboard not yet visible - continuing to wait...');
- }
- }
- }
-
- if (stopwatch.elapsed >= maxDuration) {
- tester.printToConsole('Transaction completion timeout reached');
- }
-
- tester.printToConsole('Done waiting for transaction completion');
- }
-
- //* ---- Handle Transaction Success Flow -----
- Future<void> handleTransactionSuccessFlow() async {
- await commonTestCases.defaultSleepTime();
-
- // Wait for any success dialogs to appear
- await tester.pump(Duration(seconds: 2));
-
- // Check for contact addition dialog first (if new contact address exists)
- final contactDialog = find.byKey(ValueKey('send_page_add_contact_bottom_sheet_yes_button_key'));
- if (contactDialog.tryEvaluate()) {
- tester.printToConsole('Found contact addition dialog, selecting Yes');
-
- // Check if the button is actually visible and tappable
- final buttonRect = tester.getRect(contactDialog);
- final screenSize = tester.view.physicalSize / tester.view.devicePixelRatio;
-
- if (buttonRect.bottom <= screenSize.height && buttonRect.top >= 0) {
- await commonTestCases.tapItemByKey('send_page_add_contact_bottom_sheet_yes_button_key');
- await commonTestCases.defaultSleepTime();
- } else {
- tester.printToConsole('Contact dialog button is off-screen, skipping');
- }
- }
-
- // Check for the main success dialog
- final successDialog = find.byKey(ValueKey('send_page_sent_dialog_ok_button_key'));
- if (successDialog.tryEvaluate()) {
- tester.printToConsole('Found transaction success dialog, closing it');
- await commonTestCases.tapItemByKey('send_page_sent_dialog_ok_button_key');
- await commonTestCases.defaultSleepTime();
- }
- }
-
- //* ---- Fiat/Crypto Amount Validation -----
- Future<void> testFiatAmountEntry() async {
- tester.printToConsole('Testing fiat amount entry...');
-
- await enterSendAmount('');
- await enterSendAmount('', isFiat: true);
- await commonTestCases.defaultSleepTime();
-
- await enterSendAmount(CommonTestConstants.sendTestFiatAmount, isFiat: true);
- await commonTestCases.defaultSleepTime();
-
- // Wait for conversion to complete
- await tester.pump(Duration(seconds: 3));
- await tester.pumpAndSettle();
-
- // Next we get the crypto amount value and validate it's not 0
- final cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
-
- tester.printToConsole(
- 'Crypto amount after entering \$${CommonTestConstants.sendTestFiatAmount}: $cryptoAmount',
- );
-
- if (cryptoAmount.isNotEmpty && cryptoAmount != '0' && cryptoAmount != '0.0') {
- tester.printToConsole('Fiat to crypto conversion working - crypto amount: $cryptoAmount');
- } else {
- tester.printToConsole(
- 'Fiat to crypto conversion may not be working - crypto amount: $cryptoAmount',
- );
- }
- }
-
- Future<void> testCryptoAmountEntry() async {
- tester.printToConsole('Testing crypto amount entry...');
-
- await enterSendAmount('');
- await enterSendAmount('', isFiat: true);
- await commonTestCases.defaultSleepTime();
-
- String cryptoAmount = '0.001';
-
- await enterSendAmount(cryptoAmount);
- await commonTestCases.defaultSleepTime();
-
- // Wait for conversion to complete
- await tester.pump(Duration(seconds: 3));
- await tester.pumpAndSettle();
-
- // Get the fiat amount value and validate it's not 0
- final fiatAmount = _getTextFromField(ValueKey('send_page_fiat_amount_textfield_key'));
-
- tester.printToConsole('Fiat amount after entering $cryptoAmount: $fiatAmount');
-
- if (fiatAmount.isNotEmpty && fiatAmount != '0' && fiatAmount != '0.0') {
- tester.printToConsole('Crypto to fiat conversion working, fiat amount: $fiatAmount');
- } else {
- tester.printToConsole(
- 'Crypto to fiat conversion may not be working - fiat amount: $fiatAmount');
- }
- }
-
- Future<void> setupOneDollarSend() async {
- // Clear existing amounts
- await enterSendAmount('');
- await enterSendAmount('', isFiat: true);
- await commonTestCases.defaultSleepTime();
-
- await enterSendAmount(CommonTestConstants.sendTestFiatAmount, isFiat: true);
- await commonTestCases.defaultSleepTime();
-
- // Wait for conversion to complete
- await tester.pump(Duration(seconds: 3));
- await tester.pumpAndSettle();
-
- // Get the converted crypto amount
- String cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
- if (cryptoAmount.isEmpty || cryptoAmount == '0' || cryptoAmount == '0.0') {
- cryptoAmount = '0.001'; // fallback amount
- }
-
- tester.printToConsole(
- 'Sending $cryptoAmount (equivalent to \$${CommonTestConstants.sendTestFiatAmount}) to test wallet',
- );
-
- // Update the amount field with the converted value
- await enterSendAmount(cryptoAmount);
- await commonTestCases.defaultSleepTime();
- }
-}
### integration_test/robots/setup_pin_code_robot.dart
@@ -1,29 +1,16 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart";
-import '../components/common_test_cases.dart';
-import 'pin_code_widget_robot.dart';
+import "pin_code_widget_robot.dart";
class SetupPinCodeRobot extends PinCodeWidgetRobot {
- SetupPinCodeRobot(this.tester)
- : commonTestCases = CommonTestCases(tester),
- super(tester);
+ SetupPinCodeRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isSetupPinCodePage() async {
- await commonTestCases.isSpecificPage<SetupPinCodePage>();
- await commonTestCases.takeScreenshots('setup_pin_code_page');
- }
-
- void hasTitle() {
- commonTestCases.hasText(S.current.setup_pin);
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<SetupPinCodePage>();
}
Future<void> tapSuccessButton() async {
- await commonTestCases.tapItemByKey('setup_pin_code_success_button_key');
- await commonTestCases.defaultSleepTime();
+ await tapByKey("setup_pin_code_success_button_key");
}
}
### integration_test/robots/show_keys_disclaimer_page_robot.dart
@@ -0,0 +1,16 @@
+import "package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
+
+import "../core/base_robot.dart";
+
+class ShowKeysDisclaimerPageRobot extends BaseRobot {
+ ShowKeysDisclaimerPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<ShowKeysDisclaimerPage>();
+ }
+
+ Future<void> tapShowKeys() async {
+ await tapByKey("show_keys_disclaimer_page_button_key");
+ }
+}
### integration_test/robots/transaction_success_info_robot.dart
@@ -1,21 +0,0 @@
-import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../components/common_test_cases.dart';
-
-class TransactionSuccessInfoRobot {
- TransactionSuccessInfoRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isTransactionSuccessInfoPage() async {
- await commonTestCases.isSpecificPage<TransactionSuccessPage>();
- await commonTestCases.takeScreenshots('transaction_success_info_page');
- }
-
- Future<void> onConfirmButtonPressed() async {
- await commonTestCases.tapItemByKey('transaction_success_info_page_button_key');
- await commonTestCases.defaultSleepTime();
- }
-}
### integration_test/robots/transactions_page_robot.dart
@@ -1,439 +0,0 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
-import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
-import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
-import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
-import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
-import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
-import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
-import 'package:cw_core/crypto_currency.dart';
-import 'package:cw_core/sync_status.dart';
-import 'package:cw_core/transaction_direction.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:intl/intl.dart';
-
-import '../components/common_test_cases.dart';
-
-class TransactionsPageRobot {
- TransactionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isTransactionsPage() async {
- await commonTestCases.isSpecificPage<TransactionsPage>();
- await commonTestCases.takeScreenshots('transactions_page');
- }
-
- Future<void> confirmTransactionsPageConstantsDisplayProperly() async {
- await commonTestCases.defaultSleepTime();
-
- final transactionsPage = tester.widget<TransactionsPage>(find.byType(TransactionsPage));
- final dashboardViewModel = transactionsPage.dashboardViewModel;
- if (dashboardViewModel.status is SyncingSyncStatus) {
- commonTestCases.hasValueKey('transactions_page_syncing_alert_card_key');
- commonTestCases.hasText(S.current.syncing_wallet_alert_title);
- commonTestCases.hasText(S.current.syncing_wallet_alert_content);
- }
-
- commonTestCases.hasValueKey('transactions_page_header_row_key');
- commonTestCases.hasText(S.current.transactions);
- commonTestCases.hasValueKey('transactions_page_header_row_transaction_filter_button_key');
- }
-
- Future<void> confirmTransactionHistoryListDisplaysCorrectly(bool hasTxHistoryWhileSyncing) async {
- try {
- final transactionsPage = tester.widget<TransactionsPage>(find.byType(TransactionsPage));
- final dashboardViewModel = transactionsPage.dashboardViewModel;
-
- await _waitForListToBeReady();
-
- if (!hasTxHistoryWhileSyncing) {
- await _waitForSyncToComplete(dashboardViewModel);
- } else {
- await tester.pump(Duration(seconds: 2));
- await tester.pumpAndSettle();
- }
-
- await _performComprehensiveItemCheck(dashboardViewModel);
- } catch (e) {
- tester.printToConsole('Error in transaction history check: $e');
- }
- }
-
- Future<void> _waitForSyncToComplete(DashboardViewModel dashboardViewModel) async {
- const maxWaitTime = Duration(minutes: 2);
- final endTime = DateTime.now().add(maxWaitTime);
-
- tester.printToConsole('Waiting for wallet to sync...');
-
- while (DateTime.now().isBefore(endTime)) {
- await tester.pump(Duration(seconds: 2));
- await tester.pumpAndSettle();
-
- if (dashboardViewModel.status is SyncedSyncStatus) {
- tester.printToConsole('Wallet synced successfully');
- return;
- }
-
- // Check if we have now have transaction items available
- if (dashboardViewModel.items.isNotEmpty) {
- tester.printToConsole('Items available while syncing, proceeding with test');
- return;
- }
-
- tester.printToConsole('Sync status: ${dashboardViewModel.status.runtimeType}');
- }
-
- tester.printToConsole('Warning: Wallet did not sync within expected time, proceeding anyway');
- }
-
- Future<void> _performComprehensiveItemCheck(DashboardViewModel dashboardViewModel) async {
- try {
- await _waitForItemsToLoad(dashboardViewModel);
-
- final itemsToProcess = dashboardViewModel.items.where((item) {
- if (item is DateSectionItem) return false;
- if (item is TransactionListItem) {
- return !(item.hasTokens && item.assetOfTransaction == null);
- }
- return true;
- }).toList();
-
- if (itemsToProcess.isEmpty) {
- tester.printToConsole('No transaction items to process - checking for placeholder');
- _verifyPlaceholder();
- return;
- }
-
- // This is a temporary limit to prevent the test from taking too long
- final maxItemsToProcess = 100;
- final itemsToCheck = itemsToProcess.take(maxItemsToProcess).toList();
-
- tester.printToConsole(
- 'Processing ${itemsToCheck.length} items out of ${itemsToProcess.length} total items',
- );
-
- await _processVisibleItems(itemsToCheck, dashboardViewModel);
-
- // Try to scroll and process more items if needed
- await _processItemsWithScrolling(itemsToCheck, dashboardViewModel);
- } catch (e) {
- tester.printToConsole('Error in comprehensive item check: $e');
- try {
- _verifyPlaceholder();
- } catch (placeholderError) {
- tester.printToConsole('Could not verify placeholder either: $placeholderError');
- }
- }
- }
-
- Future<void> _waitForItemsToLoad(DashboardViewModel dashboardViewModel) async {
- const maxWaitTime = Duration(seconds: 30);
- final endTime = DateTime.now().add(maxWaitTime);
-
- while (DateTime.now().isBefore(endTime)) {
- if (dashboardViewModel.items.isNotEmpty) {
- tester.printToConsole('Items loaded: ${dashboardViewModel.items.length}');
- return;
- }
-
- // Check if placeholder is shown
- if (tester.any(find.byKey(ValueKey('transactions_page_placeholder_transactions_text_key')))) {
- tester.printToConsole('Placeholder is shown - no items to load');
- return;
- }
-
- await tester.pump(Duration(seconds: 1));
- await tester.pumpAndSettle();
- }
-
- tester.printToConsole('Warning: No items loaded and no placeholder shown within expected time');
- }
-
- Future<void> _processVisibleItems(
- List<dynamic> items,
- DashboardViewModel dashboardViewModel,
- ) async {
- try {
- int processedCount = 0;
-
- for (var item in items) {
- try {
- final keyId = (item.key as ValueKey<String>).value;
-
- // Check if item is already visible
- if (tester.any(find.byKey(ValueKey(keyId)))) {
- tester.printToConsole('Processing visible item: $keyId');
- await _verifyItemDisplay(item, dashboardViewModel);
- processedCount++;
- }
- } catch (itemError) {
- tester.printToConsole('Error processing item: $itemError');
- continue;
- }
- }
-
- tester.printToConsole('Processed $processedCount visible items\n');
- } catch (e) {
- tester.printToConsole('Error in visible items processing: $e');
- }
- }
-
- Future<void> _processItemsWithScrolling(
- List<dynamic> items,
- DashboardViewModel dashboardViewModel,
- ) async {
- try {
- final scrollableFinder = find.descendant(
- of: find.byKey(ValueKey('transactions_page_list_view_builder_key')),
- matching: find.byType(Scrollable),
- );
-
- if (!tester.any(scrollableFinder)) {
- tester.printToConsole('No scrollable found, skipping scroll processing');
- return;
- }
-
- int processedCount = 0;
- const maxScrollAttempts = 10;
-
- for (var item in items) {
- try {
- final keyId = (item.key as ValueKey<String>).value;
-
- // Skip if already processed
- if (tester.any(find.byKey(ValueKey(keyId)))) {
- continue;
- }
-
- // Try to scroll to the item
- bool found = false;
- for (int attempt = 0; attempt < maxScrollAttempts; attempt++) {
- await tester.pumpAndSettle(Duration(milliseconds: 200));
-
- if (tester.any(find.byKey(ValueKey(keyId)))) {
- tester.printToConsole('Found item after scrolling: $keyId');
- await _verifyItemDisplay(item, dashboardViewModel);
- processedCount++;
- found = true;
- break;
- }
-
- // Perform scroll
- try {
- await tester.drag(scrollableFinder, Offset(0, -100));
- await tester.pumpAndSettle(Duration(milliseconds: 300));
- } catch (scrollError) {
- tester.printToConsole('Scroll failed: $scrollError');
- break;
- }
- }
-
- if (!found) {
- tester.printToConsole('Could not find item after scrolling: $keyId');
- }
- } catch (itemError) {
- tester.printToConsole('Error processing item with scrolling: $itemError');
- continue;
- }
- }
-
- tester.printToConsole('Processed $processedCount items with scrolling');
- } catch (e) {
- tester.printToConsole('Error in scroll processing: $e');
- }
- }
-
- void _verifyPlaceholder() {
- commonTestCases.hasValueKey('transactions_page_placeholder_transactions_text_key');
- commonTestCases.hasText(S.current.placeholder_transactions);
- }
-
- Future<void> _waitForListToBeReady() async {
- const maxWaitAttempts = 10;
- int attempts = 0;
-
- while (attempts < maxWaitAttempts) {
- await tester.pumpAndSettle(Duration(milliseconds: 500));
-
- // Check if the list view is present
- final listViewFinder = find.byKey(ValueKey('transactions_page_list_view_builder_key'));
- if (tester.any(listViewFinder)) {
- tester.printToConsole('List view is ready');
- return;
- }
-
- attempts++;
- tester.printToConsole('Waiting for list view to be ready, attempt $attempts\n');
- }
-
- tester.printToConsole('List view did not become ready within expected time');
- }
-
- Future<void> _verifyItemDisplay(dynamic item, DashboardViewModel dashboardViewModel) async {
- // Execute the proper check depending on item type.
- switch (item.runtimeType) {
- case TransactionListItem:
- final transactionItem = item as TransactionListItem;
- tester.printToConsole(transactionItem.formattedTitle);
- tester.printToConsole(transactionItem.formattedFiatAmount);
- tester.printToConsole('\n');
- await _verifyTransactionListItemDisplay(transactionItem, dashboardViewModel);
- break;
-
- case AnonpayTransactionListItem:
- await _verifyAnonpayTransactionListItemDisplay(item as AnonpayTransactionListItem);
- break;
-
- case TradeListItem:
- await _verifyTradeListItemDisplay(item as TradeListItem);
- break;
-
- case OrderListItem:
- await _verifyOrderListItemDisplay(item as OrderListItem);
- break;
-
- default:
- tester.printToConsole('Unhandled item type: ${item.runtimeType}');
- }
- }
-
- Future<void> _verifyTransactionListItemDisplay(
- TransactionListItem item,
- DashboardViewModel dashboardViewModel,
- ) async {
- final keyId =
- '${dashboardViewModel.type.name}_transaction_history_item_${item.transaction.id}_key';
-
- if (!tester.any(find.byKey(ValueKey(keyId)))) {
- tester.printToConsole(
- 'Could not find transaction item with key: $keyId for transaction ${item.transaction.id}',
- );
- return;
- }
-
- if (item.hasTokens && item.assetOfTransaction == null) return;
-
- try {
- //* ==============Confirm it has the right key for this item ========
- commonTestCases.hasValueKey(keyId);
-
- //* ======Confirm it displays the properly formatted amount==========
- commonTestCases.findWidgetViaDescendant(
- of: find.byKey(ValueKey(keyId)),
- matching: find.text(item.formattedCryptoAmount),
- );
-
- //* ======Confirm it displays the properly formatted date============
- final formattedDate = DateFormat('HH:mm').format(item.transaction.date);
- commonTestCases.findWidgetViaDescendant(
- of: find.byKey(ValueKey(keyId)),
- matching: find.text(formattedDate),
- );
-
- //* ======Confirm it displays the properly formatted fiat amount=====
- final formattedFiatAmount =
- dashboardViewModel.balanceViewModel.isFiatDisabled ? '' : item.formattedFiatAmount;
- if (formattedFiatAmount.isNotEmpty) {
- commonTestCases.findWidgetViaDescendant(
- of: find.byKey(ValueKey(keyId)),
- matching: find.text(formattedFiatAmount),
- );
- }
-
- //* ======Confirm it displays the right image based on the transaction direction=====
- final imageToUse = item.transaction.direction == TransactionDirection.incoming
- ? 'assets/images/down_arrow.png'
- : 'assets/images/up_arrow.png';
-
- find.widgetWithImage(Container, AssetImage(imageToUse));
- } catch (e) {
- tester.printToConsole('Error verifying transaction item ${item.transaction.id}: $e');
- }
- }
-
- Future<void> _verifyAnonpayTransactionListItemDisplay(AnonpayTransactionListItem item) async {
- final keyId = 'anonpay_invoice_transaction_list_item_${item.transaction.invoiceId}_key';
-
- //* ==============Confirm it has the right key for this item ========
- commonTestCases.hasValueKey(keyId);
-
- //* ==============Confirm it displays the correct provider =========================
- commonTestCases.hasText(item.transaction.provider);
-
- //* ===========Confirm it displays the properly formatted amount with currency ========
- final currency = item.transaction.fiatAmount != null
- ? item.transaction.fiatEquiv ?? ''
- : CryptoCurrency.fromFullName(item.transaction.coinTo).name.toUpperCase();
-
- final amount =
- item.transaction.fiatAmount?.toString() ?? (item.transaction.amountTo?.toString() ?? '');
-
- final amountCurrencyText = amount + ' ' + currency;
-
- commonTestCases.hasText(amountCurrencyText);
-
- //* ======Confirm it displays the properly formatted date=================
- final formattedDate = DateFormat('HH:mm').format(item.transaction.createdAt);
- commonTestCases.hasText(formattedDate);
-
- //* ===============Confirm it displays the right image====================
- find.widgetWithImage(ClipRRect, AssetImage('assets/images/trocador.png'));
- }
-
- Future<void> _verifyTradeListItemDisplay(TradeListItem item) async {
- final keyId = 'trade_list_item_${item.trade.id}_key';
- final from = item.trade.from?.toString() ?? '';
- final to = item.trade.to?.toString() ?? '';
-
- //* ==============Confirm it has the right key for this item ========
- commonTestCases.hasValueKey(keyId);
-
- //* ==============Confirm it displays the correct provider =========================
- final conversionFlow = '$from → $to';
-
- commonTestCases.hasText(conversionFlow);
-
- //* ===========Confirm it displays the properly formatted amount with its crypto tag ========
-
- final amountCryptoText = item.tradeFormattedAmount + ' ' + from;
-
- commonTestCases.hasText(amountCryptoText);
-
- //* ======Confirm it displays the properly formatted date=================
- final createdAtFormattedDate =
- item.trade.createdAt != null ? DateFormat('HH:mm').format(item.trade.createdAt!) : null;
-
- if (createdAtFormattedDate != null) {
- commonTestCases.hasText(createdAtFormattedDate);
- }
-
- //* ===============Confirm it displays the right image====================
- commonTestCases.hasValueKey(item.trade.provider.image);
- }
-
- Future<void> _verifyOrderListItemDisplay(OrderListItem item) async {
- final keyId = 'order_list_item_${item.order.id}_key';
-
- //* ==============Confirm it has the right key for this item ========
- commonTestCases.hasValueKey(keyId);
-
- //* ==============Confirm it displays the correct provider =========================
- final orderFlow = '${item.order.from!} → ${item.order.to}';
-
- commonTestCases.hasText(orderFlow);
-
- //* ===========Confirm it displays the properly formatted amount with its crypto tag ========
-
- final amountCryptoText = item.orderFormattedAmount + ' ' + item.order.to!;
-
- commonTestCases.hasText(amountCryptoText);
-
- //* ======Confirm it displays the properly formatted date=================
- final createdAtFormattedDate = DateFormat('HH:mm').format(item.order.createdAt);
-
- commonTestCases.hasText(createdAtFormattedDate);
- }
-}
### integration_test/robots/wallet_edit_page_robot.dart
@@ -0,0 +1,50 @@
+import "package:cake_wallet/src/screens/wallet/wallet_edit_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class WalletEditPageRobot extends BaseRobot {
+ WalletEditPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletEditPage>();
+ }
+
+ Future<void> enterName(String name) async {
+ final input = find.byKey(const ValueKey("wallet_edit_page_name_input_key"));
+
+ await pumpUntilFound(input);
+
+ final field = find.descendant(of: input, matching: find.byType(EditableText));
+
+ await tester.enterText(field.first, name);
+
+ await tester.pump(const Duration(milliseconds: 300));
+ }
+
+ Future<void> save() async {
+ await tapByKey("wallet_edit_page_save_button_key");
+ }
+
+ Future<void> tapDelete() async {
+ await tapByKey("wallet_edit_page_delete_button_key");
+ }
+
+ Future<void> confirmDelete() async {
+ await tapByKey("wallet_edit_page_confirm_delete_button_key");
+ }
+
+ Future<void> expectNameTakenRefusal() async {
+ await pumpUntilFound(find.byKey(const ValueKey("wallet_edit_page_name_taken_ok_button_key")));
+
+ hasType<WalletEditPage>();
+ }
+
+ Future<void> dismissNameTakenRefusal() async {
+ await tapByKey("wallet_edit_page_name_taken_ok_button_key");
+
+ await settle();
+ }
+}
### integration_test/robots/wallet_group_description_page_robot.dart
@@ -1,39 +1,25 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class WalletGroupDescriptionPageRobot {
- WalletGroupDescriptionPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class WalletGroupDescriptionPageRobot extends BaseRobot {
+ WalletGroupDescriptionPageRobot(super.tester);
- final WidgetTester tester;
- final CommonTestCases commonTestCases;
-
- Future<void> isWalletGroupDescriptionPage() async {
- await commonTestCases.isSpecificPage<WalletGroupDescriptionPage>();
- await commonTestCases.takeScreenshots('wallet_group_description_page');
- }
-
- void hasTitle() {
- commonTestCases.hasText(S.current.wallet_group);
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletGroupDescriptionPage>();
}
- bool hasNewSingleSeedButton() {
- return commonTestCases.isKeyPresent('wallet_group_description_page_create_new_seed_button_key');
- }
+ bool hasNewSingleSeedButton() =>
+ isKeyPresent("wallet_group_description_page_create_new_seed_button_key");
Future<void> navigateToCreateNewSeedPage() async {
if (hasNewSingleSeedButton()) {
- await commonTestCases.tapItemByKey(
- 'wallet_group_description_page_create_new_seed_button_key',
- );
+ await tapByKey("wallet_group_description_page_create_new_seed_button_key");
}
}
Future<void> navigateToChooseWalletGroup() async {
- await commonTestCases.tapItemByKey(
- 'wallet_group_description_page_choose_wallet_group_button_key',
- );
+ await tapByKey("wallet_group_description_page_choose_wallet_group_button_key");
}
}
### integration_test/robots/wallet_group_existing_seed_page_robot.dart
@@ -0,0 +1,16 @@
+import "package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart";
+
+import "../core/base_robot.dart";
+
+class WalletGroupExistingSeedPageRobot extends BaseRobot {
+ WalletGroupExistingSeedPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletGroupExistingSeedDescriptionPage>();
+ }
+
+ Future<void> openWallet() async {
+ await tapByKey("wallet_group_existing_seed_description_page_open_wallet_button_key");
+ }
+}
### integration_test/robots/wallet_groups_display_page_robot.dart
@@ -0,0 +1,21 @@
+import "package:cake_wallet/src/screens/new_wallet/wallet_group_display_page.dart";
+import "package:flutter_test/flutter_test.dart";
+
+import "../core/base_robot.dart";
+
+class WalletGroupsDisplayPageRobot extends BaseRobot {
+ WalletGroupsDisplayPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletGroupsDisplayPage>();
+ }
+
+ Future<void> selectWallet(String name) async {
+ await tapWhenVisible(find.text(name));
+ }
+
+ Future<void> continueWithSelectedSeed() async {
+ await tapByKey("wallet_group_display_page_next_button_key");
+ }
+}
### integration_test/robots/wallet_keys_robot.dart
@@ -1,48 +1,38 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/reactions/wallet_connect.dart';
-import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:cw_core/monero_wallet_keys.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:cw_monero/monero_wallet.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:polyseed/polyseed.dart';
-
-import '../components/common_test_cases.dart';
-
-class WalletKeysAndSeedPageRobot {
- WalletKeysAndSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
-
- final WidgetTester tester;
- final CommonTestCases commonTestCases;
-
- Future<void> isWalletKeysAndSeedPage() async {
- await commonTestCases.isSpecificPage<WalletKeysPage>();
- await commonTestCases.takeScreenshots('wallet_keys_page');
+import "package:cake_wallet/reactions/wallet_connect.dart";
+import "package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/monero_wallet_keys.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:cw_monero/monero_wallet.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:polyseed/polyseed.dart";
+
+import "../core/base_robot.dart";
+
+class WalletKeysAndSeedPageRobot extends BaseRobot {
+ WalletKeysAndSeedPageRobot(super.tester);
+
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletKeysPage>();
}
- void hasTitle() {
- final walletKeysPage = tester.widget<WalletKeysPage>(find.byType(WalletKeysPage));
- final walletKeysViewModel = walletKeysPage.walletKeysViewModel;
- commonTestCases.hasText(walletKeysViewModel.title);
- }
-
- void hasShareWarning() {
- commonTestCases.hasText(S.current.do_not_share_warning_text.toUpperCase());
- }
+ int _verifiedCredentials = 0;
Future<void> confirmWalletCredentials(WalletType walletType) async {
+ _verifiedCredentials = 0;
+
final walletKeysPage = tester.widget<WalletKeysPage>(find.byType(WalletKeysPage));
final walletKeysViewModel = walletKeysPage.walletKeysViewModel;
final appStore = walletKeysViewModel.appStore;
final walletName = walletType.name;
- bool hasSeed = appStore.wallet!.seed != null;
- bool hasHexSeed = appStore.wallet!.hexSeed != null;
- bool hasPrivateKey = appStore.wallet!.privateKey != null;
+ final hasSeed = appStore.wallet!.seed != null;
+ final hasHexSeed = appStore.wallet!.hexSeed != null;
+ final hasPrivateKey = appStore.wallet!.privateKey != null;
if (walletType == WalletType.monero) {
- final moneroWallet = appStore.wallet as MoneroWalletBase;
+ final moneroWallet = appStore.wallet! as MoneroWalletBase;
final lang = PolyseedLang.getByPhrase(moneroWallet.seed);
final legacySeed = moneroWallet.seedLegacy(lang.nameEnglish);
@@ -57,51 +47,89 @@ class WalletKeysAndSeedPageRobot {
if (walletType == WalletType.bitcoin ||
walletType == WalletType.litecoin ||
walletType == WalletType.bitcoinCash) {
- final seedWords = appStore.wallet!.seed!.split(" ");
- for (var seedWord in seedWords) {
- commonTestCases.hasTextAtLestOnce(seedWord);
- }
- tester.printToConsole('$walletName wallet has seeds properly displayed');
+ _showsSeedWords(appStore.wallet!.seed!);
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (isEVMCompatibleChain(walletType) ||
walletType == WalletType.solana ||
walletType == WalletType.tron) {
if (hasSeed) {
- final seedWords = appStore.wallet!.seed!.split(" ");
- for (var seedWord in seedWords) {
- commonTestCases.hasTextAtLestOnce(seedWord);
- }
- tester.printToConsole('$walletName wallet has seeds properly displayed');
+ _showsSeedWords(appStore.wallet!.seed!);
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (hasPrivateKey) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(appStore.wallet!.privateKey!);
- tester.printToConsole('$walletName wallet has private key properly displayed');
+ await _openKeysTab();
+ _showsCredentialOnce(appStore.wallet!.privateKey!, "private key");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has private key properly displayed");
}
}
if (walletType == WalletType.nano || walletType == WalletType.banano) {
if (hasSeed) {
- final seedWords = appStore.wallet!.seed!.split(" ");
- for (var seedWord in seedWords) {
- commonTestCases.hasTextAtLestOnce(seedWord);
- }
- tester.printToConsole('$walletName wallet has seeds properly displayed');
+ _showsSeedWords(appStore.wallet!.seed!);
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (hasHexSeed) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(appStore.wallet!.hexSeed!);
- tester.printToConsole('$walletName wallet has hexSeed properly displayed');
+ await _openKeysTab();
+ _showsCredentialOnce(appStore.wallet!.hexSeed!, "hex seed");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has hexSeed properly displayed");
}
if (hasPrivateKey) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(appStore.wallet!.privateKey!);
- tester.printToConsole('$walletName wallet has private key properly displayed');
+ await _openKeysTab();
+ _showsCredentialOnce(appStore.wallet!.privateKey!, "private key");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has private key properly displayed");
}
}
- await commonTestCases.defaultSleepTime(seconds: 5);
+ _expectSomethingWasVerified(walletType);
+ }
+
+ void _expectSomethingWasVerified(WalletType walletType) {
+ expect(
+ _verifiedCredentials,
+ greaterThan(0),
+ reason: "Nothing was verified for ${walletType.name}, this suite covers no credential "
+ "for that type so it cannot tell a working keys page from a broken one",
+ );
+ }
+
+ Future<void> _openKeysTab() async {
+ await tapByKey("wallet_keys_page_keys");
+
+ await settle();
+ }
+
+ void _showsCredential(String value, String label) {
+ expect(
+ tester.any(find.text(value, skipOffstage: false)),
+ true,
+ reason: "The keys page did not show the wallet's $label",
+ );
+ }
+
+ void _showsCredentialOnce(String value, String label) {
+ final rows = tester.widgetList(find.text(value, skipOffstage: false)).length;
+
+ expect(
+ rows,
+ 1,
+ reason: "The keys page showed $rows rows for the wallet's $label, expected one",
+ );
+ }
+
+ void _showsSeedWords(String seed) {
+ final words = seed.split(" ");
+
+ for (var index = 0; index < words.length; index++) {
+ _showsCredential(words[index], "seed word ${index + 1}");
+ }
}
Future<void> _confirmMoneroWalletCredentials(
@@ -111,68 +139,42 @@ class WalletKeysAndSeedPageRobot {
String legacySeed,
) async {
final keys = appStore.wallet!.keys as MoneroWalletKeys;
-
- final hasPublicSpendKey = commonTestCases.isKeyPresent(
- '${walletName}_wallet_public_spend_key_item_key',
- );
- final hasPrivateSpendKey = commonTestCases.isKeyPresent(
- '${walletName}_wallet_private_spend_key_item_key',
- );
- final hasPublicViewKey = commonTestCases.isKeyPresent(
- '${walletName}_wallet_public_view_key_item_key',
- );
- final hasPrivateViewKey = commonTestCases.isKeyPresent(
- '${walletName}_wallet_private_view_key_item_key',
- );
final hasSeeds = seed.isNotEmpty;
final hasSeedLegacy = Polyseed.isValidSeed(seed);
- if (hasPublicSpendKey) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(keys.publicSpendKey);
- tester.printToConsole('$walletName wallet has public spend key properly displayed');
- }
- if (hasPrivateSpendKey) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(keys.privateSpendKey);
- tester.printToConsole('$walletName wallet has private spend key properly displayed');
- }
- if (hasPublicViewKey) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(keys.publicViewKey);
- tester.printToConsole('$walletName wallet has public view key properly displayed');
- }
- if (hasPrivateViewKey) {
- await commonTestCases.tapItemByKey('wallet_keys_page_keys');
- commonTestCases.hasText(keys.privateViewKey);
- tester.printToConsole('$walletName wallet has private view key properly displayed');
- }
+ await _openKeysTab();
+
+ _showsCredentialOnce(keys.publicSpendKey, "public spend key");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has public spend key properly displayed");
+
+ _showsCredentialOnce(keys.privateSpendKey, "private spend key");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has private spend key properly displayed");
+
+ _showsCredentialOnce(keys.publicViewKey, "public view key");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has public view key properly displayed");
+
+ _showsCredentialOnce(keys.privateViewKey, "private view key");
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has private view key properly displayed");
+
if (hasSeeds) {
- await commonTestCases.tapItemByKey('wallet_keys_page_seed');
- final seedWords = seed.split(" ");
- for (var seedWord in seedWords) {
- commonTestCases.hasTextAtLestOnce(seedWord);
- }
- tester.printToConsole('$walletName wallet has seeds properly displayed');
+ await tapByKey("wallet_keys_page_seed");
+ await settle();
+
+ _showsSeedWords(seed);
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (hasSeedLegacy) {
- await commonTestCases.tapItemByKey('wallet_keys_page_seed_legacy');
- final seedWords = legacySeed.split(" ");
- for (var seedWord in seedWords) {
- commonTestCases.hasTextAtLestOnce(seedWord);
- }
- tester.printToConsole('$walletName wallet has legacy seeds properly displayed');
- }
- }
-
- Future<void> backToDashboard() async {
- tester.printToConsole('Going back to dashboard from credentials page');
- await tester.pumpAndSettle(Duration(milliseconds: 1000));
+ await tapByKey("wallet_keys_page_seed_legacy");
+ await settle();
- await commonTestCases.goBack();
- await tester.pumpAndSettle(Duration(milliseconds: 1000));
-
- await commonTestCases.goBack();
- await tester.pumpAndSettle(Duration(milliseconds: 2000));
+ _showsSeedWords(legacySeed);
+ _verifiedCredentials++;
+ tester.printToConsole("$walletName wallet has legacy seeds properly displayed");
+ }
}
}
### integration_test/robots/wallet_list_page_robot.dart
@@ -1,28 +1,48 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart";
+import "package:flutter/material.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class WalletListPageRobot {
- WalletListPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class WalletListPageRobot extends BaseRobot {
+ WalletListPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
+ @override
+ Future<void> isDisplayed() async {
+ final shown = await pumpUntil(_isShowing);
- Future<void> isWalletListPage() async {
- await commonTestCases.isSpecificPage<WalletListPageRobot>();
- await commonTestCases.takeScreenshots('wallet_list_page');
+ expect(shown, true, reason: "The wallets tab never came to the front");
}
- void displaysCorrectTitle() {
- commonTestCases.hasText(S.current.wallets);
+ Future<void> openEditFor(String walletName) async {
+ await tapByKey("wallet_list_edit_${walletName}_button_key");
}
+ bool hasWallet(String walletName) => tester.any(
+ find.descendant(
+ of: find.byType(WalletListPage),
+ matching: find.text(walletName),
+ ),
+ );
+
Future<void> navigateToCreateNewWalletPage() async {
- await commonTestCases.tapItemByKey('wallet_list_page_create_new_wallet_button_key');
+ await tapByKey("wallet_list_page_create_new_wallet_button_key");
}
Future<void> navigateToRestoreWalletOptionsPage() async {
- await commonTestCases.tapItemByKey('wallet_list_page_restore_wallet_button_key');
+ await tapByKey("wallet_list_page_restore_wallet_button_key");
+ }
+
+ bool _isShowing() {
+ final stackFinder = find.byType(IndexedStack);
+
+ if (!tester.any(stackFinder)) {
+ return false;
+ }
+
+ final stack = tester.widget<IndexedStack>(stackFinder.first);
+ final index = stack.children.indexWhere((child) => child is WalletListPage);
+
+ return index >= 0 && stack.index == index;
}
}
### integration_test/robots/wallet_seed_page_robot.dart
@@ -1,38 +1,19 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/src/screens/seed/wallet_seed_page.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class WalletSeedPageRobot {
- WalletSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class WalletSeedPageRobot extends BaseRobot {
+ WalletSeedPageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isWalletSeedPage() async {
- await commonTestCases.isSpecificPage<WalletSeedPage>();
- await commonTestCases.takeScreenshots('wallet_seed_page');
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WalletSeedPage>();
}
Future<void> onVerifySeedButtonPressed() async {
- await commonTestCases.tapItemByKey('wallet_seed_page_verify_seed_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onSaveSeedButtonPressed() async {
- await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onConfirmButtonOnSeedAlertDialogPressed() async {
- await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_confirm_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onBackButtonOnSeedAlertDialogPressed() async {
- await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_back_button_key');
- await commonTestCases.defaultSleepTime();
+ await tapByKey("wallet_seed_page_verify_seed_button_key");
}
void confirmWalletDetailsDisplayCorrectly() {
@@ -42,23 +23,18 @@ class WalletSeedPageRobot {
final walletName = walletSeedViewModel.name;
final walletSeeds = walletSeedViewModel.seedSplit;
- commonTestCases.hasText(walletName);
- for (var seed in walletSeeds) {
- commonTestCases.hasTextAtLestOnce(seed);
+ hasText(walletName);
+
+ for (var index = 0; index < walletSeeds.length; index++) {
+ expect(
+ tester.any(find.text(walletSeeds[index])),
+ true,
+ reason: "The seed page did not show seed word ${index + 1}",
+ );
}
}
void confirmWalletSeedReminderDisplays() {
- commonTestCases.hasText(S.current.cake_seeds_save_disclaimer);
- }
-
- Future<void> onSaveSeedsButtonPressed() async {
- await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> onCopySeedsButtonPressed() async {
- await commonTestCases.tapItemByKey('wallet_seed_page_copy_seeds_button_key');
- await commonTestCases.defaultSleepTime();
+ hasText(S.current.cake_seeds_save_disclaimer);
}
}
### integration_test/robots/welcome_page_robot.dart
@@ -1,50 +1,28 @@
-import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
+import "package:cake_wallet/src/screens/welcome/welcome_page.dart";
+import "package:flutter_test/flutter_test.dart";
-import '../components/common_test_cases.dart';
+import "../core/base_robot.dart";
-class WelcomePageRobot {
- WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
+class WelcomePageRobot extends BaseRobot {
+ WelcomePageRobot(super.tester);
- final WidgetTester tester;
- late CommonTestCases commonTestCases;
-
- Future<void> isWelcomePage() async {
- await commonTestCases.isSpecificPage<WelcomePage>();
- await commonTestCases.takeScreenshots('welcome_page');
- }
-
- void confirmActionButtonsDisplay() {
- final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key'));
-
- final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key'));
-
- expect(createNewWalletButton, findsOneWidget);
- expect(restoreWalletButton, findsOneWidget);
+ @override
+ Future<void> isDisplayed() async {
+ await isSpecificPage<WelcomePage>();
}
Future<void> navigateToCreateNewWalletPage() async {
- await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key');
- await commonTestCases.defaultSleepTime();
+ await tapByKey("welcome_page_create_new_wallet_button_key");
}
- bool hasNewSingleSeedButton() {
- return commonTestCases.isKeyPresent('wallet_group_description_page_create_new_seed_button_key');
- }
+ bool hasNewSingleSeedButton() =>
+ isKeyPresent("wallet_group_description_page_create_new_seed_button_key");
Future<void> tapNewSingleSeed() async {
- await commonTestCases.tapItemByKey('wallet_group_description_page_create_new_seed_button_key');
- await commonTestCases.defaultSleepTime();
+ await tapByKey("wallet_group_description_page_create_new_seed_button_key");
}
Future<void> navigateToRestoreWalletPage() async {
- await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key');
- await commonTestCases.defaultSleepTime();
- }
-
- Future<void> backAndVerify() async {
- await commonTestCases.goBack();
- await isWelcomePage();
+ await tapByKey("welcome_page_restore_wallet_button_key");
}
}
### integration_test/suites/tier0/address_book_test.dart
@@ -0,0 +1,46 @@
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_wallets.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/contact_robot.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A saved contact keeps the address it was given", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final contactRobot = ContactRobot(tester);
+
+ const contactName = "Integration Test Contact";
+ final contactAddress = TestWallets.receiveAddressFor(WalletType.solana);
+
+ await appLauncher.launchApp(testKey: "address_book_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await dashboardRobot.openContactsTab();
+ await contactRobot.isDisplayed();
+
+ await contactRobot.openAddContact();
+
+ await contactRobot.enterName(contactName);
+ await contactRobot.chooseCurrency("sol");
+ await contactRobot.enterAddress(contactAddress);
+
+ await contactRobot.save();
+
+ await contactRobot.isDisplayed();
+
+ contactRobot.expectContactSaved(contactName, contactAddress);
+ });
+}
### integration_test/suites/tier0/change_pin_test.dart
@@ -0,0 +1,73 @@
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_config.dart";
+import "../../flows/auth_flows.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+import "../../robots/security_backup_page_robot.dart";
+import "../../robots/setup_pin_code_robot.dart";
+import "../../robots/show_keys_disclaimer_page_robot.dart";
+import "../../robots/wallet_keys_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Changing the pin replaces the one that unlocks the wallet", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final authFlows = AuthFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+ final securityRobot = SecurityBackupPageRobot(tester);
+ final setupPinRobot = SetupPinCodeRobot(tester);
+ final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
+ final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
+
+ final newPin = [1, 2, 3, 4];
+
+ expect(newPin, isNot(TestConfig.pin), reason: "The new pin has to differ from the old one");
+
+ await appLauncher.launchApp(testKey: "change_pin_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.securityBackupPage);
+ await securityRobot.isDisplayed();
+
+ await securityRobot.openChangePin();
+ await authFlows.authenticateWithPin();
+
+ await setupPinRobot.isDisplayed();
+ await setupPinRobot.enterPinCode(newPin);
+ await setupPinRobot.enterPinCode(newPin);
+ await setupPinRobot.tapSuccessButton();
+
+ await securityRobot.isDisplayed();
+
+ await securityRobot.goBack();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.showKeysDisclaimer);
+ await disclaimerRobot.isDisplayed();
+ await disclaimerRobot.tapShowKeys();
+ await authFlows.authenticateWithPin(pin: newPin);
+
+ await walletKeysRobot.isDisplayed();
+
+ expect(find.byType(WalletKeysPage), findsOneWidget);
+ });
+}
### integration_test/suites/tier0/duplicate_wallet_name_test.dart
@@ -0,0 +1,42 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_wallet_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A wallet name already in use is refused", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final newWalletPageRobot = NewWalletPageRobot(tester);
+
+ const walletType = WalletType.solana;
+
+ await appLauncher.launchApp(testKey: "duplicate_wallet_name_test_app_key");
+
+ await onboardingFlows.createFirstWallet(walletType);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final existingName = getIt.get<AppStore>().wallet!.name;
+
+ await dashboardRobot.openWalletsTab();
+
+ await onboardingFlows.startCreatingWalletFromWalletList(walletType);
+
+ await newWalletPageRobot.enterWalletName(existingName);
+ await newWalletPageRobot.onNextButtonPressed();
+
+ await newWalletPageRobot.expectNameRejected();
+ });
+}
### integration_test/suites/tier0/fiat_currency_test.dart
@@ -0,0 +1,107 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/entities/fiat_api_mode.dart";
+import "package:cake_wallet/entities/fiat_currency.dart";
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/store/settings_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/connection_sync_page_robot.dart";
+import "../../robots/display_settings_page_robot.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Turning the fiat api off hides the currency setting, turning it on returns it",
+ (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+ final connectionRobot = ConnectionSyncPageRobot(tester);
+ final displayRobot = DisplaySettingsPageRobot(tester);
+
+ const wanted = FiatCurrency.eur;
+
+ await appLauncher.launchApp(testKey: "fiat_currency_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final settingsStore = getIt.get<SettingsStore>();
+
+ expect(
+ settingsStore.fiatCurrency,
+ isNot(wanted),
+ reason: "The wallet already starts on $wanted, so changing to it would prove nothing",
+ );
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ if (settingsStore.fiatApiMode != FiatApiMode.disabled) {
+ await settingsRobot.openRow(Routes.connectionSync);
+ await connectionRobot.isDisplayed();
+
+ await connectionRobot.setFiatApiMode(FiatApiMode.disabled);
+ connectionRobot.hasFiatApiMode(FiatApiMode.disabled);
+
+ expect(settingsStore.fiatApiMode, FiatApiMode.disabled);
+
+ await connectionRobot.goBack();
+ await settingsRobot.isDisplayed();
+ }
+
+ await settingsRobot.openRow(Routes.displaySettingsPage);
+ await displayRobot.isDisplayed();
+
+ expect(
+ displayRobot.hasFiatCurrencyRow(),
+ false,
+ reason: "The currency setting is still showing while the fiat api is off",
+ );
+
+ await displayRobot.goBack();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.connectionSync);
+ await connectionRobot.isDisplayed();
+
+ await connectionRobot.setFiatApiMode(FiatApiMode.enabled);
+ connectionRobot.hasFiatApiMode(FiatApiMode.enabled);
+
+ expect(settingsStore.fiatApiMode, FiatApiMode.enabled);
+
+ await connectionRobot.goBack();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.displaySettingsPage);
+ await displayRobot.isDisplayed();
+
+ expect(
+ displayRobot.hasFiatCurrencyRow(),
+ true,
+ reason: "The currency setting did not come back after the fiat api was turned on",
+ );
+
+ await displayRobot.openFiatCurrencyPicker();
+ await displayRobot.chooseFiatCurrency(wanted);
+
+ displayRobot.expectFiatCurrencyShown(wanted);
+
+ expect(
+ settingsStore.fiatCurrency,
+ wanted,
+ reason: "The row shows ${wanted.title} but the setting behind it did not change",
+ );
+ });
+}
### integration_test/suites/tier0/invalid_seed_test.dart
@@ -0,0 +1,51 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_wallets.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/restore_from_seed_or_key_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A seed the wallet cannot parse restores nothing", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final restoreRobot = RestoreFromSeedOrKeysPageRobot(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ const walletType = WalletType.solana;
+
+ await appLauncher.launchApp(testKey: "invalid_seed_test_app_key");
+
+ await onboardingFlows.startRestoringFirstWallet(walletType);
+
+ // Twelve words that are not in the wordlist. Restoring from this would either fail
+ // outright or, worse it derives some other wallet the user has no keys for.
+ await restoreRobot.enterSeedPhraseForWalletRestore(
+ "alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima",
+ );
+
+ await restoreRobot.onRestoreWalletButtonPressed();
+
+ await restoreRobot.expectRestoreRefused();
+
+ // The same form with the real seed goes through. Without this the test above would pass
+ // just as happily if the form were broken and restored nothing for any seed at all.
+ await restoreRobot.selectWalletNameFromAvailableOptions();
+ await restoreRobot.enterSeedPhraseForWalletRestore(TestWallets.seedFor(walletType));
+ await restoreRobot.onRestoreWalletButtonPressed();
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ expect(getIt.get<AppStore>().wallet?.type, walletType);
+ });
+}
### integration_test/suites/tier0/language_test.dart
@@ -0,0 +1,46 @@
+import "package:cake_wallet/routes.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/display_settings_page_robot.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Changing the language changes what the settings screen shows", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+ final displayRobot = DisplaySettingsPageRobot(tester);
+
+ // Not the default, so seeing it afterwards means the change took rather than the screen
+ // having always said so.
+ const languageCode = "de";
+ const languageName = "Deutsch (German)";
+
+ await appLauncher.launchApp(testKey: "language_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.displaySettingsPage);
+ await displayRobot.isDisplayed();
+
+ await displayRobot.openLanguagePicker();
+ await displayRobot.chooseLanguage(languageCode);
+
+ displayRobot.expectLanguageShown(languageName);
+ });
+}
### integration_test/suites/tier0/onboarding_create_test.dart
@@ -0,0 +1,53 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_config.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Create a wallet for each configured wallet type", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "onboarding_create_test_app_key");
+
+ final walletTypes = TestConfig.walletTypesUnderTest;
+ final firstType = walletTypes.first;
+
+ tester.printToConsole("Creating first wallet: ${firstType.name}");
+
+ await onboardingFlows.createFirstWallet(firstType);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+ expect(appStore.wallet?.type, firstType);
+
+ await homePageRobot.hasWalletName(appStore.wallet!.name);
+
+ for (final type in walletTypes.skip(1)) {
+ tester.printToConsole("Creating additional wallet: ${type.name}");
+
+ await dashboardRobot.openWalletsTab();
+
+ await onboardingFlows.createAdditionalWalletFromWalletList(type);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ expect(appStore.wallet?.type, type);
+
+ await homePageRobot.hasWalletName(appStore.wallet!.name);
+ }
+ });
+}
### integration_test/suites/tier0/onboarding_restore_test.dart
@@ -0,0 +1,94 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_config.dart";
+import "../../core/test_wallets.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Restore a wallet from seed for each configured wallet type", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "onboarding_restore_test_app_key");
+
+ final walletTypes = TestConfig.walletTypesUnderTest;
+ final firstType = walletTypes.first;
+
+ tester.printToConsole("Restoring first wallet: ${firstType.name}");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(firstType);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+ expect(appStore.wallet?.type, firstType);
+
+ await homePageRobot.hasWalletName(appStore.wallet!.name);
+
+ await _expectRestoredTheRightWallet(homePageRobot, appStore, firstType);
+
+ for (final type in walletTypes.skip(1)) {
+ tester.printToConsole("Restoring additional wallet: ${type.name}");
+
+ await dashboardRobot.openWalletsTab();
+
+ await onboardingFlows.restoreAdditionalWalletFromWalletList(type);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ expect(appStore.wallet?.type, type);
+
+ await homePageRobot.hasWalletName(appStore.wallet!.name);
+
+ await _expectRestoredTheRightWallet(homePageRobot, appStore, type);
+ }
+ });
+}
+
+Future<void> _expectRestoredTheRightWallet(
+ HomePageRobot robot,
+ AppStore appStore,
+ WalletType type,
+) async {
+ final expectedAddress = TestWallets.receiveAddressFor(type);
+
+ if (expectedAddress.isEmpty) {
+ robot.tester.printToConsole("No known address for ${type.name}, derivation not verified");
+ return;
+ }
+
+ final settled = await robot.pumpUntil(
+ () {
+ final addresses = appStore.wallet?.walletAddresses;
+
+ if (addresses == null) {
+ return false;
+ }
+
+ return addresses.address == expectedAddress ||
+ addresses.addressesMap.containsKey(expectedAddress) ||
+ addresses.allAddressesMap.containsKey(expectedAddress);
+ },
+ timeout: const Duration(seconds: 60),
+ );
+
+ expect(
+ settled,
+ true,
+ reason: "${type.name} restored from a known seed derived "
+ "${appStore.wallet?.walletAddresses.address} and does not know $expectedAddress",
+ );
+}
### integration_test/suites/tier0/receive_address_test.dart
@@ -0,0 +1,43 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_receive_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Receive sheet shows the opened wallet's own address", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final receiveRobot = NewReceivePageRobot(tester);
+
+ const walletType = WalletType.solana;
+
+ await appLauncher.launchApp(testKey: "receive_address_test_app_key");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(walletType);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final expectedAddress = getIt.get<AppStore>().wallet!.walletAddresses.address;
+ expect(expectedAddress.isNotEmpty, true);
+
+ await homePageRobot.openReceiveSheet();
+
+ await receiveRobot.isDisplayed();
+ await receiveRobot.confirmAddressMatches(expectedAddress);
+
+ await receiveRobot.dismissModal();
+ await homePageRobot.isDisplayed();
+ });
+}
### integration_test/suites/tier0/seed_confirmation_test.dart
@@ -0,0 +1,70 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/auth_flows.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+import "../../robots/show_keys_disclaimer_page_robot.dart";
+import "../../robots/wallet_keys_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Displayed seed and keys match the opened wallet", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final authFlows = AuthFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+ final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
+ final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
+
+ // These three cover the different key display paths on the wallet keys page.
+ final walletTypes = [WalletType.solana, WalletType.bitcoin, WalletType.monero];
+
+ await appLauncher.launchApp(testKey: "seed_confirmation_test_app_key");
+
+ await onboardingFlows.createFirstWallet(walletTypes.first);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+
+ for (final type in walletTypes) {
+ if (type != walletTypes.first) {
+ await dashboardRobot.openWalletsTab();
+ await onboardingFlows.createAdditionalWalletFromWalletList(type);
+ await homePageRobot.isDisplayed();
+ }
+
+ expect(appStore.wallet?.type, type);
+
+ tester.printToConsole("Confirming displayed credentials for ${type.name}");
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.showKeysDisclaimer);
+ await disclaimerRobot.isDisplayed();
+ await disclaimerRobot.tapShowKeys();
+ await authFlows.authenticateWithPin();
+
+ await walletKeysRobot.isDisplayed();
+ await walletKeysRobot.confirmWalletCredentials(type);
+
+ await settingsRobot.goBack();
+ await settingsRobot.dismissModal();
+
+ await homePageRobot.isDisplayed();
+ }
+ });
+}
### integration_test/suites/tier0/send_from_address_book_test.dart
@@ -0,0 +1,59 @@
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_wallets.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/contact_robot.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_send_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A contact picked on the send screen fills in its own address", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final contactRobot = ContactRobot(tester);
+ final sendRobot = NewSendPageRobot(tester);
+
+ const contactName = "Send Book Contact";
+ final contactAddress = TestWallets.receiveAddressFor(WalletType.solana);
+
+ await appLauncher.launchApp(testKey: "send_from_address_book_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await dashboardRobot.openContactsTab();
+ await contactRobot.isDisplayed();
+
+ await contactRobot.openAddContact();
+ await contactRobot.enterName(contactName);
+ await contactRobot.chooseCurrency("sol");
+ await contactRobot.enterAddress(contactAddress);
+ await contactRobot.save();
+
+ await contactRobot.isDisplayed();
+
+ await dashboardRobot.openHomeTab();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSendSheet();
+ await sendRobot.isDisplayed();
+
+ await sendRobot.pickContactFromAddressBook(contactName);
+
+ expect(
+ sendRobot.enteredAddress(),
+ contactAddress,
+ reason: "The send screen was filled with an address the contact does not hold",
+ );
+ });
+}
### integration_test/suites/tier0/send_validation_test.dart
@@ -0,0 +1,53 @@
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_wallets.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_send_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Send screen refuses what it cannot send", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final sendRobot = NewSendPageRobot(tester);
+
+ const walletType = WalletType.solana;
+
+ await appLauncher.launchApp(testKey: "send_validation_test_app_key");
+
+ await onboardingFlows.createFirstWallet(walletType);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSendSheet();
+ await sendRobot.isDisplayed();
+
+ await sendRobot.tapSendButtonWhenReady(timeout: const Duration(minutes: 5));
+ await sendRobot.expectNoTransactionBuilt();
+
+ // test that no transaction is built when the address is wrong or cannot be parsed
+ await sendRobot.enterAddress("not-a-real-address");
+ await sendRobot.enterAmount("0.0001");
+ await sendRobot.tapSendButton();
+
+ await sendRobot.expectNoTransactionBuilt();
+
+ // test that no transaction is built even if the address is valid but no balance
+ await sendRobot.enterAddress(TestWallets.receiveAddressFor(walletType));
+ await sendRobot.enterAmount("0.0001");
+ await sendRobot.tapSendButton();
+
+ await sendRobot.expectNoTransactionBuilt();
+
+ await sendRobot.dismissModal();
+ await homePageRobot.isDisplayed();
+ });
+}
### integration_test/suites/tier0/settings_nav_test.dart
@@ -0,0 +1,61 @@
+import "package:cake_wallet/new-ui/pages/about_page.dart";
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/src/screens/settings/connection_sync_page.dart";
+import "package:cake_wallet/src/screens/settings/display_settings_page.dart";
+import "package:cake_wallet/src/screens/settings/manage_nodes_page.dart";
+import "package:cake_wallet/src/screens/settings/other_settings_page.dart";
+import "package:cake_wallet/src/screens/settings/privacy_page.dart";
+import "package:cake_wallet/src/screens/settings/security_backup_page.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Every settings row opens its page and backs out", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+
+ final rowsUnderTest = <String, Type>{
+ Routes.manageNodes: ManageNodesPage,
+ Routes.privacyPage: PrivacyPage,
+ Routes.otherSettingsPage: OtherSettingsPage,
+ Routes.connectionSync: ConnectionSyncPage,
+ Routes.displaySettingsPage: DisplaySettingsPage,
+ Routes.securityBackupPage: SecurityBackupPage,
+ Routes.aboutPage: AboutPage,
+ };
+
+ await appLauncher.launchApp(testKey: "settings_nav_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ for (final entry in rowsUnderTest.entries) {
+ tester.printToConsole("Opening settings row ${entry.key}");
+
+ await settingsRobot.openRow(entry.key);
+ await settingsRobot.confirmLeafPageDisplayed(entry.value);
+
+ await settingsRobot.goBack();
+ await settingsRobot.isDisplayed();
+ }
+
+ await settingsRobot.dismissModal();
+ await homePageRobot.isDisplayed();
+ });
+}
### integration_test/suites/tier0/show_keys_auth_test.dart
@@ -0,0 +1,61 @@
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/auth_flows.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+import "../../robots/show_keys_disclaimer_page_robot.dart";
+import "../../robots/wallet_keys_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Seed and keys page rejects a wrong pin and accepts the right one",
+ (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final authFlows = AuthFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+ final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
+ final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
+
+ final wrongPin = [9, 9, 9, 9];
+
+ await appLauncher.launchApp(testKey: "show_keys_auth_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.showKeysDisclaimer);
+ await disclaimerRobot.isDisplayed();
+ await disclaimerRobot.tapShowKeys();
+
+ await authFlows.authenticateWithPin(pin: wrongPin);
+
+ // A wrong pin must keep the keys page locked away.
+ await settingsRobot.settle(max: const Duration(seconds: 3));
+ expect(find.byType(WalletKeysPage), findsNothing);
+ expect(authFlows.isPinWidgetShown(), true);
+
+ await authFlows.authenticateWithPin();
+
+ await walletKeysRobot.isDisplayed();
+
+ await settingsRobot.goBack();
+ await settingsRobot.dismissModal();
+ await homePageRobot.isDisplayed();
+ });
+}
### integration_test/suites/tier0/wallet_delete_test.dart
@@ -0,0 +1,85 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/auth_flows.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/wallet_edit_page_robot.dart";
+import "../../robots/wallet_list_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Deleting a wallet takes it off the list and out of storage", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final authFlows = AuthFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final walletListRobot = WalletListPageRobot(tester);
+ final walletEditRobot = WalletEditPageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "wallet_delete_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+ final deletedWallet = appStore.wallet!.name;
+
+ await dashboardRobot.openWalletsTab();
+ await onboardingFlows.createAdditionalWalletFromWalletList(WalletType.ethereum);
+
+ await homePageRobot.isDisplayed();
+
+ final keptWallet = appStore.wallet!.name;
+
+ expect(keptWallet, isNot(deletedWallet), reason: "The second wallet never became the open one");
+
+ await dashboardRobot.openWalletsTab();
+ await walletListRobot.isDisplayed();
+
+ await walletListRobot.openEditFor(deletedWallet);
+ await walletEditRobot.isDisplayed();
+
+ await walletEditRobot.tapDelete();
+ await authFlows.authenticateWithPin(required: false);
+
+ await walletEditRobot.confirmDelete();
+
+ await walletListRobot.isDisplayed();
+
+ final removed = await walletListRobot.pumpUntil(
+ () => !walletListRobot.hasWallet(deletedWallet),
+ timeout: const Duration(minutes: 1),
+ );
+
+ expect(removed, true, reason: "$deletedWallet is still on the list after being deleted");
+
+ expect(
+ walletListRobot.hasWallet(keptWallet),
+ true,
+ reason: "Deleting $deletedWallet took $keptWallet with it",
+ );
+
+ final stored = await WalletInfo.getAll();
+ final names = stored.map((wallet) => wallet.name).toList();
+
+ expect(names, isNot(contains(deletedWallet)));
+ expect(names, contains(keptWallet));
+
+ expect(
+ appStore.wallet?.name,
+ keptWallet,
+ reason: "Deleting another wallet changed which one is open",
+ );
+ });
+}
### integration_test/suites/tier0/wallet_group_test.dart
@@ -0,0 +1,70 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../flows/wallet_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A wallet added to an existing seed shares it and both stay reachable",
+ (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final walletFlows = WalletFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ const firstType = WalletType.solana;
+ const secondType = WalletType.ethereum;
+
+ await appLauncher.launchApp(testKey: "wallet_group_test_app_key");
+
+ await onboardingFlows.createFirstWallet(firstType);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+ final firstWalletName = appStore.wallet!.name;
+ final sharedSeed = appStore.wallet!.seed;
+
+ expect(sharedSeed, isNotNull, reason: "The first wallet has no seed to share");
+
+ await dashboardRobot.openWalletsTab();
+
+ await onboardingFlows.createWalletInGroupOf(secondType, firstWalletName);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final secondWalletName = appStore.wallet!.name;
+
+ expect(
+ secondWalletName,
+ isNot(firstWalletName),
+ reason: "Adding to a group opened the wallet that was already there",
+ );
+
+ expect(appStore.wallet?.type, secondType);
+
+ expect(
+ appStore.wallet!.seed,
+ sharedSeed,
+ reason: "The wallet added to the group derived its own seed instead of sharing one",
+ );
+
+ await walletFlows.switchToWallet(firstWalletName);
+
+ await homePageRobot.isDisplayed();
+ await homePageRobot.hasWalletName(firstWalletName);
+
+ expect(appStore.wallet?.seed, sharedSeed);
+ });
+}
### integration_test/suites/tier0/wallet_rename_test.dart
@@ -0,0 +1,86 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/wallet_edit_page_robot.dart";
+import "../../robots/wallet_list_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A wallet can be renamed, but not onto a name already in use", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final walletListRobot = WalletListPageRobot(tester);
+ final walletEditRobot = WalletEditPageRobot(tester);
+
+ const renamedTo = "Renamed Test Wallet";
+
+ await appLauncher.launchApp(testKey: "wallet_rename_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+ final renamedWallet = appStore.wallet!.name;
+
+ await dashboardRobot.openWalletsTab();
+ await onboardingFlows.createAdditionalWalletFromWalletList(WalletType.ethereum);
+
+ await homePageRobot.isDisplayed();
+
+ final openWallet = appStore.wallet!.name;
+
+ expect(openWallet, isNot(renamedWallet), reason: "The second wallet never became the open one");
+
+ await dashboardRobot.openWalletsTab();
+ await walletListRobot.isDisplayed();
+
+ await walletListRobot.openEditFor(renamedWallet);
+ await walletEditRobot.isDisplayed();
+
+ await walletEditRobot.enterName(openWallet);
+ await walletEditRobot.save();
+
+ await walletEditRobot.expectNameTakenRefusal();
+ await walletEditRobot.dismissNameTakenRefusal();
+
+ await walletEditRobot.enterName(renamedTo);
+ await walletEditRobot.save();
+
+ await walletListRobot.isDisplayed();
+
+ final renamed = await walletListRobot.pumpUntil(() => walletListRobot.hasWallet(renamedTo));
+
+ expect(renamed, true, reason: "The list never showed $renamedTo after the rename was saved");
+
+ expect(
+ walletListRobot.hasWallet(renamedWallet),
+ false,
+ reason: "The list still shows $renamedWallet next to the name it was renamed to",
+ );
+
+ final stored = await WalletInfo.getAll();
+ final names = stored.map((wallet) => wallet.name).toList();
+
+ expect(names, contains(renamedTo));
+ expect(names, isNot(contains(renamedWallet)));
+
+ expect(
+ appStore.wallet?.name,
+ openWallet,
+ reason: "Renaming another wallet changed which one is open",
+ );
+ });
+}
### integration_test/suites/tier1/monero_legacy_seed_test.dart
@@ -0,0 +1,47 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../core/test_wallets.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("A 25 word monero seed restores through the legacy path", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ final legacySeed = TestWallets.moneroLegacySeed;
+
+ expect(
+ legacySeed.split(" ").length,
+ 25,
+ reason: "The legacy seed secret is not a 25 word seed, this suite would restore a polyseed",
+ );
+
+ await appLauncher.launchApp(testKey: "monero_legacy_seed_test_app_key");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(WalletType.monero, seed: legacySeed);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ final appStore = getIt.get<AppStore>();
+
+ expect(appStore.wallet?.type, WalletType.monero);
+
+ expect(
+ appStore.wallet?.seed?.split(" ").length,
+ 25,
+ reason: "The wallet came back with a seed of a different length than the one restored",
+ );
+ });
+}
### integration_test/suites/tier1/node_switching_test.dart
@@ -0,0 +1,49 @@
+import "package:cake_wallet/routes.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/manage_nodes_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_settings_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Switching nodes changes which one the wallet is pointed at", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final settingsRobot = NewSettingsPageRobot(tester);
+ final nodesRobot = ManageNodesPageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "node_switching_test_app_key");
+
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSettingsSheet();
+ await settingsRobot.isDisplayed();
+
+ await settingsRobot.openRow(Routes.manageNodes);
+ await nodesRobot.isDisplayed();
+
+ final initialNodeUri = nodesRobot.currentNodeUri();
+
+ expect(initialNodeUri, isNotEmpty, reason: "The wallet started with no node at all");
+
+ await nodesRobot.switchToAnotherNode();
+
+ expect(
+ nodesRobot.currentNodeUri(),
+ isNot(initialNodeUri),
+ reason: "The node was confirmed but the wallet is still pointed at $initialNodeUri",
+ );
+ });
+}
### integration_test/suites/tier1/swap_quote_test.dart
@@ -0,0 +1,37 @@
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+import "../../robots/new_swap_page_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Swap sheet fetches a provider quote, no trade is created", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+ final swapRobot = NewSwapPageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "swap_quote_test_app_key");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.openSwapSheet();
+ await swapRobot.isDisplayed();
+
+ await swapRobot.enterDepositAmount("1");
+
+ await swapRobot.confirmQuoteReceived();
+
+ await swapRobot.dismissModal();
+ await homePageRobot.isDisplayed();
+ });
+}
### integration_test/suites/tier1/sync_status_test.dart
@@ -0,0 +1,27 @@
+import "package:cw_core/wallet_type.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Restored wallet connects to a node and starts syncing", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "sync_status_test_app_key");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(WalletType.bitcoin);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.confirmSyncIndicatorShown();
+ });
+}
### integration_test/suites/tier1/transaction_details_test.dart
@@ -0,0 +1,47 @@
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Tapping a transaction opens the details of that transaction", (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "transaction_details_test_app_key");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ await homePageRobot.confirmTransactionHistoryVisible();
+
+ await homePageRobot.openAllTransactions();
+ await homePageRobot.confirmAllTransactionsVisible();
+
+ final tapped = homePageRobot.firstTransactionIdInAllView();
+
+ expect(tapped, isNotEmpty, reason: "The tile carries no transaction id to check against");
+
+ await homePageRobot.openFirstTransactionDetails();
+
+ // A list that hands the wrong transaction to the details screen shows one person's
+ // amounts under another's id, and nothing on the screen would say so.
+ expect(
+ homePageRobot.openedTransactionId(),
+ tapped,
+ reason: "The details opened for a different transaction than the one tapped",
+ );
+
+ homePageRobot.hasTransactionIdRow();
+ });
+}
### integration_test/suites/tier1/transaction_history_test.dart
@@ -0,0 +1,37 @@
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("Transaction history renders for a wallet with known transactions",
+ (tester) async {
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ await appLauncher.launchApp(testKey: "transaction_history_test_app_key");
+
+ await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ expect(getIt.get<AppStore>().wallet?.type, WalletType.solana);
+
+ await homePageRobot.confirmTransactionHistoryVisible();
+
+ await homePageRobot.openAllTransactions();
+
+ await homePageRobot.confirmAllTransactionsVisible();
+ });
+}
### integration_test/templates/example_feature_test.dart.example
@@ -0,0 +1,55 @@
+// Copy this file into integration_test/suites/tier0/ or tier1/, rename it to
+// <feature>_test.dart and replace the example steps with your journey.
+// The runner only picks up files ending in _test.dart, this template never runs.
+
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:integration_test/integration_test.dart";
+
+import "../../core/app_launcher.dart";
+import "../../flows/onboarding_flows.dart";
+import "../../robots/home_page_robot.dart";
+import "../../robots/new_dashboard_robot.dart";
+
+void main() {
+ // Required first line in every suite, integrationTest contains background app errors
+ // that testWidgets would turn into failures.
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ integrationTest("One sentence describing the journey under test", (tester) async {
+ // Robots and flows take the tester, create the ones your journey needs.
+ final appLauncher = AppLauncher(tester);
+ final onboardingFlows = OnboardingFlows(tester);
+ final dashboardRobot = NewDashboardRobot(tester);
+ final homePageRobot = HomePageRobot(tester);
+
+ // Boots the real app, the key names the suite in failure output.
+ await appLauncher.launchApp(testKey: "example_feature_test_app_key");
+
+ // Every suite starts from a fresh install, so get to a working wallet first.
+ // Use restoreFirstWalletFromSeed instead when your feature needs a known wallet.
+ await onboardingFlows.createFirstWallet(WalletType.solana);
+
+ await dashboardRobot.isDisplayed();
+ await homePageRobot.isDisplayed();
+
+ // Drive your feature through its robot from here. Robots own the finders and
+ // assertions for their screen, the suite only sequences them:
+ //
+ // await homePageRobot.openSettingsSheet();
+ // await mySettingsRobot.isDisplayed();
+ // await mySettingsRobot.doTheThing();
+ //
+ // Rules of thumb:
+ // - Wait with pumpUntilFound and pumpUntil, never with sleeps or pumpAndSettle.
+ // - Tap and read widgets by ValueKey, add keys to lib/ widgets when they are missing.
+ // - Assert app state through getIt (AppStore) or the page widget's view model.
+ // - A widget being in the tree does not mean it is on screen. Every dashboard tab
+ // stays mounted in an IndexedStack, so find.byType on a tab passes from any other
+ // tab. Check what is in front, the way WalletListPageRobot does.
+ // - A button being findable does not mean it is enabled. Tapping a disabled button
+ // does nothing at all and reads later as a missing widget somewhere else, so wait
+ // on whatever the screen itself gates on.
+ // - Add the finished suite to the table in docs/INTEGRATION_TESTS.md.
+ });
+}
### integration_test/test_suites/confirm_seeds_flow_test.dart
@@ -1,122 +0,0 @@
-import 'package:cake_wallet/wallet_types.g.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../components/common_test_constants.dart';
-import '../components/common_test_flows.dart';
-import '../robots/auth_page_robot.dart';
-import '../robots/dashboard_page_robot.dart';
-import '../robots/security_and_backup_page_robot.dart';
-import '../robots/wallet_keys_robot.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- AuthPageRobot authPageRobot;
- CommonTestFlows commonTestFlows;
- DashboardPageRobot dashboardPageRobot;
- WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot;
- SecurityAndBackupPageRobot securityAndBackupPageRobot;
-
- testWidgets(
- 'Confirm if the seeds display properly',
- (tester) async {
- // Store the original FlutterError.onError handler
- final originalOnError = FlutterError.onError;
-
- FlutterError.onError = (FlutterErrorDetails details) {
- debugPrint('FlutterError caught: ${details.exception}');
- };
-
- try {
- authPageRobot = AuthPageRobot(tester);
- commonTestFlows = CommonTestFlows(tester);
- dashboardPageRobot = DashboardPageRobot(tester);
- walletKeysAndSeedPageRobot = WalletKeysAndSeedPageRobot(tester);
- securityAndBackupPageRobot = SecurityAndBackupPageRobot(tester);
-
- await commonTestFlows.startAppFlow(
- ValueKey('confirm_creds_display_correctly_flow_app_key'),
- );
-
- await commonTestFlows.welcomePageToCreateNewWalletFlow(
- WalletType.solana,
- CommonTestConstants.pin,
- );
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
-
- await _confirmSeedsFlowForWalletType(
- WalletType.solana,
- authPageRobot,
- dashboardPageRobot,
- securityAndBackupPageRobot,
- walletKeysAndSeedPageRobot,
- tester,
- );
-
- // Do the same for other available wallet types
- for (var walletType in availableWalletTypes) {
- if (walletType == WalletType.solana) {
- continue;
- }
-
- await dashboardPageRobot.navigateToWalletsListPage(isDrawer: true);
-
- await commonTestFlows.createNewWalletFromWalletMenu(walletType);
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
-
- await _confirmSeedsFlowForWalletType(
- walletType,
- authPageRobot,
- dashboardPageRobot,
- securityAndBackupPageRobot,
- walletKeysAndSeedPageRobot,
- tester,
- );
- }
-
- await Future.delayed(Duration(seconds: 15));
- } finally {
- FlutterError.onError = originalOnError;
- }
- },
- );
-}
-
-Future<void> _confirmSeedsFlowForWalletType(
- WalletType walletType,
- AuthPageRobot authPageRobot,
- DashboardPageRobot dashboardPageRobot,
- SecurityAndBackupPageRobot securityAndBackupPageRobot,
- WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot,
- WidgetTester tester,
-) async {
- await dashboardPageRobot.openDrawerMenu();
- await dashboardPageRobot.dashboardMenuWidgetRobot.navigateToSecurityAndBackupPage();
-
- await securityAndBackupPageRobot.navigateToShowKeysPage();
-
- final onAuthPage = authPageRobot.onAuthPage();
- if (onAuthPage) {
- await authPageRobot.enterPinCode(CommonTestConstants.pin);
- }
-
- final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
- if (onAuthPageDesktop) {
- await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
- }
- await tester.pumpAndSettle();
-
- await walletKeysAndSeedPageRobot.isWalletKeysAndSeedPage();
- walletKeysAndSeedPageRobot.hasTitle();
- walletKeysAndSeedPageRobot.hasShareWarning();
-
- await walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType);
-
- await walletKeysAndSeedPageRobot.backToDashboard();
-}
### integration_test/test_suites/create_wallet_flow_test.dart
@@ -1,60 +0,0 @@
-import 'package:cake_wallet/wallet_types.g.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../components/common_test_constants.dart';
-import '../components/common_test_flows.dart';
-import '../robots/dashboard_page_robot.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- CommonTestFlows commonTestFlows;
- DashboardPageRobot dashboardPageRobot;
-
- testWidgets(
- 'Create Wallet Flow',
- (tester) async {
- FlutterError.onError = (FlutterErrorDetails details) {
- debugPrint('FlutterError caught: ${details.exception}');
- };
- commonTestFlows = CommonTestFlows(tester);
- dashboardPageRobot = DashboardPageRobot(tester);
-
- // Start the app
- await commonTestFlows.startAppFlow(
- ValueKey('create_wallets_through_seeds_test_app_key'),
- );
-
- await commonTestFlows.welcomePageToCreateNewWalletFlow(
- WalletType.solana,
- CommonTestConstants.pin,
- );
-
- // Confirm it actually restores a solana wallet
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
-
- // Do the same for other available wallet types
- for (var walletType in availableWalletTypes) {
- if (walletType == WalletType.solana) {
- continue;
- }
-
- await dashboardPageRobot.navigateToWalletsListPage();
-
- await commonTestFlows.createNewWalletFromWalletMenu(walletType);
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
- }
-
- // Goes to the wallet menu and provides a confirmation that all the wallets were correctly restored
- await dashboardPageRobot.navigateToWalletsListPage();
-
- commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly();
-
- await Future.delayed(Duration(seconds: 5));
- },
- );
-}
### integration_test/test_suites/exchange_flow_test.dart
@@ -1,65 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../components/common_test_constants.dart';
-import '../components/common_test_flows.dart';
-import '../robots/dashboard_page_robot.dart';
-import '../robots/exchange_confirm_page_robot.dart';
-import '../robots/exchange_page_robot.dart';
-import '../robots/exchange_trade_external_send_page_robot.dart';
-import '../robots/exchange_trade_page_robot.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- CommonTestFlows commonTestFlows;
- ExchangePageRobot exchangePageRobot;
- DashboardPageRobot dashboardPageRobot;
- ExchangeTradePageRobot exchangeTradePageRobot;
- ExchangeConfirmPageRobot exchangeConfirmPageRobot;
- ExchangeTradeExternalSendPageRobot exchangeTradeExternalSendPageRobot;
-
- testWidgets('Exchange flow', (tester) async {
- FlutterError.onError = (FlutterErrorDetails details) {
- debugPrint('FlutterError caught: ${details.exception}');
- };
-
- commonTestFlows = CommonTestFlows(tester);
- exchangePageRobot = ExchangePageRobot(tester);
- dashboardPageRobot = DashboardPageRobot(tester);
- exchangeTradePageRobot = ExchangeTradePageRobot(tester);
- exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
- exchangeTradeExternalSendPageRobot = ExchangeTradeExternalSendPageRobot(tester);
-
- await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key'));
- await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
- CommonTestConstants.testWalletType,
- secrets.solanaTestWalletSeeds,
- CommonTestConstants.pin,
- );
- await dashboardPageRobot.navigateToExchangePage();
-
- // ----------- Exchange Page -------------
- await exchangePageRobot.selectDepositCurrency(CommonTestConstants.exchangeTestDepositCurrency);
- await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.exchangeTestReceiveCurrency);
-
- await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
- await exchangePageRobot.enterDepositRefundAddress(
- depositAddress: CommonTestConstants.testWalletAddress,
- );
- await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
- await exchangePageRobot.onExchangeButtonPressed();
- await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
-
- await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
-
- await exchangeTradePageRobot.onGotItButtonPressed();
- await exchangeTradePageRobot.onSendFromExternalButtonPressed();
-
- await exchangeTradeExternalSendPageRobot.isExchangeTradeExternalSendPage();
- await exchangeTradeExternalSendPageRobot.verifySendDetailsItemsDisplayProperly();
- await exchangeTradeExternalSendPageRobot.onContinueButtonPressed();
- });
-}
### integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart
@@ -1,75 +0,0 @@
-import 'package:cake_wallet/wallet_types.g.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../components/common_test_constants.dart';
-import '../components/common_test_flows.dart';
-import '../robots/dashboard_page_robot.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- CommonTestFlows commonTestFlows;
- DashboardPageRobot dashboardPageRobot;
-
- testWidgets(
- 'Restoring Wallets Through Seeds',
- (tester) async {
- FlutterError.onError = (FlutterErrorDetails details) {
- debugPrint('FlutterError caught: ${details.exception}');
- };
-
- commonTestFlows = CommonTestFlows(tester);
- dashboardPageRobot = DashboardPageRobot(tester);
-
- // Start the app
- await commonTestFlows.startAppFlow(
- ValueKey('restore_wallets_through_seeds_test_app_key'),
- );
-
- // Restore the first wallet type: Solana
- await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
- WalletType.solana,
- secrets.solanaTestWalletSeeds,
- CommonTestConstants.pin,
- );
-
- // Confirm it actually restores a solana wallet
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
-
- // Do the same for other available wallet types
- for (var walletType in availableWalletTypes) {
- if (walletType == WalletType.solana) {
- continue;
- }
- final seed = commonTestFlows.getWalletSeedsByWalletType(walletType);
- if (seed.isEmpty) {
- tester.printToConsole("----------------------------");
- tester.printToConsole("- Skipped wallet: ${walletType}");
- tester.printToConsole("- Make sure to add seed to secrets");
- tester.printToConsole("----------------------------");
- continue;
- }
-
- await dashboardPageRobot.navigateToWalletsListPage();
-
- await commonTestFlows.restoreWalletFromWalletMenu(
- walletType,
- seed,
- );
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
- }
-
- // Goes to the wallet menu and provides a visual confirmation that all the wallets were correctly restored
- await dashboardPageRobot.navigateToWalletsListPage();
-
- commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly();
-
- await Future.delayed(Duration(seconds: 5));
- },
- );
-}
### integration_test/test_suites/send_flow_test.dart
@@ -1,133 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:cw_core/wallet_type.dart';
-
-import '../components/common_test_constants.dart';
-import '../components/common_test_flows.dart';
-import '../components/common_test_cases.dart';
-import '../robots/dashboard_page_robot.dart';
-import '../robots/send_page_robot.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- SendPageRobot sendPageRobot;
- CommonTestFlows commonTestFlows;
- DashboardPageRobot dashboardPageRobot;
- CommonTestCases commonTestCases;
-
- testWidgets('Send flow with wallet switching for insufficient balance - testing \$1 send',
- (tester) async {
- FlutterError.onError = (FlutterErrorDetails details) {
- debugPrint('FlutterError caught: ${details.exception}');
- };
-
- commonTestFlows = CommonTestFlows(tester);
- sendPageRobot = SendPageRobot(tester: tester);
- dashboardPageRobot = DashboardPageRobot(tester);
- commonTestCases = CommonTestCases(tester);
-
- await commonTestFlows.startAppFlow(ValueKey('send_test_app_key'));
-
- // Various wallet options we want to use to test the send flow
- final walletConfigs = [
- {
- 'type': WalletType.solana,
- 'seed': secrets.solanaTestWalletSeeds2,
- 'name': 'Solana Wallet 1',
- },
- {
- 'type': WalletType.solana,
- 'seed': secrets.solanaTestWalletSeeds,
- 'name': 'Solana Wallet 2',
- },
- {
- 'type': WalletType.ethereum,
- 'seed': secrets.ethereumTestWalletSeeds,
- 'name': 'Ethereum Wallet',
- },
- ];
-
- bool hasSufficientBalance = false;
- WalletType? successfulWalletType;
-
- // We try each wallet configuration until we find one with sufficient balance
- for (int i = 0; i < walletConfigs.length; i++) {
- final config = walletConfigs[i];
-
- if ((config['seed'] as String).isEmpty) {
- tester.printToConsole('Skipping ${config['name']}, seed is empty');
- continue;
- }
-
- tester.printToConsole('Trying ${config['name']}');
-
- // For first wallet, we restore wallet afresh, for subsequent ones, we do a wallet switch
- if (i == 0) {
- await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
- config['type'] as WalletType,
- config['seed'] as String,
- CommonTestConstants.pin,
- );
- } else {
- await commonTestCases.goBack();
- await commonTestCases.defaultSleepTime();
-
- await dashboardPageRobot.navigateToWalletsListPage();
- await commonTestCases.defaultSleepTime();
-
- await commonTestFlows.restoreWalletFromWalletMenu(
- config['type'] as WalletType,
- config['seed'] as String,
- );
- }
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(config['type'] as WalletType);
-
- // Navigate to send page
- await dashboardPageRobot.navigateToSendPage();
- await sendPageRobot.checkIfSendPageIsVisible();
-
- await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
- await sendPageRobot.selectReceiveCurrency(CommonTestConstants.sendTestReceiveCurrency);
-
- await sendPageRobot.selectTransactionPriority();
-
- // Main check to see if this wallet has sufficient balance for $1 send
- final hasBalance = await sendPageRobot.validateWalletBalanceForOneDollarSend();
- if (hasBalance) {
- hasSufficientBalance = true;
- successfulWalletType = config['type'] as WalletType;
- tester.printToConsole(
- '${config['name']} has sufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send, proceeding with send',
- );
- break;
- } else {
- tester.printToConsole(
- '${config['name']} has insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
- );
- if (i < walletConfigs.length - 1) {
- tester.printToConsole('Switching to next wallet...');
- }
- }
- }
-
- // We only proceed with the send if we have sufficient balance
- if (hasSufficientBalance && successfulWalletType != null) {
- tester.printToConsole('Performing send transaction with $successfulWalletType wallet');
-
- await sendPageRobot.testFiatAmountEntry();
- await sendPageRobot.testCryptoAmountEntry();
-
- await sendPageRobot.onSendButtonPressed();
- await sendPageRobot.onSendSliderOnConfirmSendingBottomSheetDragged();
- await sendPageRobot.handleTransactionSuccessFlow();
- } else {
- tester.printToConsole(
- 'Test completed without sending - all available wallets have insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
- );
- }
- });
-}
### integration_test/test_suites/transaction_history_flow_test.dart
@@ -1,82 +0,0 @@
-import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../components/common_test_constants.dart';
-import '../components/common_test_flows.dart';
-import '../robots/dashboard_page_robot.dart';
-import '../robots/transactions_page_robot.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- CommonTestFlows commonTestFlows;
- DashboardPageRobot dashboardPageRobot;
- TransactionsPageRobot transactionsPageRobot;
-
- /// Two Test Scenarios
- /// - Fully Synchronizes and display the transaction history either immediately or few seconds after fully synchronizing
- /// - Displays the transaction history progressively as synchronizing happens
- testWidgets('Transaction history flow', (tester) async {
- FlutterError.onError = (FlutterErrorDetails details) {
- debugPrint('FlutterError caught: ${details.exception}');
- };
-
- commonTestFlows = CommonTestFlows(tester);
- dashboardPageRobot = DashboardPageRobot(tester);
- transactionsPageRobot = TransactionsPageRobot(tester);
-
- await commonTestFlows.startAppFlow(
- ValueKey('confirm_creds_display_correctly_flow_app_key'),
- );
-
- /// Test Scenario 1 - Displays transaction history list while synchronizing.
- ///
- /// For Solana/Tron WalletTypes.
- await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
- WalletType.solana,
- secrets.solanaTestWalletSeeds,
- CommonTestConstants.pin,
- );
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
-
- await dashboardPageRobot.swipeDashboardTab(true);
-
- await transactionsPageRobot.isTransactionsPage();
-
- await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly();
-
- // Wait time for the first scenario to ensure proper loading
- await tester.pump(Duration(seconds: 3));
- await tester.pumpAndSettle();
-
- await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(true);
-
- /// Test Scenario 2 - Displays transaction history list after fully synchronizing.
- ///
- /// For Bitcoin/Monero/Wownero WalletTypes.
- await dashboardPageRobot.navigateToWalletsListPage();
-
- await commonTestFlows.restoreWalletFromWalletMenu(
- WalletType.bitcoin,
- secrets.bitcoinTestWalletSeeds,
- );
-
- await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.bitcoin);
-
- await dashboardPageRobot.swipeDashboardTab(true);
-
- await transactionsPageRobot.isTransactionsPage();
-
- await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly();
-
- // Wait time for the second scenario to ensure proper loading
- await tester.pump(Duration(seconds: 3));
- await tester.pumpAndSettle();
-
- await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(false);
- });
-}
### integration_test_runner.sh
@@ -1,38 +1,65 @@
#!/bin/bash
-# Exit on any error
set -euo pipefail
-# Configurations
-export DESKTOP_FORCE_MOBILE="Y"
-MAX_INACTIVITY=${MAX_INACTIVITY:-180}
-MAX_PARALLEL_TESTS=${MAX_PARALLEL_TESTS:-2}
-TEST_TIMEOUT=${TEST_TIMEOUT:-600}
+# Runner params/knobs, all overridable from the environment:
+# SUITE_DIR directory to collect *_test.dart suites from, or a single suite file
+# (default integration_test/suites)
+# TEST_TIER tier0, tier1 or all, narrows SUITE_DIR to a tier subdirectory
+# PLATFORM android, linux, macos or auto, picks the between-suite data reset
+# ANDROID_APP_ID package cleared between suites on android (default read from android/app.properties)
+# PREBUILT_APK prebuilt apk passed to flutter drive, its dart defines are already baked in
+# EXTRA_DART_DEFINES semicolon separated KEY=VALUE pairs
+# FLUTTER_DEVICE device id passed to flutter drive, needed when several devices are attached
+# TEST_TIMEOUT per-attempt timeout in seconds (default 900)
+# RETRY_COUNT retries per suite after a failure (default 1)
+# MAX_VOID_ATTEMPTS extra attempts when the driver never attached (default 2)
+# MAX_TOTAL_VOID_ATTEMPTS give up on the whole run after this many, the environment is broken (default 4)
+# REMOVE_DATA_DIRECTORY set to N to keep app data between suites (default wipes it)
+# REMOVE_DESKTOP_DATA set to Y to let a desktop run wipe the wallet directories below,
+# they belong to the installed wallet so this is off by default
+# SUMMARY_FILE path to write a key=value summary of the run to, for reporting
+# VOID_GRACE seconds to let a driver that cannot attach recover before the
+# attempt is ended early (default 60)
+
+SUITE_DIR=${SUITE_DIR:-integration_test/suites}
+TEST_TIER=${TEST_TIER:-all}
+PLATFORM=${PLATFORM:-auto}
+TEST_TIMEOUT=${TEST_TIMEOUT:-900}
RETRY_COUNT=${RETRY_COUNT:-1}
+MAX_VOID_ATTEMPTS=${MAX_VOID_ATTEMPTS:-2}
+MAX_TOTAL_VOID_ATTEMPTS=${MAX_TOTAL_VOID_ATTEMPTS:-4}
+REMOVE_DATA_DIRECTORY=${REMOVE_DATA_DIRECTORY:-Y}
+REMOVE_DESKTOP_DATA=${REMOVE_DESKTOP_DATA:-N}
+EXTRA_DART_DEFINES=${EXTRA_DART_DEFINES:-}
+SUMMARY_FILE=${SUMMARY_FILE:-}
+VOID_GRACE=${VOID_GRACE:-60}
+
+# Using this to capture as many of these annoying flutter drive errors as possible,
+# so we can handle them, and the test doesn't just hang forever
+VOID_MARKERS="unusually long time to connect to the VM|taking unusually long time to initialize|Service has disappeared|Flutter Driver extension is taking a long time to become available"
+PREBUILT_APK=${PREBUILT_APK:-}
-# Data directories to clean
+# Desktop data directories, shared with the installed wallet, see REMOVE_DESKTOP_DATA
DATA_DIRS=(
"$HOME/.local/share/com.example.cake_wallet"
"$HOME/Documents/cake_wallet"
"$HOME/.config/cake_wallet"
)
-# Global state
-declare -a targets
-declare -a passed_tests
-declare -a failed_tests
-declare -a test_durations
-declare -a test_start_times
+targets=()
+passed_tests=()
+failed_tests=()
+funds_results=()
+total_void_attempts=0
-# Signal handling
cleanup() {
- echo "🛑 Received interrupt signal, cleaning up..."
+ echo "Received interrupt signal, cleaning up..."
exit 1
}
trap cleanup SIGINT SIGTERM
-# Utility functions
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
@@ -41,7 +68,6 @@ error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2
}
-# Format seconds into hours, minutes, seconds
format_duration() {
local seconds=$1
local hours=$((seconds / 3600))
@@ -57,128 +83,383 @@ format_duration() {
fi
}
+resolve_platform() {
+ if [[ "$PLATFORM" != "auto" ]]; then
+ return
+ fi
+
+ if command -v adb > /dev/null 2>&1 && adb devices 2>/dev/null | grep -q "device$"; then
+ PLATFORM="android"
+ elif [[ "$(uname -s)" == "Darwin" ]]; then
+ PLATFORM="macos"
+ else
+ PLATFORM="linux"
+ fi
+
+ log "Resolved platform: $PLATFORM"
+}
+
+resolve_android_app_id() {
+ if [[ -n "${ANDROID_APP_ID:-}" ]]; then
+ return
+ fi
+
+ if [[ -f "android/app.properties" ]]; then
+ ANDROID_APP_ID=$(grep '^id=' android/app.properties | cut -d'=' -f2)
+ fi
+
+ ANDROID_APP_ID=${ANDROID_APP_ID:-com.cakewallet.cake_wallet}
+}
+
+restart_adb() {
+ if [[ "$PLATFORM" != "android" ]]; then
+ return
+ fi
+
+ bounded 30 adb kill-server
+ bounded 30 adb start-server
+ bounded 60 adb wait-for-device
+
+ if ! bounded 30 adb devices | grep -q "device$"; then
+ error "No device is attached after restarting adb"
+ return 1
+ fi
+}
+
+bounded() {
+ local limit="$1"
+ shift
+
+ if command -v timeout > /dev/null 2>&1; then
+ timeout "$limit" "$@" 2>/dev/null || return 1
+ elif command -v perl > /dev/null 2>&1; then
+ perl -e 'alarm shift; exec @ARGV' "$limit" "$@" 2>/dev/null || return 1
+ else
+ "$@" 2>/dev/null || return 1
+ fi
+}
+
+capture_failure_logcat() {
+ local test_name="$1"
+
+ if [[ "$PLATFORM" != "android" ]]; then
+ return
+ fi
+
+ echo "===== logcat after failed attempt: $test_name ====="
+ bounded 60 adb logcat -d -t 400 || echo "logcat unavailable"
+ echo "===== end logcat ====="
+}
+
clean_data_directories() {
- if [[ "${REMOVE_DATA_DIRECTORY:-}" == "Y" ]]; then
- log "Cleaning data directories..."
- for dir in "${DATA_DIRS[@]}"; do
- if [[ -d "$dir" ]]; then
- rm -rf "$dir" || error "Failed to remove $dir"
- fi
+ if [[ "$REMOVE_DATA_DIRECTORY" != "Y" ]]; then
+ return
+ fi
+
+ if [[ "$PLATFORM" == "android" ]]; then
+ if ! command -v adb > /dev/null 2>&1; then
+ error "adb is not on PATH, every suite would run on whatever data is already installed"
+ exit 1
+ fi
+
+ if [[ "$ANDROID_APP_ID" != *".test_"* ]]; then
+ error "Refusing to clear $ANDROID_APP_ID, it is not a test build. Rename the app first: printf 'id=com.cakewallet.test_local\\nname=local\\n' > android/app.properties"
+ exit 1
+ fi
+
+ log "Cleaning app data..."
+ adb shell pm clear "$ANDROID_APP_ID" > /dev/null 2>&1 || log "pm clear skipped, $ANDROID_APP_ID not installed yet"
+ return
+ fi
+
+ if [[ "$REMOVE_DESKTOP_DATA" != "Y" ]]; then
+ log "Keeping the desktop wallet data, set REMOVE_DESKTOP_DATA=Y to wipe it between suites"
+ return
+ fi
+
+ log "Cleaning app data..."
+
+ for dir in "${DATA_DIRS[@]}"; do
+ if [[ -d "$dir" ]]; then
+ rm -rf "$dir" || error "Failed to remove $dir"
+ fi
+ done
+}
+
+build_drive_command() {
+ drive_command=(flutter drive --driver=test_driver/integration_test.dart --dart-define=CI_BUILD=true)
+
+ if [[ -n "${FLUTTER_DEVICE:-}" ]]; then
+ drive_command+=("-d" "$FLUTTER_DEVICE")
+ fi
+
+ if [[ -n "$PREBUILT_APK" ]]; then
+ drive_command+=("--use-application-binary=$PREBUILT_APK")
+ fi
+
+ if [[ -n "$EXTRA_DART_DEFINES" ]]; then
+ IFS=';' read -ra defines <<< "$EXTRA_DART_DEFINES"
+ for define in "${defines[@]}"; do
+ drive_command+=("--dart-define=$define")
done
fi
+
+ if command -v timeout > /dev/null 2>&1; then
+ drive_command=(timeout "$TEST_TIMEOUT" "${drive_command[@]}")
+ elif command -v perl > /dev/null 2>&1; then
+ drive_command=(perl -e 'alarm shift; exec @ARGV' "$TEST_TIMEOUT" "${drive_command[@]}")
+ fi
+}
+
+end_process_tree() {
+ local root="$1" signal="$2"
+ local child
+
+ for child in $(pgrep -P "$root" 2>/dev/null); do
+ end_process_tree "$child" "$signal"
+ done
+
+ kill "-$signal" "$root" 2>/dev/null || true
+}
+
+watch_for_dead_driver() {
+ local log_file="$1" drive_pid="$2"
+ local marker_seen=0
+
+ while kill -0 "$drive_pid" 2>/dev/null; do
+ if (( marker_seen == 0 )) && grep -qE "$VOID_MARKERS" "$log_file" 2>/dev/null; then
+ marker_seen=$(date +%s)
+ log "Driver is struggling to attach, giving it ${VOID_GRACE}s to recover"
+ fi
+
+ if (( marker_seen > 0 )) && (( $(date +%s) - marker_seen >= VOID_GRACE )); then
+ log "Driver never attached within ${VOID_GRACE}s, ending this attempt early"
+
+ end_process_tree "$drive_pid" TERM
+ sleep 5
+ end_process_tree "$drive_pid" KILL
+
+ return 0
+ fi
+
+ sleep 5
+ done
+}
+
+collect_funds_results() {
+ local log_file="$1"
+ local line
+
+ while IFS= read -r line; do
+ funds_results+=("$line")
+ done < <(sed -n 's/.*FUNDS_RESULT|//p' "$log_file" | sort -u)
}
-# Run a single test with retry logic
run_test() {
local test_file="$1"
local test_name=$(basename "$test_file" .dart)
local retry_count=0
+ local void_attempts=0
while (( retry_count <= RETRY_COUNT )); do
log "Running test: $test_name (attempt $((retry_count + 1)))"
- # Clean data directories before each attempt
clean_data_directories
- # Record start time
local start_time=$(date +%s)
- test_start_times+=("$start_time")
-
- # Run the test in foreground
- if flutter drive \
- --driver=test_driver/integration_test.dart \
- --target="$test_file" \
- --dart-define=CI_BUILD=true; then
- # Calculate duration
- local end_time=$(date +%s)
- local duration=$((end_time - start_time))
- test_durations+=("$duration")
-
- log "✅ Test passed: $test_name ($(format_duration $duration))"
- passed_tests+=("$test_name")
+ local attempt_log
+ attempt_log=$(mktemp)
+
+ "${drive_command[@]}" --target="$test_file" > >(tee "$attempt_log") 2>&1 &
+ local drive_pid=$!
+
+ watch_for_dead_driver "$attempt_log" "$drive_pid" &
+ local watchdog_pid=$!
+ disown "$watchdog_pid" 2>/dev/null || true
+
+ local attempt_status=0
+ wait "$drive_pid" 2>/dev/null || attempt_status=$?
+
+ kill "$watchdog_pid" 2>/dev/null || true
+
+ if (( attempt_status == 0 )) && ! grep -q "All tests passed." "$attempt_log"; then
+ attempt_status=1
+ fi
+
+ if (( attempt_status == 0 )); then
+ local duration=$(( $(date +%s) - start_time ))
+
+ log "PASS: $test_name ($(format_duration $duration))"
+ passed_tests+=("$test_name|$duration")
+ collect_funds_results "$attempt_log"
+ rm -f "$attempt_log"
return 0
else
- # Calculate duration even for failed tests
- local end_time=$(date +%s)
- local duration=$((end_time - start_time))
- test_durations+=("$duration")
+ local duration=$(( $(date +%s) - start_time ))
+
+ local attempt_was_void=N
+ if grep -qE "$VOID_MARKERS" "$attempt_log"; then
+ attempt_was_void=Y
+ fi
+
+ collect_funds_results "$attempt_log"
+
+ rm -f "$attempt_log"
+
+ log "FAIL: $test_name ($(format_duration $duration))"
- log "❌ Test failed: $test_name ($(format_duration $duration))"
+ capture_failure_logcat "$test_name"
- if (( retry_count < RETRY_COUNT )); then
+ if [[ "$attempt_was_void" == "Y" ]]; then
+ total_void_attempts=$((total_void_attempts + 1))
+
+ if (( total_void_attempts > MAX_TOTAL_VOID_ATTEMPTS )); then
+ error "The flutter driver failed to attach $total_void_attempts times, the emulator or adb is broken"
+ failed_tests+=("$test_name|$duration|void")
+ return 1
+ fi
+ fi
+
+ if [[ "$attempt_was_void" == "Y" ]] && (( void_attempts < MAX_VOID_ATTEMPTS )); then
+ log "Driver never attached to the app, retrying without spending a retry"
+ void_attempts=$((void_attempts + 1))
+
+ if ! restart_adb; then
+ error "Could not bring adb back, aborting this suite"
+ failed_tests+=("$test_name|$duration|void")
+ return 1
+ fi
+
+ sleep 5
+ elif (( retry_count < RETRY_COUNT )); then
log "Retrying test: $test_name"
retry_count=$((retry_count + 1))
- sleep 5 # Brief pause before retry
+ sleep 5
else
- failed_tests+=("$test_name")
+ local fail_reason="test"
+ [[ "$attempt_was_void" == "Y" ]] && fail_reason="void"
+
+ failed_tests+=("$test_name|$duration|$fail_reason")
return 1
fi
fi
done
}
-# Main execution flow
+write_github_summary() {
+ if [[ -z "${GITHUB_STEP_SUMMARY:-}" ]]; then
+ return
+ fi
+
+ {
+ echo "### Integration tests ($SUITE_DIR, tier: $TEST_TIER)"
+ echo ""
+ echo "| Suite | Result | Duration |"
+ echo "|---|---|---|"
+ for entry in "${passed_tests[@]+"${passed_tests[@]}"}"; do
+ echo "| ${entry%%|*} | pass | $(format_duration "${entry##*|}") |"
+ done
+ for entry in "${failed_tests[@]+"${failed_tests[@]}"}"; do
+ local rest="${entry#*|}"
+ echo "| ${entry%%|*} | ${rest#*|} | $(format_duration "${rest%%|*}") |"
+ done
+ } >> "$GITHUB_STEP_SUMMARY"
+}
+
+write_summary_file() {
+ if [[ -z "$SUMMARY_FILE" ]]; then
+ return
+ fi
+
+ {
+ echo "tier=$TEST_TIER"
+ echo "total=${#targets[@]}"
+ echo "passed=${#passed_tests[@]}"
+ echo "failed=${#failed_tests[@]}"
+ echo "duration=$1"
+ for entry in "${passed_tests[@]+"${passed_tests[@]}"}"; do
+ echo "pass=$entry"
+ done
+ for entry in "${failed_tests[@]+"${failed_tests[@]}"}"; do
+ echo "fail=$entry"
+ done
+ for entry in "${funds_results[@]+"${funds_results[@]}"}"; do
+ echo "chain=$entry"
+ done
+ } > "$SUMMARY_FILE"
+}
+
main() {
log "Starting integration test runner"
- log "Configuration: RETRY_COUNT=${RETRY_COUNT}"
- # Collect all Dart test files from test_suites directory
- while IFS= read -r -d $'\0' file; do
- targets+=("$file")
- done < <(find integration_test/test_suites -name "*.dart" -type f -print0)
+ local search_dir="$SUITE_DIR"
+ if [[ "$TEST_TIER" != "all" ]]; then
+ search_dir="$SUITE_DIR/$TEST_TIER"
+ fi
- if [[ $? -ne 0 ]]; then
- error "Failed to find test files"
+ if [[ -f "$SUITE_DIR" ]]; then
+ search_dir="$SUITE_DIR"
+ elif [[ ! -d "$search_dir" ]]; then
+ error "Suite directory not found: $search_dir"
exit 1
fi
+ resolve_platform
+ resolve_android_app_id
+ build_drive_command
+
+ log "Configuration: SUITE_DIR=$search_dir PLATFORM=$PLATFORM RETRY_COUNT=$RETRY_COUNT"
+
+ if [[ -f "$search_dir" ]]; then
+ targets+=("$search_dir")
+ else
+ while IFS= read -r -d $'\0' file; do
+ targets+=("$file")
+ done < <(find "$search_dir" -name "*_test.dart" -type f -print0 | sort -z)
+ fi
+
if (( ${#targets[@]} == 0 )); then
- error "No test files found in integration_test/test_suites directory"
+ error "No test files found in $search_dir"
exit 1
fi
log "Found ${#targets[@]} test files"
- # Record overall start time
local overall_start_time=$(date +%s)
- # Run tests sequentially
for target in "${targets[@]}"; do
- run_test "$target"
+ run_test "$target" || true
done
- # Calculate total duration
- local overall_end_time=$(date +%s)
- local total_duration=$((overall_end_time - overall_start_time))
+ local total_duration=$(( $(date +%s) - overall_start_time ))
- # Generate summary
echo -e "\n===== Test Summary ====="
echo "Total tests: ${#targets[@]}"
echo "Passed: ${#passed_tests[@]}"
echo "Failed: ${#failed_tests[@]}"
echo "Total duration: $(format_duration $total_duration)"
if (( ${#passed_tests[@]} > 0 )); then
- echo -e "\n✅ Passed Tests:"
- for i in $(seq 0 $((${#passed_tests[@]} - 1))); do
- local test_name="${passed_tests[$i]}"
- local duration="${test_durations[$i]}"
- echo " - $test_name ($(format_duration $duration))"
+ echo -e "\nPassed Tests:"
+ for entry in "${passed_tests[@]}"; do
+ echo " - ${entry%%|*} ($(format_duration "${entry##*|}"))"
done
fi
+ write_github_summary
+ write_summary_file "$total_duration"
+
if (( ${#failed_tests[@]} > 0 )); then
- echo -e "\n❌ Failed Tests:"
- for i in $(seq 0 $((${#failed_tests[@]} - 1))); do
- local test_name="${failed_tests[$i]}"
- local duration="${test_durations[$i]}"
- echo " - $test_name ($(format_duration $duration))"
+ echo -e "\nFailed Tests:"
+ local rest
+ for entry in "${failed_tests[@]}"; do
+ rest="${entry#*|}"
+ echo " - ${entry%%|*} ($(format_duration "${rest%%|*}"), ${rest#*|})"
done
exit 1
- else
- echo -e "\n🎉 All tests passed successfully!"
fi
+
+ echo -e "\nAll tests passed successfully!"
}
-# Run main function
main "$@"
### lib/new-ui/new_dashboard.dart
@@ -71,6 +71,7 @@ class _NewDashboardState extends State<NewDashboard> {
bottomSheetService: widget.bottomSheetService,
child: CupertinoScaffold(
body: Material(
+ key: ValueKey("new_dashboard_page_key"),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
### lib/new-ui/pages/lightning_username_page.dart
@@ -112,6 +112,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
),
SizedBox(),
NewPrimaryButton(
+ key: ValueKey("lightning_username_page_skip_button_key"),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
### lib/new-ui/pages/send_page.dart
@@ -464,6 +464,7 @@ class _NewSendPageState extends State<NewSendPage> {
child: Text(S.of(context).address_or_alias),
),
NewSendAddressInput(
+ key: ValueKey("send_page_address_input_key"),
displayName: output.displayName,
validator: output.isParsedAddress
? widget.sendViewModel.textValidator
@@ -534,6 +535,7 @@ class _NewSendPageState extends State<NewSendPage> {
// test/new-ui/widgets/send_page/send_amount_input_test.dart.
Text(S.of(context).amount),
NewSendAmountInput(
+ key: ValueKey("send_page_amount_input_key"),
validator: output.sendAll
? widget.sendViewModel.allAmountValidator
: widget.sendViewModel.amountValidator(output),
### lib/new-ui/pages/settings_page.dart
@@ -146,7 +146,7 @@ class SettingsMainPage extends StatelessWidget {
List<ListItem> buildItems(SettingsSectionData section) => section.items
.map((item) => item.condition(dashboardViewModel)
? ListItemRegularRow(
- keyValue: item.title,
+ keyValue: item.route,
label: item.title,
iconPath: item.iconPath,
onTap: () {
### lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
@@ -44,6 +44,7 @@ class CoinActionRow extends StatelessWidget {
spacing: MediaQuery.of(context).size.width * 0.05,
children: [
CoinActionButton(
+ key: ValueKey("home_page_send_button_key"),
icon: CakeImageWidget(
imageUrl: "assets/new-ui/send.svg",
colorFilter: ColorFilter.mode(
@@ -76,6 +77,7 @@ class CoinActionRow extends StatelessWidget {
},
),
CoinActionButton(
+ key: ValueKey("home_page_receive_button_key"),
icon: CakeImageWidget(
imageUrl: "assets/new-ui/receive.svg",
colorFilter: ColorFilter.mode(
@@ -97,6 +99,7 @@ class CoinActionRow extends StatelessWidget {
),
if (showSwap)
CoinActionButton(
+ key: ValueKey("home_page_swap_button_key"),
icon: CakeImageWidget(
imageUrl: "assets/new-ui/exchange.svg",
colorFilter: ColorFilter.mode(
@@ -120,6 +123,7 @@ class CoinActionRow extends StatelessWidget {
},
),
CoinActionButton(
+ key: ValueKey("home_page_scan_button_key"),
icon: CakeImageWidget(
imageUrl: "assets/new-ui/scan.svg",
colorFilter: ColorFilter.mode(
### lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart
@@ -67,6 +67,7 @@ class AssetsTopBar extends StatelessWidget {
child: Semantics(
button: true,
child: GestureDetector(
+ key: const ValueKey("assets_history_action_button_key"),
onTap: actionButton.onPressed,
child: Container(
height: 40,
### lib/new-ui/widgets/coins_page/assets_history/history_modal.dart
@@ -18,6 +18,7 @@ class HistoryModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
+ key: const ValueKey("history_modal_key"),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
### lib/new-ui/widgets/coins_page/assets_history/history_section.dart
@@ -116,6 +116,7 @@ class HistorySection extends StatelessWidget {
}
},
child: HistoryTile(
+ key: ValueKey("home_page_transaction_${transaction.id}_key"),
title: item.formattedTitle + transactionType,
date: _formatTransactionDate(item.date, localeName),
amount: item.formattedCryptoAmount,
### lib/new-ui/widgets/coins_page/assets_history/history_top_bar.dart
@@ -17,6 +17,7 @@ class HistoryTopBar extends StatelessWidget {
onTap: onTap,
child: ExcludeSemantics(
child: GestureDetector(
+ key: const ValueKey("history_top_bar_key"),
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
### lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart
@@ -153,7 +153,11 @@ class SyncBar extends StatelessWidget {
width: 20,
height: 20,
),
- if (_showDot()) const CupertinoActivityIndicator(radius: 8),
+ if (_showDot())
+ CupertinoActivityIndicator(
+ key: ValueKey(dashboardViewModel.status.runtimeType),
+ radius: 8,
+ ),
if (_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
);
### lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
@@ -165,6 +165,7 @@ class _TopBarState extends State<TopBar> {
),
),
ModernButton.svg(
+ key: const ValueKey("home_page_settings_button_key"),
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
### lib/new-ui/widgets/coins_page/wallet_info.dart
@@ -49,6 +49,7 @@ class WalletInfoBar extends StatelessWidget {
Expanded(
child: Text(
name,
+ key: const ValueKey("home_page_wallet_name_text_key"),
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
### lib/new-ui/widgets/line_tab_switcher.dart
@@ -101,6 +101,7 @@ class _LineTabSwitcherState extends State<LineTabSwitcher> {
selected: widget.selectedTab == index,
inMutuallyExclusiveGroup: true,
child: GestureDetector(
+ key: ValueKey("line_tab_switcher_${index}_key"),
onTap: () {
widget.onTabChange(index);
},
### lib/new-ui/widgets/receive_page/receive_address_widget.dart
@@ -11,6 +11,7 @@ class ReceiveAddressWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
+ key: const ValueKey("receive_page_address_key"),
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: Observer(
builder: (_) => AddressFormatter.buildSegmentedAddress(
### lib/new-ui/widgets/send_page/send_address_input.dart
@@ -148,6 +148,7 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
_presentQRScanner(context);
}),
FloatingIconButton(
+ key: ValueKey("send_page_address_book_button_key"),
iconPath: "assets/new-ui/contacts_outlined.svg",
semanticLabel: S.of(context).address_book,
onPressed: () {
### lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart
@@ -46,6 +46,7 @@ class SendConfirmBottomWidget extends StatelessWidget {
switch (state) {
case ExecutedSuccessfullyState:
return ConfirmSwiper(
+ key: ValueKey("send_page_confirm_swiper_key"),
onConfirmed: () {
sendViewModel.commitTransaction(context);
},
### lib/new-ui/widgets/swap_page/swap_amount_box.dart
@@ -211,6 +211,9 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextField(
+ key: ValueKey(widget.isReceiverCard
+ ? "swap_page_receive_amount_field_key"
+ : "swap_page_deposit_amount_field_key"),
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
@@ -260,6 +263,9 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
),
),
GestureDetector(
+ key: ValueKey(widget.isReceiverCard
+ ? "swap_amount_box_receive_currency_button_key"
+ : "swap_amount_box_deposit_currency_button_key"),
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
### lib/src/screens/contact/contact_list_page.dart
@@ -68,6 +68,7 @@ class ContactListPage extends BasePage implements PageOpenListener {
minWidth: 32.0,
height: 32.0,
child: TextButton(
+ key: ValueKey("contact_list_page_add_contact_button_key"),
// FIX-ME: Style
//shape: CircleBorder(),
onPressed: () async {
### lib/src/screens/contact/contact_page.dart
@@ -73,6 +73,7 @@ class ContactPage extends BasePage {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
BaseTextFormField(
+ key: ValueKey("contact_page_name_textfield_key"),
controller: _nameController,
hintText: S.of(context).contact_name,
validator: ContactNameValidator(),
@@ -81,6 +82,7 @@ class ContactPage extends BasePage {
padding: EdgeInsets.only(top: 20),
child: Container(
child: InkWell(
+ key: ValueKey("contact_page_currency_picker_button_key"),
onTap: () => _presentCurrencyPicker(context),
child: IgnorePointer(
child: BaseTextFormField(
@@ -105,6 +107,7 @@ class ContactPage extends BasePage {
child: Observer(
builder: (_) {
return NewSendAddressInput(
+ key: ValueKey("contact_page_address_input_key"),
focusNode: addressFocusNode,
displayName: contactViewModel.displayName,
addressController: _addressController,
@@ -138,6 +141,7 @@ class ContactPage extends BasePage {
Expanded(
child: Observer(
builder: (_) => PrimaryButton(
+ key: ValueKey("contact_page_save_button_key"),
onPressed: () async {
FocusScope.of(context).unfocus();
_extractParsedAddress(context);
### lib/src/screens/dashboard/favorite_token_modal.dart
@@ -39,11 +39,13 @@ class FavoriteTokenModal extends StatelessWidget {
child: NewListSections(
sections: {
"": tokens
- .map((item) => ListItemRegularRow(
- keyValue: item.title,
- label: "${item.fullName} (${item.title})",
- iconPath: item.iconPath,
- onTap: () => Navigator.of(context).pop(item)))
+ .asMap()
+ .entries
+ .map((entry) => ListItemRegularRow(
+ keyValue: "${entry.value.title}_${entry.key}",
+ label: "${entry.value.fullName} (${entry.value.title})",
+ iconPath: entry.value.iconPath,
+ onTap: () => Navigator.of(context).pop(entry.value)))
.toList()
},
),
### lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart
@@ -193,6 +193,7 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
inMutuallyExclusiveGroup: true,
label: visibleActions[i].name(context),
child: InkWell(
+ key: visibleActions[i].key,
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
borderRadius: BorderRadius.circular(pillBorderRadius),
### lib/src/screens/new_wallet/wallet_group_display_page.dart
@@ -114,6 +114,7 @@ class WalletGroupsDisplayBody extends StatelessWidget {
Observer(
builder: (context) {
return LoadingPrimaryButton(
+ key: ValueKey("wallet_group_display_page_next_button_key"),
isLoading: walletGroupsDisplayViewModel.isFetchingMnemonic,
onPressed: () {
if (walletGroupsDisplayViewModel.hasNoFilteredWallet) {
### lib/src/screens/restore/wallet_restore_page.dart
@@ -102,6 +102,7 @@ class WalletRestorePage extends BasePage {
builder: (context) {
return walletRestoreViewModel.passphraseAvailable
? StandardCheckbox(
+ key: const ValueKey("wallet_restore_has_passphrase_checkbox_key"),
captionColor: Theme.of(context).colorScheme.onSecondaryContainer,
value: walletRestoreViewModel.hasPassphrase,
caption: S.of(context).wallet_has_passphrase,
### lib/src/screens/wallet/wallet_edit_page.dart
@@ -56,6 +56,7 @@ class WalletEditPage extends BasePage {
Expanded(
child: Center(
child: BaseTextFormField(
+ key: ValueKey("wallet_edit_page_name_input_key"),
controller: _labelController,
hintText: S.of(context).wallet_list_wallet_name,
validator: WalletNameValidator(),
@@ -74,6 +75,7 @@ class WalletEditPage extends BasePage {
child: Container(
padding: EdgeInsets.only(right: 8.0),
child: LoadingPrimaryButton(
+ key: ValueKey("wallet_edit_page_delete_button_key"),
isDisabled: isLoading,
onPressed: () => _removeWallet(context),
text: S.of(context).delete,
@@ -86,6 +88,7 @@ class WalletEditPage extends BasePage {
child: Container(
padding: EdgeInsets.only(left: 8.0),
child: LoadingPrimaryButton(
+ key: ValueKey("wallet_edit_page_save_button_key"),
onPressed: () async {
if (_formKey.currentState?.validate() ?? false) {
if (!pageArguments.isWalletGroup &&
@@ -95,6 +98,8 @@ class WalletEditPage extends BasePage {
context: context,
builder: (_) {
return AlertWithOneAction(
+ buttonKey:
+ ValueKey("wallet_edit_page_name_taken_ok_button_key"),
alertTitle: '',
alertContent: S.of(context).wallet_name_exists,
buttonText: S.of(context).ok,
@@ -182,6 +187,8 @@ class WalletEditPage extends BasePage {
context: context,
builder: (BuildContext dialogContext) {
return AlertWithTwoActions(
+ alertRightActionButtonKey: ValueKey("wallet_edit_page_confirm_delete_button_key"),
+ alertLeftActionButtonKey: ValueKey("wallet_edit_page_cancel_delete_button_key"),
alertTitle: S.of(context).delete_wallet,
alertContent:
S.of(context).delete_wallet_confirm_message(pageArguments.editingWallet.name),
### lib/src/screens/wallet_list/wallet_list_page.dart
@@ -231,6 +231,7 @@ class WalletListBodyState extends State<WalletListBody> {
return item.isCurrent
? SizedBox.shrink()
: EditWalletButtonWidget(
+ key: ValueKey("wallet_list_edit_${item.name}_button_key"),
width: 64,
onTap: () => Navigator.of(context).pushNamed(
Routes.walletEdit,
@@ -314,6 +315,7 @@ class WalletListBodyState extends State<WalletListBody> {
trailingWidget: wallet.isCurrent
? null
: EditWalletButtonWidget(
+ key: ValueKey("wallet_list_edit_${wallet.name}_button_key"),
width: 64,
onTap: () {
Navigator.of(context).pushNamed(
### lib/src/widgets/alert_with_one_action.dart
@@ -47,31 +47,6 @@ class AlertWithOneAction extends BaseAlertDialog {
@override
String get rightActionButtonText => buttonText;
- // @override
- // Widget actionButtons(BuildContext context) {
- // return Container(
- // padding: EdgeInsets.only(left: 12, right: 12),
- // color: Theme.of(context).colorScheme.surface,
- // child: ButtonTheme(
- // minWidth: double.infinity,
- // child: TextButton(
- // key: buttonKey,
- // onPressed: buttonAction,
- // // FIX-ME: Style
- // //highlightColor: Colors.transparent,
- // //splashColor: Colors.transparent,
- // child: Text(
- // buttonText,
- // textAlign: TextAlign.center,
- // style: Theme.of(context).textTheme.bodyMedium!.copyWith(
- // fontSize: 15,
- // fontWeight: FontWeight.w600,
- // color: Theme.of(context).colorScheme.primary,
- // decoration: TextDecoration.none,
- // ),
- // ),
- // ),
- // ),
- // );
- // }
+ @override
+ Key? get rightActionButtonKey => buttonKey;
}
### lib/src/widgets/new_list_row/new_list_section.dart
@@ -90,6 +90,7 @@ class NewListSections extends StatelessWidget {
if (item is ListItemRegularRow) {
return ListItemRegularRowWidget(
+ key: ValueKey(item.keyValue),
keyValue: item.keyValue,
label: item.label,
subtitle: item.subtitle,
@@ -118,6 +119,7 @@ class NewListSections extends StatelessWidget {
if (item is ListItemToggle) {
return ListItemToggleWidget(
+ key: ValueKey(item.keyValue),
keyValue: item.keyValue,
label: item.label,
subtitle: item.subtitle,
@@ -132,6 +134,7 @@ class NewListSections extends StatelessWidget {
if (item is ListItemCheckbox) {
return ListItemCheckboxWidget(
+ key: ValueKey(item.keyValue),
keyValue: item.keyValue,
label: item.label,
subtitle: item.subtitle,
@@ -148,6 +151,7 @@ class NewListSections extends StatelessWidget {
if (item is ListItemDropdown) {
return ListItemDropdownWidget(
+ key: ValueKey(item.keyValue),
keyValue: item.keyValue,
label: item.label,
trailingText: item.trailingText,
@@ -159,6 +163,7 @@ class NewListSections extends StatelessWidget {
if (item is ListItemSelector) {
return ListItemSelectorWidget(
+ key: ValueKey(item.keyValue),
keyValue: item.keyValue,
label: item.label,
options: item.options,
### lib/src/widgets/picker.dart
@@ -2,13 +2,11 @@
import 'dart:math';
-import 'package:cake_wallet/entities/seed_type.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/crypto_currency.dart';
-import 'package:cw_core/transaction_priority.dart';
import 'package:flutter/material.dart';
import 'package:cw_core/currency.dart';
import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
@@ -304,20 +302,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
);
}
- String _getItemName(Item item) {
- String itemName;
- if (item is Currency) {
- itemName = item.name;
- } else if (item is TransactionPriority) {
- itemName = item.title;
- } else if (item is MoneroSeedType) {
- itemName = item.title;
- } else {
- itemName = '';
- }
-
- return itemName;
- }
+ String _getItemName(Item item) => item is Currency ? item.name : item.toString();
Widget buildItem(int index) {
final item = widget.headerEnabled ? filteredItems[index] : items[index];
### lib/src/widgets/standard_checkbox.dart
@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
class StandardCheckbox extends StatelessWidget {
StandardCheckbox(
- {required this.value,
+ {super.key,
+ required this.value,
this.caption = '',
this.gradientBackground = false,
this.borderColor,
### scripts/ci/emulator_ready.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+echo "Waiting for boot completion..."
+timeout 300 bash -c 'until adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; do sleep 5; done' || echo "Boot completion timeout, continuing"
+
+echo "Waiting for package manager..."
+timeout 60 bash -c 'until adb shell pm list packages 2>/dev/null >/dev/null; do sleep 2; done' || echo "Package manager timeout, continuing"
+
+echo "Waiting for settings service..."
+timeout 60 bash -c 'until adb shell settings get global window_animation_scale 2>/dev/null >/dev/null; do sleep 2; done' || echo "Settings service timeout, continuing"
+
+echo "Emulator readiness check completed"
### scripts/ci/run_integration_tests.sh
@@ -0,0 +1,115 @@
+#!/bin/bash
+
+TIER="$1"
+
+if [[ -z "${SUITE_DIR_INPUT:-}" ]]; then
+ echo "SUITE_DIR_INPUT never reached the emulator script"
+ exit 1
+fi
+
+KEYS_IMPORTED=""
+
+import_encryption_keys() {
+ if [[ -n "$KEYS_IMPORTED" ]]; then
+ return
+ fi
+
+ gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 6B3199AD9B3D23B8 || true # konstantin@cakewallet.com
+ gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 35C8DBAFB8D9ACAC || true # cyjan@mrcyjanek.net
+ gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0126DFF8D54495EB || true # david@cakewallet.com
+
+ if gpg --list-keys 6B3199AD9B3D23B8 > /dev/null 2>&1 &&
+ gpg --list-keys 35C8DBAFB8D9ACAC > /dev/null 2>&1 &&
+ gpg --list-keys 0126DFF8D54495EB > /dev/null 2>&1; then
+ KEYS_IMPORTED=1
+ else
+ echo "Recipient keys missing from the keyring, the next call retries the import"
+ fi
+}
+
+encrypt_and_drop() {
+ gpg --trust-model always --encrypt --output "$1.gpg" \
+ --recipient 6B3199AD9B3D23B8 \
+ --recipient 35C8DBAFB8D9ACAC \
+ --recipient 0126DFF8D54495EB \
+ "$1" || echo "Encrypting $1 failed, discarding it"
+ rm -f "$1"
+}
+
+bash scripts/ci/emulator_ready.sh
+
+adb shell settings put global window_animation_scale 0.0 || true
+adb shell settings put global transition_animation_scale 0.0 || true
+adb shell settings put global animator_duration_scale 0.0 || true
+
+echo "===== resources before $TIER ====="
+free -h || true
+df -h / || true
+
+if [[ "$RECORD_SCREEN" == "true" ]]; then
+ echo "Starting chunked screen recording..."
+ (i=0; while true; do adb shell screenrecord --time-limit 180 --bit-rate 2000000 "/sdcard/rec_${TIER}_$i.mp4" || break; i=$((i+1)); done) &
+ RECORD_LOOP_PID=$!
+fi
+
+# Both tiers get one retry. tier1 talks to live nodes and providers, and giving it
+# no retry meant a driver that never attached was reported as a failing test.
+RETRIES=1
+
+# The step log is public, so a run log that gets encrypted below must not print here.
+run_suites() {
+ env \
+ SUITE_DIR="$SUITE_DIR_INPUT" \
+ TEST_TIER="$TIER" \
+ EXTRA_DART_DEFINES="$EXTRA_DART_DEFINES_INPUT" \
+ TEST_TIMEOUT=600 \
+ RETRY_COUNT=$RETRIES \
+ PLATFORM=android \
+ SUMMARY_FILE="${TIER}_summary.txt" \
+ ./integration_test_runner.sh 2>&1
+}
+
+set -o pipefail
+set +e
+if [[ "${ENCRYPT_LOGS:-}" == "true" ]]; then
+ run_suites > "${TIER}_run.log"
+else
+ run_suites | tee "${TIER}_run.log"
+fi
+TEST_EXIT_CODE=$?
+set -e
+
+echo "===== resources after $TIER ====="
+free -h || true
+df -h / || true
+
+adb logcat -d > "${TIER}_logcat.txt" || true
+
+# A funds run puts real funded seeds on the device and these carry whatever the app
+# logs, so they leave the runner the same way the recordings do
+if [[ "${ENCRYPT_LOGS:-}" == "true" ]]; then
+ import_encryption_keys
+ encrypt_and_drop "${TIER}_logcat.txt"
+ encrypt_and_drop "${TIER}_run.log"
+fi
+
+if [[ -n "${RECORD_LOOP_PID:-}" ]]; then
+ echo "Stopping screen recording and encrypting..."
+ kill "$RECORD_LOOP_PID" || true
+ adb shell killall -INT screenrecord || true
+ sleep 5
+
+ mkdir -p recordings
+ for f in $(adb shell ls /sdcard/rec_*.mp4 2>/dev/null | tr -d '\r'); do
+ adb pull "$f" recordings/ || true
+ done
+
+ # Recordings show seed words on screen so they are never uploaded in plaintext
+ import_encryption_keys
+ for f in recordings/*.mp4; do
+ [[ -f "$f" ]] || continue
+ encrypt_and_drop "$f"
+ done
+fi
+
+exit $TEST_EXIT_CODE
### test/widget_test.dart
@@ -1,34 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility that Flutter provides. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:cake_wallet/main.dart';
-
-final quickActionsStream = StreamController<Uri?>.broadcast();
-
-void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(App(quickActionsStream: quickActionsStream.stream));
-
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
- });
-}
### test_driver/integration_test.dart
@@ -1,19 +1,40 @@
-import 'dart:convert';
+import "dart:async";
+import "dart:convert";
-import 'package:integration_test/integration_test_driver.dart';
-import 'package:path/path.dart' as path;
-
-import 'package:flutter_driver/flutter_driver.dart';
+import "package:flutter_driver/flutter_driver.dart";
+import "package:integration_test/integration_test_driver_extended.dart";
+import "package:path/path.dart" as path;
Future<void> main() async {
- integrationDriver(
- responseDataCallback: (Map<String, dynamic>? data) async {
+ await runZoned(
+ _drive,
+ zoneSpecification: ZoneSpecification(
+ print: (self, parent, zone, line) {
+ if (!line.startsWith("result {")) {
+ parent.print(zone, line);
+ }
+ },
+ ),
+ );
+}
+
+Future<void> _drive() async {
+ await integrationDriver(
+ onScreenshot: (name, bytes, [args]) async {
+ await fs.directory(_screenshotDirectory).create(recursive: true);
+ await fs.file(path.join(_screenshotDirectory, "$name.png")).writeAsBytes(bytes);
+
+ return true;
+ },
+ responseDataCallback: (data) async {
+ data?.remove("screenshots");
+
await fs.directory(_destinationDirectory).create(recursive: true);
final file = fs.file(
path.join(
_destinationDirectory,
- '$_testOutputFilename.json',
+ "$_testOutputFilename.json",
),
);
@@ -28,6 +49,7 @@ String _encodeJson(Map<String, dynamic>? jsonObject) {
return _prettyEncoder.convert(jsonObject);
}
-const _prettyEncoder = JsonEncoder.withIndent(' ');
-const _testOutputFilename = 'integration_response_data';
-const _destinationDirectory = 'integration_test';
+const _prettyEncoder = JsonEncoder.withIndent(" ");
+const _testOutputFilename = "integration_response_data";
+const _destinationDirectory = "build/integration_test";
+const _screenshotDirectory = "build/integration_test_screenshots";
### tool/utils/secret_key.dart
@@ -50,7 +50,7 @@ class SecretKey {
SecretKey('meldTestApiKey', () => ''),
SecretKey('meldTestPublicKey', () => ''),
SecretKey('moneroTestWalletSeeds', () => ''),
- SecretKey('moneroLegacyTestWalletSeeds ', () => ''),
+ SecretKey('moneroLegacyTestWalletSeeds', () => ''),
SecretKey('bitcoinTestWalletSeeds', () => ''),
SecretKey('ethereumTestWalletSeeds', () => ''),
SecretKey('litecoinTestWalletSeeds', () => ''),
@@ -60,9 +60,14 @@ class SecretKey {
SecretKey('arbitrumTestWalletSeeds', () => ''),
SecretKey('bscTestWalletSeeds', () => ''),
SecretKey('solanaTestWalletSeeds', () => ''),
+ SecretKey('solanaTestWalletSeeds2', () => ''),
SecretKey('tronTestWalletSeeds', () => ''),
SecretKey('nanoTestWalletSeeds', () => ''),
SecretKey('wowneroTestWalletSeeds', () => ''),
+ SecretKey('zanoTestWalletSeeds', () => ''),
+ SecretKey('decredTestWalletSeeds', () => ''),
+ SecretKey('dogeTestWalletSeeds', () => ''),
+ SecretKey('zcashTestWalletSeeds', () => ''),
SecretKey('moneroTestWalletReceiveAddress', () => ''),
SecretKey('bitcoinTestWalletReceiveAddress', () => ''),
SecretKey('ethereumTestWalletReceiveAddress', () => ''),
@@ -76,13 +81,18 @@ class SecretKey {
SecretKey('tronTestWalletReceiveAddress', () => ''),
SecretKey('nanoTestWalletReceiveAddress', () => ''),
SecretKey('wowneroTestWalletReceiveAddress', () => ''),
+ SecretKey('zanoTestWalletReceiveAddress', () => ''),
+ SecretKey('decredTestWalletReceiveAddress', () => ''),
+ SecretKey('dogeTestWalletReceiveAddress', () => ''),
+ SecretKey('zcashTestWalletReceiveAddress', () => ''),
SecretKey('etherScanApiKey', () => ''),
SecretKey('polygonScanApiKey', () => ''),
SecretKey('letsExchangeBearerToken', () => ''),
SecretKey('letsExchangeAffiliateId', () => ''),
SecretKey('stealthExBearerToken', () => ''),
SecretKey('stealthExAdditionalFeePercent', () => ''),
SecretKey('moneroTestWalletBlockHeight', () => ''),
+ SecretKey('zcashTestWalletRestoreHeight', () => ''),
SecretKey('chainflipApiKey', () => ''),
SecretKey('chainflipAffiliateFee', () => ''),
SecretKey('kryptonimApiKey', () => ''),
@@ -100,7 +110,7 @@ class SecretKey {
SecretKey('etherScanApiKey', () => ''),
SecretKey('polygonScanApiKey', () => ''),
SecretKey('moralisApiKey', () => ''),
- SecretKey('nowNodesApiKey ', () => ''),
+ SecretKey('nowNodesApiKey', () => ''),
SecretKey('blinkApiKey', () => ''),
];
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.