143 lines
5.6 KiB
Python
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
|