Files
dotfiles/.agents/skills/ios-simulator-skill/scripts/push_notification.py
2026-02-19 00:33:08 -08:00

241 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""
iOS Push Notification Simulator
Send simulated push notifications to test notification handling.
Supports custom payloads and test tracking.
Usage: python scripts/push_notification.py --bundle-id com.app --title "Alert" --body "Message"
"""
import argparse
import json
import subprocess
import sys
import tempfile
from pathlib import Path
from common import resolve_udid
class PushNotificationSender:
"""Sends simulated push notifications to iOS simulator."""
def __init__(self, udid: str | None = None):
"""Initialize push notification sender.
Args:
udid: Optional device UDID (auto-detects booted simulator if None)
"""
self.udid = udid
def send(
self,
bundle_id: str,
payload: dict | str,
_test_name: str | None = None,
_expected_behavior: str | None = None,
) -> bool:
"""
Send push notification to app.
Args:
bundle_id: Target app bundle ID
payload: Push payload (dict or JSON string) or path to JSON file
test_name: Test scenario name for tracking
expected_behavior: Expected behavior after notification arrives
Returns:
Success status
"""
# Handle different payload formats
if isinstance(payload, str):
# Check if it's a file path
payload_path = Path(payload)
if payload_path.exists():
with open(payload_path) as f:
payload_data = json.load(f)
else:
# Try to parse as JSON string
try:
payload_data = json.loads(payload)
except json.JSONDecodeError:
print(f"Error: Invalid JSON payload: {payload}")
return False
else:
payload_data = payload
# Ensure payload has aps dictionary
if "aps" not in payload_data:
payload_data = {"aps": payload_data}
# Create temp file with payload
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(payload_data, f)
temp_payload_path = f.name
# Build simctl command
cmd = ["xcrun", "simctl", "push"]
if self.udid:
cmd.append(self.udid)
else:
cmd.append("booted")
cmd.extend([bundle_id, temp_payload_path])
# Send notification
subprocess.run(cmd, capture_output=True, text=True, check=True)
# Clean up temp file
Path(temp_payload_path).unlink()
return True
except subprocess.CalledProcessError as e:
print(f"Error sending push notification: {e.stderr}")
return False
except Exception as e:
print(f"Error: {e}")
return False
def send_simple(
self,
bundle_id: str,
title: str | None = None,
body: str | None = None,
badge: int | None = None,
sound: bool = True,
) -> bool:
"""
Send simple push notification with common parameters.
Args:
bundle_id: Target app bundle ID
title: Alert title
body: Alert body
badge: Badge number
sound: Whether to play sound
Returns:
Success status
"""
payload = {}
if title or body:
alert = {}
if title:
alert["title"] = title
if body:
alert["body"] = body
payload["alert"] = alert
if badge is not None:
payload["badge"] = badge
if sound:
payload["sound"] = "default"
# Wrap in aps
full_payload = {"aps": payload}
return self.send(bundle_id, full_payload)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Send simulated push notification to iOS app")
# Required
parser.add_argument(
"--bundle-id", required=True, help="Target app bundle ID (e.g., com.example.app)"
)
# Simple payload options
parser.add_argument("--title", help="Alert title (for simple notifications)")
parser.add_argument("--body", help="Alert body message")
parser.add_argument("--badge", type=int, help="Badge number")
parser.add_argument("--no-sound", action="store_true", help="Don't play notification sound")
# Custom payload
parser.add_argument(
"--payload",
help="Custom JSON payload file or inline JSON string",
)
# Test tracking
parser.add_argument("--test-name", help="Test scenario name for tracking")
parser.add_argument(
"--expected",
help="Expected behavior after notification",
)
# Device
parser.add_argument(
"--udid",
help="Device UDID (auto-detects booted simulator if not provided)",
)
args = parser.parse_args()
# Resolve UDID with auto-detection
try:
udid = resolve_udid(args.udid)
except RuntimeError as e:
print(f"Error: {e}")
sys.exit(1)
sender = PushNotificationSender(udid=udid)
# Send notification
if args.payload:
# Custom payload mode
success = sender.send(args.bundle_id, args.payload)
else:
# Simple notification mode
success = sender.send_simple(
args.bundle_id,
title=args.title,
body=args.body,
badge=args.badge,
sound=not args.no_sound,
)
if success:
# Token-efficient output
output = "Push notification sent"
if args.test_name:
output += f" (test: {args.test_name})"
print(output)
if args.expected:
print(f"Expected: {args.expected}")
print()
print("Notification details:")
if args.title:
print(f" Title: {args.title}")
if args.body:
print(f" Body: {args.body}")
if args.badge:
print(f" Badge: {args.badge}")
print()
print("Verify notification handling:")
print("1. Check app log output: python scripts/log_monitor.py --app " + args.bundle_id)
print(
"2. Capture state: python scripts/app_state_capture.py --app-bundle-id "
+ args.bundle_id
)
else:
print("Failed to send push notification")
sys.exit(1)
if __name__ == "__main__":
main()