1 parent
b458dbaa78
commit
31a07e4a46
5 files changed
+308
-4
No files matched your search
@@ -0,0 +1,212 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.views 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_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 <strong>Add to Home Screen</strong>' in html
|
||||
assert '.install-app-card button { min-height:44px;' in html
|
||||
assert '<script src="static/install-app.js"></script>' 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
|
||||
@@ -91,11 +91,11 @@ async function dispatchNotificationClick(route) {{
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_live_section_revisions_ship_in_a_new_shell_cache():
|
||||
def test_mobile_install_flow_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v23" in source
|
||||
assert "BASE + 'static/today-work.js'" in source
|
||||
assert "stackchain-dashboard-shell-v24" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
||||
|
||||
def test_background_sync_event_flushes_closed_app_issue_outbox_only_for_its_tag():
|
||||
@@ -207,6 +207,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/work-route.js",
|
||||
"/dashboard/static/context-poller.js",
|
||||
"/dashboard/static/mobile-task-dock.js",
|
||||
"/dashboard/static/install-app.js",
|
||||
"/dashboard/static/background-issue-sync.js",
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user