222 lines
10 KiB
Python
222 lines
10 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
|
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
|
|
|
|
|
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
|
harness = f"""
|
|
const fs=require('fs'); const vm=require('vm');
|
|
const buttons=[
|
|
{{dataset:{{workFilter:'all'}},attrs:{{}},addEventListener(n,cb){{this.cb=cb;}},setAttribute(n,v){{this.attrs[n]=v;}}}},
|
|
{{dataset:{{workFilter:'issue'}},attrs:{{}},addEventListener(n,cb){{this.cb=cb;}},setAttribute(n,v){{this.attrs[n]=v;}}}},
|
|
];
|
|
const list={{innerHTML:''}}; const status={{textContent:''}};
|
|
const document={{querySelector:s=>s==='#my-work-list'?list:s==='#my-work-status'?status:null,querySelectorAll:()=>buttons}};
|
|
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 createProgressiveMyWork=context.module.exports;
|
|
const flow=createProgressiveMyWork({{document,fetchSnapshot:async()=>({{
|
|
user:{{login:'timmy'}},issues:[{{number:7,title:'Fix mobile queue',repository:'stackchain/dashboard',assignees:['timmy'],url:'https://forge.example/issues/7'}}],pull_requests:[]
|
|
}})}});
|
|
(async()=>{{
|
|
await flow.start(); const rendered=list.innerHTML;
|
|
buttons[1].cb();
|
|
console.log(JSON.stringify({{status:status.textContent,rendered,pressed:buttons.map(b=>b.attrs['aria-pressed'])}}));
|
|
}})().catch(e=>{{console.error(e);process.exit(1);}});
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
state = json.loads(result.stdout)
|
|
assert state["status"] == "1 assigned work item ready."
|
|
assert "Fix mobile queue" in state["rendered"]
|
|
assert 'href="https://forge.example/issues/7"' in state["rendered"]
|
|
assert state["pressed"] == ["false", "true"]
|
|
|
|
|
|
def test_progressive_my_work_keeps_supported_queues_truthful_while_planning_loads():
|
|
harness = f"""
|
|
const fs=require('fs'); const vm=require('vm');
|
|
const names=['all','today','agenda','attention','filed','authored','issue','pull','review','update','later','draft'];
|
|
const counts=Object.fromEntries(names.map(name=>[name,{{textContent:'0'}}]));
|
|
const buttons=names.map(name=>({{
|
|
dataset:{{workFilter:name}},attrs:{{}},
|
|
addEventListener(_name,cb){{this.cb=cb;}},
|
|
removeEventListener(){{}},
|
|
setAttribute(key,value){{this.attrs[key]=value;}},
|
|
}}));
|
|
const list={{innerHTML:''}}; const status={{textContent:''}};
|
|
const document={{
|
|
querySelector(selector){{
|
|
if(selector==='#my-work-list')return list;
|
|
if(selector==='#my-work-status')return status;
|
|
const match=selector.match(/^\\[data-work-count="(.+)"\\]$/);
|
|
return match?counts[match[1]]:null;
|
|
}},
|
|
querySelectorAll:()=>buttons,
|
|
}};
|
|
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()=>({{
|
|
user:{{login:'timmy'}},
|
|
issues:[{{number:7,title:'Filed from phone',repository:'stackchain/dashboard',assignees:[],work_reasons:['created_by_me'],url:'https://forge.example/issues/7'}}],
|
|
pull_requests:[{{number:8,title:'Authored release',repository:'stackchain/dashboard',assignees:[],work_reasons:['authored_by_me'],url:'https://forge.example/pulls/8'}}],
|
|
}})}});
|
|
(async()=>{{
|
|
await flow.start();
|
|
buttons[names.indexOf('filed')].cb(); const filed=list.innerHTML;
|
|
buttons[names.indexOf('authored')].cb(); const authored=list.innerHTML;
|
|
buttons[names.indexOf('today')].cb(); const today=list.innerHTML;
|
|
console.log(JSON.stringify({{filed,authored,today,counts:Object.fromEntries(names.map(name=>[name,counts[name].textContent]))}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
state = json.loads(result.stdout)
|
|
assert "Filed from phone" in state["filed"]
|
|
assert "Authored release" in state["authored"]
|
|
assert "Today is still loading" in state["today"]
|
|
assert "No work" not in state["today"]
|
|
assert state["counts"] == {
|
|
"all": "2",
|
|
"today": "0",
|
|
"agenda": "0",
|
|
"attention": "0",
|
|
"filed": "1",
|
|
"authored": "1",
|
|
"issue": "1",
|
|
"pull": "1",
|
|
"review": "0",
|
|
"update": "0",
|
|
"later": "0",
|
|
"draft": "0",
|
|
}
|
|
|
|
|
|
def test_progressive_queue_selection_is_handed_to_the_hydrated_workspace():
|
|
harness = f"""
|
|
const fs=require('fs'); const vm=require('vm');
|
|
const buttons=['all','filed'].map(name=>({{
|
|
dataset:{{workFilter:name}},attrs:{{}},removed:false,
|
|
addEventListener(_name,cb){{this.cb=cb;}},
|
|
removeEventListener(_name,cb){{if(cb===this.cb)this.removed=true;}},
|
|
setAttribute(key,value){{this.attrs[key]=value;}},
|
|
}}));
|
|
const document={{
|
|
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
|
querySelectorAll:()=>buttons,
|
|
}};
|
|
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()=>({{user:{{login:'timmy'}},issues:[],pull_requests:[]}})}});
|
|
(async()=>{{
|
|
await flow.start(); buttons[1].cb();
|
|
const before=flow.handoff(); flow.stop(); const after=flow.handoff();
|
|
console.log(JSON.stringify({{before,after,removed:buttons.map(button=>button.removed)}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
state = json.loads(result.stdout)
|
|
assert state == {
|
|
"before": {"selectedFilter": "filed"},
|
|
"after": {"selectedFilter": "filed"},
|
|
"removed": [True, True],
|
|
}
|
|
|
|
dashboard = DASHBOARD.read_text()
|
|
assert "const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();" in dashboard
|
|
assert "progressiveWorkHandoff?.selectedFilter" in dashboard
|
|
|
|
|
|
def test_progressive_live_snapshot_is_handed_to_the_workspace_exactly_once():
|
|
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 snapshot={{
|
|
context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}},
|
|
events:[{{id:7}}], notifications:[],
|
|
revisions:{{context:'0123456789abcdef.1',events:'fedcba9876543210.2',notifications:'0011223344556677.3'}},
|
|
freshness:{{fresh_for_seconds:8,sections:{{
|
|
context:{{degraded:false,age_seconds:2}},events:{{degraded:false,age_seconds:2}},notifications:{{degraded:false,age_seconds:2}},
|
|
}}}},
|
|
}};
|
|
const flow=context.module.exports({{document,fetchSnapshot:async()=>snapshot}});
|
|
(async()=>{{
|
|
await flow.start();
|
|
const first=flow.handoff(); const second=flow.handoff();
|
|
console.log(JSON.stringify({{
|
|
firstSnapshot:first.liveSnapshot,
|
|
secondHasSnapshot:Object.prototype.hasOwnProperty.call(second,'liveSnapshot'),
|
|
}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
state = json.loads(result.stdout)
|
|
|
|
assert state == {"firstSnapshot": {
|
|
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
|
"events": [{"id": 7}],
|
|
"notifications": [],
|
|
"revisions": {
|
|
"context": "0123456789abcdef.1",
|
|
"events": "fedcba9876543210.2",
|
|
"notifications": "0011223344556677.3",
|
|
},
|
|
"freshness": {
|
|
"fresh_for_seconds": 8,
|
|
"sections": {
|
|
"context": {"degraded": False, "age_seconds": 2},
|
|
"events": {"degraded": False, "age_seconds": 2},
|
|
"notifications": {"degraded": False, "age_seconds": 2},
|
|
},
|
|
},
|
|
}, "secondHasSnapshot": False}
|
|
|
|
|
|
def test_progressive_inflight_snapshot_survives_workspace_handoff_and_stop():
|
|
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);
|
|
let resolveSnapshot;
|
|
const snapshot={{context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}},events:[],notifications:[]}};
|
|
const flow=context.module.exports({{document,fetchSnapshot:()=>new Promise(resolve=>{{resolveSnapshot=resolve;}})}});
|
|
(async()=>{{
|
|
const started=flow.start();
|
|
await Promise.resolve();
|
|
const handoff=flow.handoff();
|
|
flow.stop();
|
|
resolveSnapshot(snapshot);
|
|
const transferred=await handoff.liveSnapshotPromise;
|
|
await started;
|
|
console.log(JSON.stringify({{transferred}}));
|
|
}})().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) == {"transferred": {
|
|
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
|
"events": [],
|
|
"notifications": [],
|
|
}} |