- Android: move plugin source to org/timesafari/dailynotification, update namespace, manifest package, and all package/imports; change intent actions to org.timesafari.daily.NOTIFICATION and DISMISS - iOS: update bundle IDs, BGTask identifiers, subsystem labels, and queue names in Plugin and Xcode projects - Capacitor: update plugin class registration and appIds in configs - Test apps (android-test-app, daily-notification-test, ios-test-app): applicationId/bundleId, manifests, ProGuard, scripts, and docs - Docs: bulk update references; add CONSUMING_APP_MIGRATION_COM_TO_ORG.md for consuming app migration BREAKING CHANGE: Consuming apps must update plugin class to org.timesafari.dailynotification.DailyNotificationPlugin, manifest receivers/actions, and iOS BGTask identifiers per migration doc.
263 lines
9.6 KiB
Python
Executable File
263 lines
9.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
DailyNotification Plugin Automated Test Suite
|
||
Usage: python3 daily-notification-test.py [OPTIONS]
|
||
"""
|
||
|
||
import subprocess
|
||
import time
|
||
import json
|
||
import sys
|
||
import argparse
|
||
from typing import Optional, Dict, Any, List
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
|
||
class TestResult(Enum):
|
||
PASS = "PASS"
|
||
FAIL = "FAIL"
|
||
SKIP = "SKIP"
|
||
|
||
@dataclass
|
||
class TestCase:
|
||
name: str
|
||
description: str
|
||
result: TestResult
|
||
duration: float
|
||
error: Optional[str] = None
|
||
|
||
class DailyNotificationTester:
|
||
def __init__(self, package: str = "org.timesafari.dailynotification", verbose: bool = False):
|
||
self.package = package
|
||
self.activity = f"{package}/.MainActivity"
|
||
self.verbose = verbose
|
||
self.test_results: List[TestCase] = []
|
||
|
||
def log(self, message: str, level: str = "INFO"):
|
||
"""Log message with timestamp"""
|
||
timestamp = time.strftime("%H:%M:%S")
|
||
if level == "ERROR":
|
||
print(f"❌ [{timestamp}] {message}")
|
||
elif level == "SUCCESS":
|
||
print(f"✅ [{timestamp}] {message}")
|
||
elif level == "WARNING":
|
||
print(f"⚠️ [{timestamp}] {message}")
|
||
elif level == "INFO":
|
||
print(f"ℹ️ [{timestamp}] {message}")
|
||
else:
|
||
print(f"[{timestamp}] {message}")
|
||
|
||
def run_adb_command(self, command: str, capture_output: bool = True) -> subprocess.CompletedProcess:
|
||
"""Run ADB command and return result"""
|
||
full_command = f"adb {command}"
|
||
if self.verbose:
|
||
self.log(f"Running: {full_command}")
|
||
|
||
try:
|
||
return subprocess.run(
|
||
full_command,
|
||
shell=True,
|
||
capture_output=capture_output,
|
||
text=True,
|
||
timeout=30
|
||
)
|
||
except subprocess.TimeoutExpired:
|
||
self.log(f"Command timed out: {full_command}", "ERROR")
|
||
raise
|
||
|
||
def is_app_running(self) -> bool:
|
||
"""Check if app is currently running"""
|
||
result = self.run_adb_command(f'shell "ps | grep {self.package}"')
|
||
return result.returncode == 0 and self.package in result.stdout
|
||
|
||
def launch_app(self) -> bool:
|
||
"""Launch the app"""
|
||
self.log("Launching app...")
|
||
result = self.run_adb_command(f"shell am start -n {self.activity}")
|
||
time.sleep(3) # Wait for app to load
|
||
return self.is_app_running()
|
||
|
||
def send_to_background(self) -> bool:
|
||
"""Send app to background"""
|
||
self.log("Sending app to background...")
|
||
self.run_adb_command("shell input keyevent KEYCODE_HOME")
|
||
time.sleep(2)
|
||
return self.is_app_running()
|
||
|
||
def force_stop_app(self) -> bool:
|
||
"""Force stop the app"""
|
||
self.log("Force stopping app...")
|
||
self.run_adb_command(f"shell am force-stop {self.package}")
|
||
time.sleep(2)
|
||
return not self.is_app_running()
|
||
|
||
def check_notification_logs(self, timeout: int = 60) -> bool:
|
||
"""Check for notification success in logs"""
|
||
self.log(f"Checking notification logs (timeout: {timeout}s)...")
|
||
start_time = time.time()
|
||
|
||
while time.time() - start_time < timeout:
|
||
result = self.run_adb_command("logcat -d")
|
||
if "Notification displayed successfully" in result.stdout:
|
||
return True
|
||
time.sleep(5)
|
||
return False
|
||
|
||
def check_permissions(self) -> Dict[str, bool]:
|
||
"""Check app permissions"""
|
||
self.log("Checking app permissions...")
|
||
result = self.run_adb_command(f"shell dumpsys package {self.package}")
|
||
|
||
permissions = {
|
||
"notifications": "POST_NOTIFICATIONS" in result.stdout,
|
||
"exact_alarm": "SCHEDULE_EXACT_ALARM" in result.stdout,
|
||
"wake_lock": "WAKE_LOCK" in result.stdout
|
||
}
|
||
|
||
return permissions
|
||
|
||
def check_scheduled_alarms(self) -> bool:
|
||
"""Check if any alarms are scheduled for the app"""
|
||
self.log("Checking scheduled alarms...")
|
||
result = self.run_adb_command("shell \"dumpsys alarm | grep timesafari\"")
|
||
return result.returncode == 0 and self.package in result.stdout
|
||
|
||
def run_test(self, test_name: str, test_func, *args, **kwargs) -> TestCase:
|
||
"""Run a single test case"""
|
||
self.log(f"Running test: {test_name}")
|
||
start_time = time.time()
|
||
|
||
try:
|
||
result = test_func(*args, **kwargs)
|
||
duration = time.time() - start_time
|
||
|
||
if result:
|
||
self.log(f"Test passed: {test_name}", "SUCCESS")
|
||
return TestCase(test_name, "", TestResult.PASS, duration)
|
||
else:
|
||
self.log(f"Test failed: {test_name}", "ERROR")
|
||
return TestCase(test_name, "", TestResult.FAIL, duration, "Test returned False")
|
||
|
||
except Exception as e:
|
||
duration = time.time() - start_time
|
||
self.log(f"Test error: {test_name} - {str(e)}", "ERROR")
|
||
return TestCase(test_name, "", TestResult.FAIL, duration, str(e))
|
||
|
||
def run_test_suite(self) -> Dict[str, Any]:
|
||
"""Run complete test suite"""
|
||
self.log("🧪 Starting DailyNotification Test Suite")
|
||
self.log("=" * 50)
|
||
|
||
# Check ADB connection
|
||
result = self.run_adb_command("devices")
|
||
if "device" not in result.stdout:
|
||
self.log("No Android device connected via ADB", "ERROR")
|
||
sys.exit(1)
|
||
|
||
self.log("ADB device connected", "SUCCESS")
|
||
|
||
# Test 1: App Launch
|
||
test_case = self.run_test("App Launch", self.launch_app)
|
||
self.test_results.append(test_case)
|
||
|
||
# Test 2: Background Test
|
||
test_case = self.run_test("Background Operation", self.send_to_background)
|
||
self.test_results.append(test_case)
|
||
|
||
# Test 3: Force Stop Test
|
||
test_case = self.run_test("Force Stop", self.force_stop_app)
|
||
self.test_results.append(test_case)
|
||
|
||
# Test 4: Permission Check
|
||
def check_permissions_test():
|
||
permissions = self.check_permissions()
|
||
return any(permissions.values())
|
||
|
||
test_case = self.run_test("Permission Check", check_permissions_test)
|
||
self.test_results.append(test_case)
|
||
|
||
# Test 5: Alarm Check
|
||
test_case = self.run_test("Alarm Scheduling", self.check_scheduled_alarms)
|
||
self.test_results.append(test_case)
|
||
|
||
# Test 6: Notification Test (requires manual scheduling)
|
||
self.log("📱 Manual Notification Test")
|
||
self.log("⚠️ Manual step required: Schedule notification in app", "WARNING")
|
||
input("Press Enter when notification is scheduled...")
|
||
|
||
test_case = self.run_test("Notification Delivery", self.check_notification_logs, 120)
|
||
self.test_results.append(test_case)
|
||
|
||
return self.generate_report()
|
||
|
||
def generate_report(self) -> Dict[str, Any]:
|
||
"""Generate test report"""
|
||
total_tests = len(self.test_results)
|
||
passed_tests = sum(1 for test in self.test_results if test.result == TestResult.PASS)
|
||
failed_tests = sum(1 for test in self.test_results if test.result == TestResult.FAIL)
|
||
|
||
report = {
|
||
"summary": {
|
||
"total": total_tests,
|
||
"passed": passed_tests,
|
||
"failed": failed_tests,
|
||
"success_rate": (passed_tests / total_tests * 100) if total_tests > 0 else 0
|
||
},
|
||
"tests": [
|
||
{
|
||
"name": test.name,
|
||
"result": test.result.value,
|
||
"duration": test.duration,
|
||
"error": test.error
|
||
}
|
||
for test in self.test_results
|
||
]
|
||
}
|
||
|
||
return report
|
||
|
||
def print_report(self, report: Dict[str, Any]):
|
||
"""Print test report"""
|
||
self.log("\n📊 Test Results Summary:")
|
||
self.log("=" * 30)
|
||
|
||
summary = report["summary"]
|
||
self.log(f"Total Tests: {summary['total']}")
|
||
self.log(f"Passed: {summary['passed']}")
|
||
self.log(f"Failed: {summary['failed']}")
|
||
self.log(f"Success Rate: {summary['success_rate']:.1f}%")
|
||
|
||
self.log("\nDetailed Results:")
|
||
for test in report["tests"]:
|
||
status = "✅ PASS" if test["result"] == "PASS" else "❌ FAIL"
|
||
duration = f"({test['duration']:.1f}s)"
|
||
self.log(f"{test['name']}: {status} {duration}")
|
||
if test["error"]:
|
||
self.log(f" Error: {test['error']}", "ERROR")
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="DailyNotification Plugin Test Suite")
|
||
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
|
||
parser.add_argument("-p", "--package", default="org.timesafari.dailynotification", help="App package name")
|
||
parser.add_argument("-o", "--output", help="Output report to JSON file")
|
||
parser.add_argument("--timeout", type=int, default=120, help="Test timeout in seconds")
|
||
|
||
args = parser.parse_args()
|
||
|
||
tester = DailyNotificationTester(package=args.package, verbose=args.verbose)
|
||
report = tester.run_test_suite()
|
||
tester.print_report(report)
|
||
|
||
# Save report to file if specified
|
||
if args.output:
|
||
with open(args.output, 'w') as f:
|
||
json.dump(report, f, indent=2)
|
||
tester.log(f"Report saved to: {args.output}")
|
||
|
||
# Exit with error code if any test failed
|
||
if report["summary"]["failed"] > 0:
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|