stackchain-dashboard/tests/test_create_pull_frontend.py
timmy e5b9833096
All checks were successful
CI / lint (pull_request) Successful in 3m19s
CI / build-release (pull_request) Successful in 7s
CI / release-candidate (pull_request) Has been skipped
CI / browser-journey (pull_request) Successful in 5m16s
feat: create mobile pull requests from pushed branches (Closes #1362)
2026-08-24 19:31:16 +00:00

143 lines
5.6 KiB
Python

import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).parents[1]
MODULE = ROOT / "frontend" / "create-pull-sheet.js"
def run_node(scenario: str) -> dict:
script = f"""
const createPullSheet = require({json.dumps(str(MODULE))});
{scenario}
"""
result = subprocess.run(["node", "-e", script], text=True, capture_output=True, cwd=ROOT)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_create_pull_controller_loads_branches_and_defaults_base():
result = run_node("""
const calls=[];
const controller=createPullSheet.createController({
request:async (url) => {
calls.push(url);
return {default_branch:'main',branches:[
{name:'feature/mobile',sha:'abc1234'},
{name:'main',sha:'def5678'}
]};
},
storage:{getItem:()=>null,setItem:()=>{}},
login:'timmy'
});
controller.selectRepository('stackchain/api').then(value => {
process.stdout.write(JSON.stringify({value,calls,state:controller.state()}));
}).catch(error=>{console.error(error);process.exit(1)});
""")
assert result["calls"] == ["api/v1/repos/stackchain/api/pull-creation-options"]
assert result["state"]["base"] == "main"
assert result["state"]["head"] == "feature/mobile"
assert result["state"]["expected_head_sha"] == "abc1234"
def test_create_pull_controller_submits_idempotently_and_keeps_draft_on_failure():
result = run_node("""
const calls=[]; const saved=[]; let fail=true;
const controller=createPullSheet.createController({
request:async (url, options) => {
calls.push([url,options]);
if (!options) return {default_branch:'main',branches:[{name:'topic',sha:'abc1234'},{name:'main',sha:'def5678'}]};
if (fail) { fail=false; throw new Error('offline'); }
return {number:9,repository:'acme/app',head:{ref:'topic',sha:'abc1234'},base:{ref:'main'},draft:true};
},
storage:{getItem:()=>null,setItem:(key,value)=>saved.push([key,JSON.parse(value)])},
login:'timmy', createId:()=> 'stable-id'
});
(async()=>{
await controller.selectRepository('acme/app');
controller.update({title:'Ship it',body:'Context'});
let error=''; try { await controller.submit(); } catch (caught) { error=caught.message; }
const retained=controller.state();
const confirmed=await controller.submit();
process.stdout.write(JSON.stringify({calls,error,retained,confirmed,saved}));
})().catch(error=>{console.error(error);process.exit(1)});
""")
assert result["error"] == "offline"
assert result["retained"]["title"] == "Ship it"
posts = [call for call in result["calls"] if call[1]]
assert len(posts) == 2
assert posts[0][1]["headers"]["Idempotency-Key"] == "stable-id"
assert posts[1][1]["headers"]["Idempotency-Key"] == "stable-id"
assert posts[0][1]["body"]["expected_head_sha"] == "abc1234"
assert result["confirmed"]["number"] == 9
def test_create_pull_controller_restores_only_the_current_accounts_draft():
result = run_node("""
const values={
'stackchain.create-pull.v1.timmy':JSON.stringify({repository:'acme/app',head:'topic',base:'main',title:'Saved title',body:'Saved body',draft:false}),
'stackchain.create-pull.v1.alexander':JSON.stringify({repository:'private/repo',title:'Other account'})
};
const controller=createPullSheet.createController({
request:async()=>({}), storage:{getItem:key=>values[key] || null,setItem:()=>{}}, login:'Timmy'
});
process.stdout.write(JSON.stringify(controller.state()));
""")
assert result == {
"repository": "acme/app",
"branches": [],
"head": "topic",
"base": "main",
"expected_head_sha": "",
"title": "Saved title",
"body": "Saved body",
"draft": False,
}
def test_mobile_pull_creation_sheet_is_packaged_and_touch_safe():
html = (ROOT / "frontend" / "index.html").read_text()
css = (ROOT / "frontend" / "dashboard.css").read_text()
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
assert 'id="create-pull-sheet" role="dialog" aria-modal="true"' in html
assert 'id="create-pull-form"' in html
assert 'id="create-pull-repository"' in html
assert 'id="create-pull-head"' in html
assert 'id="create-pull-base"' in html
assert 'id="submit-create-pull"' in html
assert 'static/create-pull-sheet.js' in bundle
assert ".create-pull-panel button" in css
assert "min-height:44px" in css.replace(" ", "")
def test_create_pull_binding_renders_branch_receipt_and_created_pull():
result = run_node("""
const rendered=[]; const statuses=[]; const opened=[];
const controller={
selectRepository:async repository=>({repository,branches:[{name:'topic',sha:'abc1234'},{name:'main',sha:'def5678'}],head:'topic',base:'main',expected_head_sha:'abc1234'}),
update:values=>values,
submit:async()=>({number:12,repository:'acme/app',existing:false})
};
const binding=createPullSheet.createBinding({controller,
render:state=>rendered.push(state), status:value=>statuses.push(value),
onCreated:value=>opened.push(value)
});
(async()=>{
await binding.repositoryChanged('acme/app');
await binding.submit();
process.stdout.write(JSON.stringify({rendered,statuses,opened}));
})().catch(error=>{console.error(error);process.exit(1)});
""")
assert result["rendered"][0]["expected_head_sha"] == "abc1234"
assert result["statuses"][-1] == "Pull request #12 created."
assert result["opened"][0]["number"] == 12
def test_dashboard_wires_create_pull_inside_lazy_issue_capture_chunk():
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
module = MODULE.read_text()
assert "cp.b();" not in dashboard
assert "else cp.b();" in module