import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard INSTALL_APP = Path(__file__).resolve().parents[1] / "frontend" / "install-app.js" def run_install_scenario(scenario: str) -> dict: script = f""" const createInstallApp = require({json.dumps(str(INSTALL_APP))}); class FakeTarget {{ constructor() {{ this.listeners = {{}}; }} addEventListener(name, callback) {{ (this.listeners[name] ||= []).push(callback); }} dispatch(name, event = {{}}) {{ for (const callback of this.listeners[name] || []) callback(event); }} }} class FakeButton extends FakeTarget {{ constructor() {{ super(); this.disabled = false; }} click() {{ this.dispatch('click', {{currentTarget:this}}); }} }} (async () => {{ {scenario} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ completed = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert completed.returncode == 0, completed.stderr return json.loads(completed.stdout) def test_native_install_prompt_is_revealed_by_eligibility_and_invoked_once(): result = run_install_scenario( """ const windowTarget = new FakeTarget(); const card = {hidden:true}; const install = new FakeButton(); const dismiss = new FakeButton(); const guidance = {hidden:true}; const status = {textContent:''}; let promptCalls = 0; const app = createInstallApp({ window:windowTarget, card, installButton:install, dismissButton:dismiss, guidance, status, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>false, isIosSafari:()=>false, }); app.start(); windowTarget.dispatch('beforeinstallprompt', { preventDefault(){}, prompt:async () => { promptCalls += 1; }, userChoice:Promise.resolve({outcome:'accepted'}), }); const shown = !card.hidden; install.click(); await new Promise(resolve => setImmediate(resolve)); install.click(); await new Promise(resolve => setImmediate(resolve)); process.stdout.write(JSON.stringify({shown, promptCalls, hidden:card.hidden, status:status.textContent})); """ ) assert result == { "shown": True, "promptCalls": 1, "hidden": True, "status": "Stackchain was added to your device.", } def test_declined_native_install_hides_the_spent_choice_and_announces_it(): result = run_install_scenario( """ const windowTarget = new FakeTarget(); const card = {hidden:true}; const install = new FakeButton(); const status = {textContent:''}; const app = createInstallApp({ window:windowTarget, card, installButton:install, dismissButton:new FakeButton(), guidance:{hidden:true}, status, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>false, isIosSafari:()=>false, }); app.start(); windowTarget.dispatch('beforeinstallprompt', { preventDefault(){}, prompt:async()=>{}, userChoice:Promise.resolve({outcome:'dismissed'}), }); install.click(); await new Promise(resolve => setImmediate(resolve)); process.stdout.write(JSON.stringify({hidden:card.hidden, disabled:install.disabled, status:status.textContent})); """ ) assert result == { "hidden": True, "disabled": False, "status": "Installation was not completed.", } def test_ios_safari_gets_manual_add_to_home_screen_guidance(): result = run_install_scenario( """ const windowTarget = new FakeTarget(); const card = {hidden:true}; const install = new FakeButton(); install.hidden = false; const dismiss = new FakeButton(); const guidance = {hidden:true}; const status = {textContent:''}; const app = createInstallApp({ window:windowTarget, card, installButton:install, dismissButton:dismiss, guidance, status, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>false, isIosSafari:()=>true, }); app.start(); process.stdout.write(JSON.stringify({cardHidden:card.hidden, installHidden:install.hidden, guidanceHidden:guidance.hidden})); """ ) assert result == { "cardHidden": False, "installHidden": True, "guidanceHidden": False, } def test_standalone_launch_never_promotes_installation(): result = run_install_scenario( """ const windowTarget = new FakeTarget(); const card = {hidden:true}; const app = createInstallApp({ window:windowTarget, card, installButton:new FakeButton(), dismissButton:new FakeButton(), guidance:{hidden:true}, status:{textContent:''}, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>true, isIosSafari:()=>true, }); app.start(); windowTarget.dispatch('beforeinstallprompt', {preventDefault(){}, prompt:async()=>{}, userChoice:Promise.resolve({outcome:'accepted'})}); process.stdout.write(JSON.stringify({hidden:card.hidden})); """ ) assert result == {"hidden": True} def test_dismissal_is_remembered_and_suppresses_later_promotion(): result = run_install_scenario( """ const values = new Map(); const storage = {getItem:key => values.get(key) || null, setItem:(key, value) => values.set(key, value)}; const firstWindow = new FakeTarget(); const firstCard = {hidden:true}; const firstDismiss = new FakeButton(); const first = createInstallApp({ window:firstWindow, card:firstCard, installButton:new FakeButton(), dismissButton:firstDismiss, guidance:{hidden:true}, status:{textContent:''}, storage, isStandalone:()=>false, isIosSafari:()=>true, }); first.start(); firstDismiss.click(); const secondCard = {hidden:true}; const second = createInstallApp({ window:new FakeTarget(), card:secondCard, installButton:new FakeButton(), dismissButton:new FakeButton(), guidance:{hidden:true}, status:{textContent:''}, storage, isStandalone:()=>false, isIosSafari:()=>true, }); second.start(); process.stdout.write(JSON.stringify({firstHidden:firstCard.hidden, remembered:values.size, secondHidden:secondCard.hidden})); """ ) assert result == {"firstHidden": True, "remembered": 1, "secondHidden": True} def test_setup_api_reports_install_state_without_prompting(): result = run_install_scenario( """ const windowTarget = new FakeTarget(); const app = createInstallApp({ window:windowTarget, card:{hidden:true}, installButton:new FakeButton(), dismissButton:new FakeButton(), guidance:{hidden:true}, status:{textContent:''}, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>false, isIosSafari:()=>false, }); app.start(); const before = app.state(); windowTarget.dispatch('beforeinstallprompt', {preventDefault(){}, prompt:async()=>{}, userChoice:Promise.resolve({outcome:'accepted'})}); const after = app.state(); process.stdout.write(JSON.stringify({before, after})); """ ) assert result == { "before": {"state": "unavailable", "detail": "Installation is available from a supported browser."}, "after": {"state": "incomplete", "detail": "Stackchain is ready to install."}, } def test_captured_install_prompt_remains_available_until_setup_opens(): result = run_install_scenario( """ let prevented = 0; const prompt = {preventDefault(){prevented += 1;}, prompt:async()=>{}, userChoice:Promise.resolve({outcome:'accepted'})}; const card = {hidden:true}; const app = createInstallApp({ window:new FakeTarget(), initialPrompt:prompt, card, installButton:new FakeButton(), dismissButton:new FakeButton(), guidance:{hidden:true}, status:{textContent:''}, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>false, isIosSafari:()=>false, }); app.start(); process.stdout.write(JSON.stringify({state:app.state(), shown:!card.hidden, prevented})); """ ) assert result == { "state": {"state": "incomplete", "detail": "Stackchain is ready to install."}, "shown": True, "prevented": 1, } def test_appinstalled_hides_an_visible_native_promotion(): result = run_install_scenario( """ const windowTarget = new FakeTarget(); const card = {hidden:true}; const app = createInstallApp({ window:windowTarget, card, installButton:new FakeButton(), dismissButton:new FakeButton(), guidance:{hidden:true}, status:{textContent:''}, storage:{getItem:()=>null, setItem:()=>{}}, isStandalone:()=>false, isIosSafari:()=>false, }); app.start(); windowTarget.dispatch('beforeinstallprompt', {preventDefault(){}, prompt:async()=>{}, userChoice:Promise.resolve({outcome:'accepted'})}); const shown = !card.hidden; windowTarget.dispatch('appinstalled'); process.stdout.write(JSON.stringify({shown, hidden:card.hidden})); """ ) assert result == {"shown": True, "hidden": True} @pytest.mark.anyio async def test_dashboard_renders_and_wires_touch_safe_install_promotion(): html = await dashboard() assert 'id="install-app-card"' in html assert 'id="install-app"' in html assert 'id="dismiss-install-app"' in html assert 'id="install-app-guidance"' in html assert 'Share, then choose Add to Home Screen' in html assert '.install-app-card button { min-height:44px;' in html assert '' in html assert 'createInstallApp({' in html assert "window.matchMedia('(display-mode: standalone)').matches" in html assert "navigator.standalone === true" in html assert "isIosSafari" in html