import json import subprocess from pathlib import Path MODULE = Path(__file__).parents[1] / "frontend" / "push-notifications.js" def run_scenario(script: str) -> dict: harness = r""" const createPushNotifications = require(__MODULE__); const state = { prompts:0, requests:[], subscriptions:[], text:'' }; const control = { checked:false, disabled:false, addEventListener:(_name, callback) => state.change = callback, }; const deadlineControl = { checked:false, disabled:false, addEventListener:(_name, callback) => state.deadlineChange = callback, }; const deadlineStatus = {set textContent(value) { state.deadlineText = value; }, get textContent() { return state.deadlineText; }}; const status = {set textContent(value) { state.text = value; }, get textContent() { return state.text; }}; const existing = {endpoint:'https://push.example/device', toJSON() { return {endpoint:this.endpoint, keys:{p256dh:'key',auth:'auth'}}; }}; const registration = {pushManager:{ getSubscription: async () => state.current || null, subscribe: async options => { state.subscriptions.push(options); state.current=existing; return existing; }, }}; const feature = createPushNotifications({ control, status, deadlineControl, deadlineStatus, notification: {permission:'default', requestPermission:async () => { state.prompts += 1; return state.permission || 'granted'; }}, serviceWorker: {ready:Promise.resolve(registration)}, fetchJson: async (url, options={}) => { state.requests.push([url,options.method || 'GET',options.body || '']); return state.server || {available:true,subscribed:false,public_key:'AQID'}; }, }); (async () => { __SCENARIO__ })().catch(error => { console.error(error); process.exit(1); }); """.replace("__MODULE__", json.dumps(str(MODULE))).replace("__SCENARIO__", script) completed = subprocess.run( ["node", "-e", harness], capture_output=True, check=True, text=True ) return json.loads(completed.stdout) def test_initialization_reports_availability_without_prompting_for_permission(): result = run_scenario("await feature.init(); process.stdout.write(JSON.stringify(state));") assert result["prompts"] == 0 assert result["requests"] == [["api/v1/push-subscription", "GET", ""]] assert result["text"] == "New update notifications are off for this device." def test_user_gesture_subscribes_device_and_reports_enabled_state(): result = run_scenario(""" await feature.init(); control.checked = true; await state.change(); process.stdout.write(JSON.stringify(state)); """) assert result["prompts"] == 1 assert result["subscriptions"][0]["userVisibleOnly"] is True assert result["requests"][-1][0:2] == ["api/v1/push-subscription", "PUT"] assert json.loads(result["requests"][-1][2])["endpoint"] == "https://push.example/device" assert result["text"] == "New update notifications enabled for this device." def test_deadline_opt_in_reuses_subscription_and_sends_local_timezone_without_second_prompt(): result = run_scenario(""" state.current = existing; state.server = {available:true,subscribed:true,deadline_enabled:false,public_key:'AQID'}; await feature.init(); deadlineControl.checked = true; await state.deadlineChange(); process.stdout.write(JSON.stringify(state)); """) assert result["prompts"] == 0 assert result["subscriptions"] == [] assert result["requests"][-1][0:2] == ["api/v1/push-subscription/deadlines", "PUT"] body = json.loads(result["requests"][-1][2]) assert body["enabled"] is True assert body["reminder_hour"] == 9 assert isinstance(body["timezone"], str) and body["timezone"] assert result["deadlineText"] == "Deadline reminders enabled for 9:00 local time." def test_mobile_dashboard_mounts_opt_in_and_precaches_its_controller(): root = MODULE.parents[1] html = (root / "frontend" / "index.html").read_text() dashboard = (root / "frontend" / "dashboard.js").read_text() worker = (root / "frontend" / "service-worker.js").read_text() css = (root / "frontend" / "dashboard.css").read_text() requirements = (root / "requirements.txt").read_text() readme = (root / "README.md").read_text() assert 'id="push-updates"' in html assert 'id="push-update-status"' in html assert 'id="push-deadlines"' in html assert 'id="push-deadline-status"' in html assert '' in html assert "createPushNotifications({" in dashboard assert "BASE + 'static/push-notifications.js'" in worker assert ".push-update-control" in css and "min-height:44px" in css assert "pywebpush==" in requirements assert "STACKCHAIN_VAPID_PUBLIC_KEY" in readme assert "STACKCHAIN_VAPID_PRIVATE_KEY" in readme