feat: review mobile issue payload before filing (Closes #833)
This commit is contained in:
1 parent
e125019f57
commit
59a2e4f616
11 files changed
+362
-34
No files matched your search
@@ -61,7 +61,8 @@ def test_mobile_capture_wires_distinct_create_and_start_intent_into_the_offline_
|
||||
assert '<button id="create-and-start-issue" type="submit">Create & start</button>' in html
|
||||
assert '<script src="static/create-and-start.js"></script>' in html
|
||||
assert "const createAndStart = createCreateAndStart({" in dashboard
|
||||
assert "event.submitter?.id === 'create-and-start-issue'" in dashboard
|
||||
assert "event.submitter.id === 'create-and-start-issue'" in dashboard
|
||||
assert "review.intent === 'create-and-start'" in dashboard
|
||||
assert "createAndStart.complete(created)" in dashboard
|
||||
assert ".create-issue-actions" in css and "grid-template-columns" in css
|
||||
assert "BASE + 'static/create-and-start.js'" in worker
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE = Path(__file__).parents[1] / "frontend" / "issue-filing-review.js"
|
||||
|
||||
|
||||
def run_node(script: str) -> dict:
|
||||
completed = subprocess.run(
|
||||
["node", "-e", script], capture_output=True, text=True, check=True
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_review_requires_one_confirmation_and_preserves_the_reviewed_payload():
|
||||
script = f"""
|
||||
const createReview = require({json.dumps(str(MODULE))});
|
||||
function target() {{
|
||||
const listeners = {{}};
|
||||
return {{
|
||||
disabled:false, hidden:true, textContent:'', children:[],
|
||||
addEventListener:(name, fn)=>listeners[name]=fn,
|
||||
dispatch:(name, event={{}})=>listeners[name]?.({{preventDefault(){{}}, ...event}}),
|
||||
focus(){{this.focused=true;}}, replaceChildren(...items){{this.children=items;}},
|
||||
setAttribute(){{}}, appendChild(item){{this.children.push(item);}},
|
||||
}};
|
||||
}}
|
||||
const sheet=target(), confirm=target(), back=target(), trigger=target(), evidence=target();
|
||||
const documentRef={{
|
||||
createElement:tag=>({{tag, textContent:'', children:[], appendChild(item){{this.children.push(item);}}}}),
|
||||
addEventListener:()=>{{}},
|
||||
}};
|
||||
const admitted=[];
|
||||
const review=createReview({{
|
||||
sheet, confirmButton:confirm, backButton:back, evidenceList:evidence,
|
||||
repository:target(), intent:target(), title:target(), body:target(), metadata:target(),
|
||||
document:documentRef,
|
||||
onConfirm:async payload=>{{ admitted.push(payload); return {{ok:true}}; }},
|
||||
}});
|
||||
const payload={{
|
||||
draft:{{repository:'stackchain/dashboard',title:'Ship it',body:'Full note',labels:['P1'],
|
||||
milestone:{{title:'Sprint'}},dueDate:'2026-08-20',attachments:[
|
||||
{{filename:'first.png',note:'Before'}},{{filename:'second.png',note:'After'}}
|
||||
]}},
|
||||
intent:'create-and-start',
|
||||
}};
|
||||
review.open(payload, trigger);
|
||||
payload.draft.title='Mutated outside';
|
||||
confirm.dispatch('click');
|
||||
confirm.dispatch('click');
|
||||
setImmediate(()=>process.stdout.write(JSON.stringify({{
|
||||
admissions:admitted.length,
|
||||
reviewedTitle:admitted[0].draft.title,
|
||||
evidence:evidence.children.map(item=>item.textContent),
|
||||
hidden:sheet.hidden,
|
||||
triggerFocused:trigger.focused || false,
|
||||
}})));
|
||||
"""
|
||||
|
||||
result = run_node(script)
|
||||
|
||||
assert result == {
|
||||
"admissions": 1,
|
||||
"reviewedTitle": "Ship it",
|
||||
"evidence": ["1. first.png — Before", "2. second.png — After"],
|
||||
"hidden": True,
|
||||
"triggerFocused": True,
|
||||
}
|
||||
|
||||
|
||||
def test_escape_returns_to_the_unchanged_issue_form():
|
||||
script = f"""
|
||||
const createReview = require({json.dumps(str(MODULE))});
|
||||
function target() {{
|
||||
const listeners={{}};
|
||||
return {{hidden:true,disabled:false,textContent:'',children:[],
|
||||
addEventListener:(name,fn)=>listeners[name]=fn,
|
||||
replaceChildren(...items){{this.children=items;}},
|
||||
focus(){{this.focused=true;}}, dispatch:(name,event)=>listeners[name]?.(event)}};
|
||||
}}
|
||||
const listeners={{}}, documentRef={{
|
||||
createElement:()=>({{textContent:''}}),
|
||||
addEventListener:(name,fn)=>listeners[name]=fn,
|
||||
dispatch:(name,event)=>listeners[name]?.(event),
|
||||
}};
|
||||
const sheet=target(), trigger=target();
|
||||
const review=createReview({{
|
||||
sheet,confirmButton:target(),backButton:target(),evidenceList:target(),
|
||||
repository:target(),intent:target(),title:target(),body:target(),metadata:target(),
|
||||
document:documentRef,onConfirm:async()=>{{}},
|
||||
}});
|
||||
const payload={{draft:{{repository:'o/r',title:'Original',body:'Note'}},intent:'create-and-assign'}};
|
||||
review.open(payload,trigger);
|
||||
documentRef.dispatch('keydown',{{key:'Escape',preventDefault(){{this.prevented=true;}}}});
|
||||
process.stdout.write(JSON.stringify({{hidden:sheet.hidden,focused:trigger.focused||false,title:payload.draft.title}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {"hidden": True, "focused": True, "title": "Original"}
|
||||
|
||||
|
||||
def test_create_actions_review_the_complete_payload_before_durable_admission():
|
||||
root = Path(__file__).parents[1]
|
||||
html = (root / "frontend" / "index.html").read_text()
|
||||
dashboard = (root / "frontend" / "dashboard.js").read_text()
|
||||
css = (root / "frontend" / "dashboard.css").read_text()
|
||||
bundle = (root / "src" / "frontend_bundle.py").read_text()
|
||||
|
||||
assert 'id="issue-filing-review" role="dialog" aria-modal="true"' in html
|
||||
assert 'id="issue-filing-review-repository"' in html
|
||||
assert 'id="issue-filing-review-intent"' in html
|
||||
assert 'id="issue-filing-review-title"' in html
|
||||
assert 'id="issue-filing-review-body"' in html
|
||||
assert 'id="issue-filing-review-evidence"' in html
|
||||
assert 'id="back-to-issue-edit"' in html
|
||||
assert 'id="confirm-issue-filing"' in html
|
||||
assert '<script src="static/issue-filing-review.js"></script>' in html
|
||||
|
||||
submit_handler = dashboard.split(
|
||||
"qs('#create-issue-form').addEventListener('submit'", 1
|
||||
)[1].split("qs('#close-issue-sheet').addEventListener", 1)[0]
|
||||
assert "filingReview.open" in submit_handler
|
||||
assert "issueOutbox.enqueueDurably" not in submit_handler
|
||||
assert "async function admitReviewedIssue" in dashboard
|
||||
assert "issueOutbox.enqueueDurably" in dashboard.split(
|
||||
"async function admitReviewedIssue", 1
|
||||
)[1].split("qs('#create-issue-form').addEventListener", 1)[0]
|
||||
|
||||
assert ".issue-filing-review" in css
|
||||
assert "max-height:100dvh" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
assert "env(safe-area-inset-bottom)" in css
|
||||
assert '"static/issue-filing-review.js"' in bundle
|
||||
|
||||
|
||||
def test_duplicate_acknowledgement_preserves_the_selected_filing_intent():
|
||||
dashboard = (Path(__file__).parents[1] / "frontend" / "dashboard.js").read_text()
|
||||
|
||||
assert "let pendingIssueFilingIntent = 'create-and-assign';" in dashboard
|
||||
assert "if (event.submitter) pendingIssueFilingIntent" in dashboard
|
||||
assert "const intent = pendingIssueFilingIntent;" in dashboard
|
||||
assert "pendingIssueFilingIntent = 'create-and-assign';" in dashboard.split(
|
||||
"async function admitReviewedIssue", 1
|
||||
)[1].split("qs('#create-issue-form').addEventListener", 1)[0]
|
||||
@@ -6333,9 +6333,12 @@ async def test_mobile_issue_capture_warns_before_queuing_a_possible_duplicate():
|
||||
assert "issueCapture.findDuplicates(draft)" in html
|
||||
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in html
|
||||
assert "issueCapture.acknowledgeDuplicates(captureDraft)" in html
|
||||
assert html.index("issueCapture.needsDuplicateAcknowledgement(captureDraft)") < html.index(
|
||||
"issueOutbox.enqueueDurably(durableDraft)"
|
||||
)
|
||||
submit_handler = html.split("qs('#create-issue-form').addEventListener('submit'", 1)[1].split(
|
||||
"qs('#close-issue-sheet').addEventListener", 1
|
||||
)[0]
|
||||
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in submit_handler
|
||||
assert "filingReview.open" in submit_handler
|
||||
assert "issueOutbox.enqueueDurably" not in submit_handler
|
||||
assert ".create-issue-duplicate-card" in html
|
||||
assert ".create-issue-duplicate-card a" in html and "min-height:44px" in html
|
||||
assert "overflow-wrap:anywhere" in html
|
||||
|
||||
@@ -840,6 +840,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/issue-evidence-review.js",
|
||||
"/dashboard/static/issue-evidence-editor.js",
|
||||
"/dashboard/static/issue-attachment.js",
|
||||
"/dashboard/static/issue-filing-review.js",
|
||||
"/dashboard/static/issue-sheet.js",
|
||||
"/dashboard/static/create-issue-sheet.js",
|
||||
"/dashboard/static/create-and-start.js",
|
||||
|
||||
@@ -192,7 +192,8 @@ async def test_follow_up_capture_offers_durable_create_and_next_flow():
|
||||
assert 'id="create-follow-up-next"' in html
|
||||
assert '>Create follow-up & next</button>' in html
|
||||
assert "updateFollowUp.stageSource(source.item)" in html
|
||||
assert "event.submitter?.id === 'create-follow-up-next'" in html
|
||||
assert "event.submitter.id === 'create-follow-up-next'" in html
|
||||
assert "review.intent === 'follow-up-and-next'" in html
|
||||
assert "queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId)" in html
|
||||
assert "advance: source => notificationReader.acceptReadAndNext(lastMyWork, source)" in html
|
||||
assert ".create-issue-actions button { min-height:44px;" in html
|
||||
Reference in new issue
Block a user