stackchain-dashboard/tests/test_progressive_capture.py
timmy 8bb8fb0cd9
All checks were successful
CI / lint (pull_request) Successful in 3m36s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 6m26s
CI / release-candidate (pull_request) Has been skipped
feat: enable progressive mobile capture
Closes #1407
2026-08-25 20:11:13 +00:00

165 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import subprocess
from pathlib import Path
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-capture.js"
UNFILED = Path(__file__).parents[1] / "frontend" / "unfiled-captures.js"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
def run_capture(scenario: str) -> dict:
harness = f"""
const createProgressiveCapture = require({json.dumps(str(MODULE))});
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
(async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}});
"""
completed = subprocess.run(
["node", "-e", harness], check=True, capture_output=True, text=True
)
return json.loads(completed.stdout)
def test_progressive_mobile_new_opens_and_saves_one_account_bound_draft():
result = run_capture(r"""
const listeners = new Map();
const button = {dataset:{mobileTask:'new'}, addEventListener(name, callback){listeners.set('new:'+name, callback);}, removeEventListener(name, callback){if(listeners.get('new:'+name)===callback) listeners.delete('new:'+name);}, setAttribute(){}, removeAttribute(){}};
const save = {disabled:false, addEventListener(name, callback){listeners.set('save:'+name, callback);}, removeEventListener(name, callback){if(listeners.get('save:'+name)===callback) listeners.delete('save:'+name);}};
const file = {disabled:false, addEventListener(){}, removeEventListener(){}};
const cancel = {addEventListener(){}, removeEventListener(){}};
const title = {value:'', focused:0, focus(){this.focused++;}};
const body = {value:''};
const heading = {textContent:''};
const status = {textContent:''};
const classes = new Set();
const root = {classList:{add:name=>classes.add(name), remove:name=>classes.delete(name), contains:name=>classes.has(name)}};
const nodes = {
'#create-issue-sheet':root, '#create-issue-title':title, '#create-issue-body':body,
'#create-issue-heading':heading, '#save-unfiled-issue':save, '#file-new-issue':file,
'#cancel-new-issue':cancel, '#my-work-action-status':status,
};
const document = {
querySelector(selector){return selector==='[data-mobile-task="new"]'?button:(nodes[selector]||null);},
};
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
const flow = createProgressiveCapture({document, storage, getLogin:()=> 'timmy', createCaptures:createUnfiledCaptures, createId:()=> 'draft-1', now:()=> 42});
flow.start();
listeners.get('new:click')({currentTarget:button});
title.value='Production outage'; body.value='Investigate mobile reports';
await listeners.get('save:click')();
await listeners.get('save:click')();
const stored=JSON.parse(values.get('stackchain.unfiled-issues.v1'));
console.log(JSON.stringify({open:classes.has('open'),focused:title.focused,heading:heading.textContent,status:status.textContent,items:stored.items,listeners:[...listeners.keys()]}));
""")
assert result == {
"open": False,
"focused": 1,
"heading": "Capture work",
"status": "Saved to Drafts. Choose a repository when youre ready to file it.",
"items": [{
"id": "draft-1",
"ownerLogin": "timmy",
"title": "Production outage",
"body": "Investigate mobile reports",
"savedAt": 42,
}],
"listeners": ["new:click", "save:click"],
}
def test_progressive_file_now_preserves_capture_on_failure_and_hands_it_off_once():
result = run_capture(r"""
const listeners = new Map();
const control = name => ({disabled:false,
addEventListener(event, callback){listeners.set(name+':'+event, callback);},
removeEventListener(event, callback){if(listeners.get(name+':'+event)===callback) listeners.delete(name+':'+event);},
focus(){this.focused=(this.focused||0)+1;},
});
const button=control('new'); const save=control('save'); const file=control('file'); const cancel=control('cancel');
const title=control('title'); title.value='Keep this'; const body={value:'Do not lose this note'};
const status={textContent:''}; const heading={textContent:''}; const classes=new Set();
const root={classList:{add:name=>classes.add(name),remove:name=>classes.delete(name),contains:name=>classes.has(name)}};
const nodes={'#create-issue-sheet':root,'#create-issue-title':title,'#create-issue-body':body,
'#create-issue-heading':heading,'#save-unfiled-issue':save,'#file-new-issue':file,
'#cancel-new-issue':cancel,'#my-work-action-status':status};
const document={querySelector:selector=>selector==='[data-mobile-task="new"]'?button:(nodes[selector]||null)};
let requests=0;
const flow=createProgressiveCapture({
document,storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',createCaptures:createUnfiledCaptures,
requestFullWorkspace:async()=>{requests++;throw new Error('offline');},
});
flow.start(); listeners.get('new:click')({currentTarget:button});
const filed=await listeners.get('file:click')();
const before=Array.from(listeners.keys()).sort();
const handoff=flow.handoff(); const second=flow.handoff();
console.log(JSON.stringify({filed,requests,open:classes.has('open'),status:status.textContent,
fileDisabled:file.disabled,focused:title.focused,title:title.value,body:body.value,before,
after:Array.from(listeners.keys()),handoff,second}));
""")
assert result == {
"filed": False,
"requests": 1,
"open": True,
"status": "Filing tools unavailable. Your capture is still editable; retry when connected.",
"fileDisabled": False,
"focused": 2,
"title": "Keep this",
"body": "Do not lose this note",
"before": ["cancel:click", "file:click", "new:click", "save:click"],
"after": [],
"handoff": {"open": True, "title": "Keep this", "body": "Do not lose this note"},
"second": None,
}
def test_hydrated_dashboard_adopts_open_progressive_capture_without_losing_fields():
source = DASHBOARD.read_text()
assert "const progressiveCaptureHandoff = window.stackchainProgressiveCapture?.handoff?.();" in source
assert "issueCapture.saveDraft({repository:'', labelIds:[]," in source
assert "title:progressiveCaptureHandoff.title, body:progressiveCaptureHandoff.body" in source
assert "await openCreateIssueSheet(false);" in source
def test_progressive_capture_is_mobile_only_and_hides_unhydrated_filing_controls():
source = MODULE.read_text()
css = CSS.read_text()
assert "window.matchMedia?.('(max-width: 600px)').matches === true" in source
assert "root?.classList.add('progressive-capture')" in source
assert "root?.classList.remove('progressive-capture')" in source
assert ".create-issue-sheet.progressive-capture .mobile-create-issue-nav" in css
assert ".create-issue-sheet.progressive-capture .create-issue-attachment" in css
assert ".create-issue-sheet.progressive-capture .create-issue-filing" in css
assert ".create-issue-sheet.progressive-capture #switch-to-create-pull" in css
def test_successful_filing_handoff_reenables_the_hydrated_file_control():
result = run_capture(r"""
const listeners={};
const control=name=>({disabled:false,addEventListener(event,callback){listeners[name+event]=callback;},removeEventListener(){},focus(){}});
const button=control('new'); const save=control('save'); const file=control('file'); const cancel=control('cancel');
const title={value:'Ready to file',focus(){}}; const body={value:'Context'}; const status={textContent:''};
const classes=new Set(); const root={classList:{add:name=>classes.add(name),remove:name=>classes.delete(name),contains:name=>classes.has(name)}};
const nodes={'#create-issue-sheet':root,'#create-issue-title':title,'#create-issue-body':body,
'#create-issue-heading':{textContent:''},'#save-unfiled-issue':save,'#file-new-issue':file,
'#cancel-new-issue':cancel,'#my-work-action-status':status};
const document={querySelector:selector=>selector==='[data-mobile-task="new"]'?button:nodes[selector]};
const flow=createProgressiveCapture({document,storage:{getItem:()=>null,setItem(){}},getLogin:()=>'timmy',
createCaptures:createUnfiledCaptures,requestFullWorkspace:async()=>true});
flow.start(); listeners.newclick({currentTarget:button});
const filed=await listeners.fileclick(); const before=file.disabled; const handoff=flow.handoff();
console.log(JSON.stringify({filed,before,after:file.disabled,handoff}));
""")
assert result == {
"filed": True,
"before": True,
"after": False,
"handoff": {"open": True, "title": "Ready to file", "body": "Context"},
}