|
|
|
|
@ -28,8 +28,12 @@ const button = {dataset:{mobileTask:'new'}, addEventListener(name, callback){lis
|
|
|
|
|
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 field = name => ({value:'', focused:0, focus(){this.focused++;},
|
|
|
|
|
addEventListener(event, callback){listeners.set(name+':'+event, callback);},
|
|
|
|
|
removeEventListener(event, callback){if(listeners.get(name+':'+event)===callback) listeners.delete(name+':'+event);},
|
|
|
|
|
});
|
|
|
|
|
const title = field('title');
|
|
|
|
|
const body = field('body');
|
|
|
|
|
const heading = {textContent:''};
|
|
|
|
|
const status = {textContent:''};
|
|
|
|
|
const classes = new Set();
|
|
|
|
|
@ -43,15 +47,16 @@ 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 storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
|
|
|
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';
|
|
|
|
|
title.value='Production outage'; listeners.get('title:input')();
|
|
|
|
|
body.value='Investigate mobile reports'; listeners.get('body:input')();
|
|
|
|
|
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()]}));
|
|
|
|
|
console.log(JSON.stringify({open:classes.has('open'),focused:title.focused,heading:heading.textContent,status:status.textContent,items:stored.items,listeners:[...listeners.keys()],checkpoint:values.get('stackchain.progressive-capture.v1')||null}));
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
assert result == {
|
|
|
|
|
@ -66,7 +71,136 @@ console.log(JSON.stringify({open:classes.has('open'),focused:title.focused,headi
|
|
|
|
|
"body": "Investigate mobile reports",
|
|
|
|
|
"savedAt": 42,
|
|
|
|
|
}],
|
|
|
|
|
"listeners": ["new:click", "save:click"],
|
|
|
|
|
"listeners": ["new:click", "save:click", "title:input", "body:input"],
|
|
|
|
|
"checkpoint": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_failed_progressive_save_keeps_rendered_fields_and_checkpoint_for_retry():
|
|
|
|
|
result = run_capture(r"""
|
|
|
|
|
const listeners={};
|
|
|
|
|
const control=name=>({disabled:false,value:'',addEventListener(event,callback){listeners[name+':'+event]=callback;},removeEventListener(){},focus(){this.focused=true;}});
|
|
|
|
|
const button=control('new'); const save=control('save'); const file=control('file'); const cancel=control('cancel');
|
|
|
|
|
const title=control('title'); const body=control('body'); 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 values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
|
|
|
const flow=createProgressiveCapture({document,storage,getLogin:()=>'timmy',createCaptures:()=>({save(){throw new Error('Phone storage is full.');}})});
|
|
|
|
|
flow.start(); listeners['new:click']();
|
|
|
|
|
title.value='Keep title'; listeners['title:input'](); body.value='Keep note'; listeners['body:input']();
|
|
|
|
|
const saved=await listeners['save:click']();
|
|
|
|
|
console.log(JSON.stringify({saved,open:classes.has('open'),title:title.value,body:body.value,status:status.textContent,
|
|
|
|
|
checkpoint:JSON.parse(values.get('stackchain.progressive-capture.v1'))}));
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
assert result == {
|
|
|
|
|
"saved": False,
|
|
|
|
|
"open": True,
|
|
|
|
|
"title": "Keep title",
|
|
|
|
|
"body": "Keep note",
|
|
|
|
|
"status": "Phone storage is full.",
|
|
|
|
|
"checkpoint": {
|
|
|
|
|
"version": 1,
|
|
|
|
|
"items": [{
|
|
|
|
|
"ownerLogin": "timmy",
|
|
|
|
|
"title": "Keep title",
|
|
|
|
|
"body": "Keep note",
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_progressive_capture_restores_latest_checkpoint_only_for_its_account():
|
|
|
|
|
result = run_capture(r"""
|
|
|
|
|
const values = new Map();
|
|
|
|
|
const storage = {
|
|
|
|
|
getItem:key=>values.get(key)||null,
|
|
|
|
|
setItem:(key,value)=>values.set(key,value),
|
|
|
|
|
removeItem:key=>values.delete(key),
|
|
|
|
|
};
|
|
|
|
|
function mount(login) {
|
|
|
|
|
const listeners = new Map();
|
|
|
|
|
const control = name => ({disabled:false,value:'',
|
|
|
|
|
addEventListener(event, callback){listeners.set(name+':'+event, callback);},
|
|
|
|
|
removeEventListener(){},focus(){},
|
|
|
|
|
});
|
|
|
|
|
const button=control('new'); const save=control('save'); const file=control('file');
|
|
|
|
|
const cancel=control('cancel'); const title=control('title'); const body=control('body');
|
|
|
|
|
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,getLogin:()=>login,createCaptures:createUnfiledCaptures});
|
|
|
|
|
flow.start();
|
|
|
|
|
return {flow,listeners,title,body,status,button};
|
|
|
|
|
}
|
|
|
|
|
const first=mount('timmy'); first.listeners.get('new:click')();
|
|
|
|
|
first.title.value='Production outage'; first.listeners.get('title:input')?.();
|
|
|
|
|
first.body.value='Latest phone note'; first.listeners.get('body:input')?.();
|
|
|
|
|
const restored=mount('timmy'); restored.listeners.get('new:click')();
|
|
|
|
|
const other=mount('alexander'); other.listeners.get('new:click')();
|
|
|
|
|
other.title.value='Alexander note'; other.listeners.get('title:input')?.();
|
|
|
|
|
const timmyAgain=mount('timmy'); timmyAgain.listeners.get('new:click')();
|
|
|
|
|
console.log(JSON.stringify({
|
|
|
|
|
restored:{title:restored.title.value,body:restored.body.value,status:restored.status.textContent},
|
|
|
|
|
other:{title:other.title.value,body:other.body.value,status:other.status.textContent},
|
|
|
|
|
timmyAgain:{title:timmyAgain.title.value,body:timmyAgain.body.value},
|
|
|
|
|
checkpoint:JSON.parse(values.get('stackchain.progressive-capture.v1') || 'null'),
|
|
|
|
|
}));
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
assert result == {
|
|
|
|
|
"restored": {
|
|
|
|
|
"title": "Production outage",
|
|
|
|
|
"body": "Latest phone note",
|
|
|
|
|
"status": "Unfinished capture restored from this phone.",
|
|
|
|
|
},
|
|
|
|
|
"other": {"title": "Alexander note", "body": "", "status": ""},
|
|
|
|
|
"timmyAgain": {"title": "Production outage", "body": "Latest phone note"},
|
|
|
|
|
"checkpoint": {
|
|
|
|
|
"version": 1,
|
|
|
|
|
"items": [
|
|
|
|
|
{"ownerLogin": "timmy", "title": "Production outage", "body": "Latest phone note"},
|
|
|
|
|
{"ownerLogin": "alexander", "title": "Alexander note", "body": ""},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hydration_handoff_clears_checkpoint_only_after_explicit_completion():
|
|
|
|
|
result = run_capture(r"""
|
|
|
|
|
const listeners={};
|
|
|
|
|
const control=name=>({disabled:false,value:'',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=control('title'); const body=control('body'); const root={classList:{add(){},remove(){},contains:()=>true}};
|
|
|
|
|
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':{textContent:''}};
|
|
|
|
|
const document={querySelector:selector=>selector==='[data-mobile-task="new"]'?button:nodes[selector]};
|
|
|
|
|
const values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
|
|
|
const flow=createProgressiveCapture({document,storage,getLogin:()=>'timmy',createCaptures:createUnfiledCaptures});
|
|
|
|
|
flow.start(); title.value='Hand off'; listeners['title:input'](); body.value='Preserve me'; listeners['body:input']();
|
|
|
|
|
const handoff=flow.handoff();
|
|
|
|
|
const before=JSON.parse(values.get('stackchain.progressive-capture.v1'));
|
|
|
|
|
if (typeof handoff.complete === 'function') handoff.complete();
|
|
|
|
|
console.log(JSON.stringify({before,after:values.get('stackchain.progressive-capture.v1')||null}));
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
assert result == {
|
|
|
|
|
"before": {
|
|
|
|
|
"version": 1,
|
|
|
|
|
"items": [{
|
|
|
|
|
"ownerLogin": "timmy",
|
|
|
|
|
"title": "Hand off",
|
|
|
|
|
"body": "Preserve me",
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
"after": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -109,7 +243,7 @@ console.log(JSON.stringify({filed,requests,open:classes.has('open'),status:statu
|
|
|
|
|
"focused": 2,
|
|
|
|
|
"title": "Keep this",
|
|
|
|
|
"body": "Do not lose this note",
|
|
|
|
|
"before": ["cancel:click", "file:click", "new:click", "save:click"],
|
|
|
|
|
"before": ["cancel:click", "file:click", "new:click", "save:click", "title:input"],
|
|
|
|
|
"after": [],
|
|
|
|
|
"handoff": {"open": True, "title": "Keep this", "body": "Do not lose this note"},
|
|
|
|
|
"second": None,
|
|
|
|
|
@ -122,6 +256,7 @@ def test_hydrated_dashboard_adopts_open_progressive_capture_without_losing_field
|
|
|
|
|
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 "progressiveCaptureHandoff.complete?.();" in source
|
|
|
|
|
assert "await openCreateIssueSheet(false);" in source
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|