Implemented comprehensive bulk operations for BLE sensor management to improve efficiency when working with multiple sensors simultaneously. Features Added: - bulkDisconnect: Disconnect multiple sensors at once - bulkReboot: Reboot multiple sensors sequentially - bulkSetWiFi: Configure WiFi for multiple sensors with progress tracking Implementation Details: - Added BulkOperationResult and BulkWiFiResult types to track operation outcomes - Implemented bulk operations in both RealBLEManager and MockBLEManager - Exposed bulk operations through BLEContext for easy UI integration - Sequential processing ensures reliable operation completion - Progress callbacks for real-time UI updates during bulk operations Testing: - Added comprehensive test suite with 14 test cases - Tests cover success scenarios, error handling, and edge cases - All tests passing with appropriate timeout configurations - Verified both individual and sequential bulk operations Technical Notes: - Bulk operations maintain device connection state consistency - Error handling allows graceful continuation despite individual failures - MockBLEManager includes realistic delays for testing - Integration with existing BLE service architecture preserved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
416 lines
11 KiB
Bash
Executable File
416 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# WellNuo Full E2E Test Runner
|
|
# Orchestrates Maestro tests with temp email for real OTP verification
|
|
#
|
|
# Usage: ./run-full-e2e.sh [--device SERIAL] [--skip-build]
|
|
#
|
|
# Requirements:
|
|
# - Maestro installed (~/.maestro/bin/maestro)
|
|
# - Android device connected via USB
|
|
# - Node.js for temp email operations
|
|
# - Release APK built
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
MAESTRO_BIN="${HOME}/.maestro/bin/maestro"
|
|
RESULTS_DIR="${SCRIPT_DIR}/test-results/$(date +%Y-%m-%d_%H%M%S)"
|
|
APK_PATH="${PROJECT_DIR}/android/app/build/outputs/apk/release/app-release.apk"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Parse arguments
|
|
DEVICE_SERIAL=""
|
|
SKIP_BUILD=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--device)
|
|
DEVICE_SERIAL="$2"
|
|
shift 2
|
|
;;
|
|
--skip-build)
|
|
SKIP_BUILD=true
|
|
shift
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
log() {
|
|
echo -e "${BLUE}[E2E]${NC} $1"
|
|
}
|
|
|
|
success() {
|
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
}
|
|
|
|
error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
# Check prerequisites
|
|
check_prerequisites() {
|
|
log "Checking prerequisites..."
|
|
|
|
# Check Maestro
|
|
if [ ! -f "$MAESTRO_BIN" ]; then
|
|
error "Maestro not found at $MAESTRO_BIN"
|
|
echo "Install: curl -Ls 'https://get.maestro.mobile.dev' | bash"
|
|
exit 1
|
|
fi
|
|
|
|
# Check device
|
|
if [ -z "$DEVICE_SERIAL" ]; then
|
|
DEVICE_SERIAL=$(adb devices | grep -v "List" | grep "device$" | head -1 | awk '{print $1}')
|
|
fi
|
|
|
|
if [ -z "$DEVICE_SERIAL" ]; then
|
|
error "No Android device connected"
|
|
echo "Connect device via USB and enable USB debugging"
|
|
exit 1
|
|
fi
|
|
|
|
log "Using device: $DEVICE_SERIAL"
|
|
export ANDROID_SERIAL="$DEVICE_SERIAL"
|
|
|
|
# Keep screen on during tests
|
|
adb -s "$DEVICE_SERIAL" shell settings put system screen_off_timeout 600000
|
|
adb -s "$DEVICE_SERIAL" shell settings put global stay_on_while_plugged_in 3
|
|
|
|
success "Prerequisites OK"
|
|
}
|
|
|
|
# Build release APK
|
|
build_apk() {
|
|
if [ "$SKIP_BUILD" = true ]; then
|
|
log "Skipping build (--skip-build)"
|
|
return
|
|
fi
|
|
|
|
log "Building release APK..."
|
|
cd "$PROJECT_DIR/android"
|
|
./gradlew assembleRelease --quiet
|
|
|
|
if [ ! -f "$APK_PATH" ]; then
|
|
error "APK not found at $APK_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
success "APK built: $APK_PATH"
|
|
}
|
|
|
|
# Install APK
|
|
install_apk() {
|
|
log "Installing APK on device..."
|
|
adb -s "$DEVICE_SERIAL" install -r "$APK_PATH" 2>/dev/null || true
|
|
success "APK installed"
|
|
}
|
|
|
|
# Create temp email (using Node.js script)
|
|
create_temp_email() {
|
|
log "Creating temporary email..."
|
|
|
|
# Use temp-mail MCP to create email
|
|
# This creates a simple Node script that uses the tempmail API
|
|
|
|
TEMP_EMAIL_SCRIPT=$(mktemp)
|
|
cat > "$TEMP_EMAIL_SCRIPT" << 'EMAILSCRIPT'
|
|
const https = require('https');
|
|
|
|
// Simple temp email using mail.tm API
|
|
async function createTempEmail() {
|
|
return new Promise((resolve, reject) => {
|
|
// Get available domains
|
|
https.get('https://api.mail.tm/domains', (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
const domains = JSON.parse(data);
|
|
const domain = domains['hydra:member'][0].domain;
|
|
|
|
// Generate random email
|
|
const username = 'wellnuo_test_' + Math.random().toString(36).substring(7);
|
|
const email = `${username}@${domain}`;
|
|
const password = 'TestPass123!';
|
|
|
|
// Create account
|
|
const postData = JSON.stringify({ address: email, password });
|
|
const options = {
|
|
hostname: 'api.mail.tm',
|
|
path: '/accounts',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': postData.length
|
|
}
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let body = '';
|
|
res.on('data', chunk => body += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode === 201) {
|
|
// Get token
|
|
const loginData = JSON.stringify({ address: email, password });
|
|
const loginOptions = {
|
|
hostname: 'api.mail.tm',
|
|
path: '/token',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': loginData.length
|
|
}
|
|
};
|
|
|
|
const loginReq = https.request(loginOptions, (loginRes) => {
|
|
let loginBody = '';
|
|
loginRes.on('data', chunk => loginBody += chunk);
|
|
loginRes.on('end', () => {
|
|
const token = JSON.parse(loginBody).token;
|
|
resolve({ email, password, token });
|
|
});
|
|
});
|
|
loginReq.write(loginData);
|
|
loginReq.end();
|
|
} else {
|
|
reject(new Error(`Failed to create email: ${res.statusCode}`));
|
|
}
|
|
});
|
|
});
|
|
req.write(postData);
|
|
req.end();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
createTempEmail()
|
|
.then(result => console.log(JSON.stringify(result)))
|
|
.catch(err => {
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
});
|
|
EMAILSCRIPT
|
|
|
|
TEMP_EMAIL_JSON=$(node "$TEMP_EMAIL_SCRIPT" 2>/dev/null)
|
|
rm "$TEMP_EMAIL_SCRIPT"
|
|
|
|
if [ -z "$TEMP_EMAIL_JSON" ]; then
|
|
error "Failed to create temp email"
|
|
exit 1
|
|
fi
|
|
|
|
TEMP_EMAIL=$(echo "$TEMP_EMAIL_JSON" | grep -o '"email":"[^"]*"' | cut -d'"' -f4)
|
|
TEMP_TOKEN=$(echo "$TEMP_EMAIL_JSON" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
|
|
|
success "Temp email created: $TEMP_EMAIL"
|
|
echo "$TEMP_EMAIL_JSON" > "$RESULTS_DIR/temp_email.json"
|
|
}
|
|
|
|
# Wait for OTP email and extract code
|
|
get_otp_from_email() {
|
|
log "Waiting for OTP email (up to 60 seconds)..."
|
|
|
|
OTP_SCRIPT=$(mktemp)
|
|
cat > "$OTP_SCRIPT" << 'OTPSCRIPT'
|
|
const https = require('https');
|
|
|
|
const token = process.argv[2];
|
|
const maxAttempts = 12; // 12 * 5s = 60s
|
|
let attempts = 0;
|
|
|
|
function checkMessages() {
|
|
return new Promise((resolve) => {
|
|
const options = {
|
|
hostname: 'api.mail.tm',
|
|
path: '/messages',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
};
|
|
|
|
https.get(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
const messages = JSON.parse(data)['hydra:member'];
|
|
if (messages && messages.length > 0) {
|
|
// Get first message
|
|
const msgId = messages[0].id;
|
|
|
|
// Fetch full message
|
|
const msgOptions = {
|
|
hostname: 'api.mail.tm',
|
|
path: `/messages/${msgId}`,
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
};
|
|
|
|
https.get(msgOptions, (msgRes) => {
|
|
let msgData = '';
|
|
msgRes.on('data', chunk => msgData += chunk);
|
|
msgRes.on('end', () => {
|
|
const msg = JSON.parse(msgData);
|
|
// Extract 6-digit OTP from email body
|
|
const body = msg.text || msg.html || '';
|
|
const otpMatch = body.match(/\b(\d{6})\b/);
|
|
if (otpMatch) {
|
|
resolve(otpMatch[1]);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
} catch (e) {
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function waitForOtp() {
|
|
while (attempts < maxAttempts) {
|
|
const otp = await checkMessages();
|
|
if (otp) {
|
|
console.log(otp);
|
|
process.exit(0);
|
|
}
|
|
attempts++;
|
|
await new Promise(r => setTimeout(r, 5000));
|
|
}
|
|
console.error('Timeout waiting for OTP');
|
|
process.exit(1);
|
|
}
|
|
|
|
waitForOtp();
|
|
OTPSCRIPT
|
|
|
|
OTP_CODE=$(node "$OTP_SCRIPT" "$TEMP_TOKEN" 2>/dev/null)
|
|
rm "$OTP_SCRIPT"
|
|
|
|
if [ -z "$OTP_CODE" ] || [ "$OTP_CODE" = "Timeout waiting for OTP" ]; then
|
|
error "Failed to get OTP code from email"
|
|
exit 1
|
|
fi
|
|
|
|
success "Got OTP code: $OTP_CODE"
|
|
}
|
|
|
|
# Run Maestro test with variable substitution
|
|
run_maestro_test() {
|
|
local test_file="$1"
|
|
local test_name=$(basename "$test_file" .yaml)
|
|
|
|
log "Running test: $test_name"
|
|
|
|
# Create temp file with variables substituted
|
|
local temp_test=$(mktemp)
|
|
sed -e "s/\${EMAIL}/$TEMP_EMAIL/g" \
|
|
-e "s/\${OTP_CODE}/$OTP_CODE/g" \
|
|
"$test_file" > "$temp_test"
|
|
|
|
# Run Maestro
|
|
if "$MAESTRO_BIN" test "$temp_test" --output "$RESULTS_DIR/$test_name" 2>&1; then
|
|
success "Test passed: $test_name"
|
|
rm "$temp_test"
|
|
return 0
|
|
else
|
|
error "Test failed: $test_name"
|
|
rm "$temp_test"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Main test flow
|
|
main() {
|
|
echo ""
|
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
|
echo "║ WellNuo Full E2E Test Suite ║"
|
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
|
|
# Setup
|
|
mkdir -p "$RESULTS_DIR"
|
|
check_prerequisites
|
|
|
|
# Build and install
|
|
build_apk
|
|
install_apk
|
|
|
|
# Create temp email
|
|
create_temp_email
|
|
|
|
# Phase 1: Registration + Email entry
|
|
log "=== Phase 1: Registration ==="
|
|
if ! run_maestro_test "$SCRIPT_DIR/01-registration-flow.yaml"; then
|
|
error "Registration flow failed"
|
|
exit 1
|
|
fi
|
|
|
|
# Get OTP from email
|
|
get_otp_from_email
|
|
|
|
# Phase 2: OTP Entry
|
|
log "=== Phase 2: OTP Verification ==="
|
|
if ! run_maestro_test "$SCRIPT_DIR/02-enter-otp.yaml"; then
|
|
warn "OTP entry failed, may be existing user"
|
|
fi
|
|
|
|
# Phase 3: Enter Name (new users only)
|
|
log "=== Phase 3: Enter Name ==="
|
|
run_maestro_test "$SCRIPT_DIR/03-enter-name.yaml" || true
|
|
|
|
# Phase 4: Add Beneficiary
|
|
log "=== Phase 4: Add Beneficiary ==="
|
|
run_maestro_test "$SCRIPT_DIR/04-add-beneficiary.yaml" || true
|
|
|
|
# Phase 5: Purchase/Demo
|
|
log "=== Phase 5: Purchase Decision ==="
|
|
run_maestro_test "$SCRIPT_DIR/05-purchase-or-demo.yaml" || true
|
|
|
|
# Phase 6: Activation
|
|
log "=== Phase 6: Device Activation ==="
|
|
run_maestro_test "$SCRIPT_DIR/06-activate-device.yaml" || true
|
|
|
|
# Phase 7: Subscribe (Payment)
|
|
log "=== Phase 7: Subscribe (Stripe Payment) ==="
|
|
run_maestro_test "$SCRIPT_DIR/08-subscribe.yaml" || true
|
|
|
|
# Phase 8: Dashboard Verification
|
|
log "=== Phase 8: Dashboard Verification ==="
|
|
if run_maestro_test "$SCRIPT_DIR/07-verify-dashboard.yaml"; then
|
|
success "FULL E2E TEST PASSED!"
|
|
else
|
|
warn "Dashboard verification had issues"
|
|
fi
|
|
|
|
echo ""
|
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
|
echo "║ TEST COMPLETE ║"
|
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
log "Results saved to: $RESULTS_DIR"
|
|
log "Screenshots: $RESULTS_DIR/*/screenshots/"
|
|
|
|
# List screenshots
|
|
find "$RESULTS_DIR" -name "*.png" -type f 2>/dev/null | head -20
|
|
}
|
|
|
|
main "$@"
|