1 parent
a23276f0b8
commit
8bb8fb0cd9
11 files changed
+344
-3
No files matched your search
@@ -133,7 +133,11 @@ def test_my_work_has_a_small_blocking_bundle_before_optional_workspace_hydration
|
||||
|
||||
assert b"function buildMyWork" in work_core.runtime_bytes
|
||||
assert b"function createProgressiveMyWork" in work_core.runtime_bytes
|
||||
assert b"function createProgressiveCapture" in work_core.runtime_bytes
|
||||
assert b"function createUnfiledCaptures" in work_core.runtime_bytes
|
||||
assert b"function buildMyWork" not in today.runtime_bytes
|
||||
assert b"function createProgressiveCapture" not in today.runtime_bytes
|
||||
assert b"function createUnfiledCaptures" not in today.runtime_bytes
|
||||
assert b"loadWorkspace({document,window})" in build.runtime_bytes
|
||||
assert b"const workspaceLifecycle" not in build.runtime_bytes
|
||||
assert b"const workspaceLifecycle" in today.runtime_bytes
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
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 you’re 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"},
|
||||
}
|
||||
@@ -219,4 +219,28 @@ const flow=context.module.exports({{document,fetchSnapshot:()=>new Promise(resol
|
||||
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
||||
"events": [],
|
||||
"notifications": [],
|
||||
}}
|
||||
}}
|
||||
|
||||
|
||||
def test_progressive_capture_can_read_only_the_confirmed_snapshot_login():
|
||||
harness = f"""
|
||||
const fs=require('fs'); const vm=require('vm');
|
||||
const document={{
|
||||
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
||||
querySelectorAll:()=>[],
|
||||
}};
|
||||
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
||||
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
||||
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
||||
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
||||
const flow=context.module.exports({{document,fetchSnapshot:async()=>({{
|
||||
context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}}, events:[], notifications:[],
|
||||
}})}});
|
||||
(async()=>{{
|
||||
const before=flow.login(); await flow.start(); const after=flow.login(); flow.stop();
|
||||
console.log(JSON.stringify({{before,after,stopped:flow.login()}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
||||
|
||||
assert json.loads(result.stdout) == {"before": "", "after": "timmy", "stopped": "timmy"}
|
||||
@@ -1409,6 +1409,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/offline-today.js",
|
||||
"/dashboard/static/my-work.js",
|
||||
"/dashboard/static/progressive-my-work.js",
|
||||
"/dashboard/static/progressive-capture.js",
|
||||
"/dashboard/static/agenda-replan.js",
|
||||
"/dashboard/static/agenda-calendar.js",
|
||||
"/dashboard/static/protect-today.js",
|
||||
|
||||
Reference in new issue
Block a user