185 lines
9.9 KiB
Python
185 lines
9.9 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).parents[1]
|
|
MODULE = ROOT / "frontend" / "progressive-mobile-dock.js"
|
|
INDEX = ROOT / "frontend" / "index.html"
|
|
DASHBOARD = ROOT / "frontend" / "dashboard.js"
|
|
CSS = ROOT / "frontend" / "dashboard.css"
|
|
MY_WORK = ROOT / "frontend" / "my-work.js"
|
|
PROGRESSIVE_MY_WORK = ROOT / "frontend" / "progressive-my-work.js"
|
|
|
|
|
|
def test_progressive_mobile_dock_opens_work_and_truthful_queues_before_hydration():
|
|
harness = f"""
|
|
const createDock=require({json.dumps(str(MODULE))});
|
|
function element(dataset={{}}) {{
|
|
return {{dataset,attrs:{{}},listeners:{{}},open:false,textContent:'',focusCount:0,
|
|
addEventListener(name,cb){{this.listeners[name]=cb;}},
|
|
removeEventListener(name,cb){{if(this.listeners[name]===cb)delete this.listeners[name];}},
|
|
setAttribute(name,value){{this.attrs[name]=String(value);}},
|
|
removeAttribute(name){{delete this.attrs[name];}},
|
|
showModal(){{this.open=true;}},close(){{this.open=false;}},focus(){{this.focusCount+=1;}},
|
|
}};
|
|
}}
|
|
const work=element({{mobileTask:'work'}}); const queues=element({{mobileTask:'queues'}});
|
|
const sheet=element(); const close=element(); const next=element();
|
|
const rows=Object.fromEntries(['today','agenda','delivery','gate','attention','update','filed','later','draft','authored']
|
|
.map(name=>[name,element({{mobileQueue:name}})]));
|
|
const counts=Object.fromEntries(Object.keys(rows).map(name=>[name,element({{mobileQueueCount:name}})]));
|
|
const smalls=Object.fromEntries(Object.keys(rows).map(name=>[name,{{textContent:'source label'}}]));
|
|
Object.keys(rows).forEach(name=>rows[name].querySelector=selector=>selector==='small'?smalls[name]:null);
|
|
const document={{
|
|
querySelector(selector){{return {{
|
|
'#mobile-queue-sheet':sheet,'#close-mobile-queues':close,'#mobile-queue-next-action':next,
|
|
'[data-mobile-task="work"]':work,'[data-mobile-task="queues"]':queues,
|
|
}}[selector]||null;}},
|
|
querySelectorAll(selector){{
|
|
if(selector==='[data-mobile-queue]')return Object.values(rows);
|
|
if(selector==='[data-mobile-queue-count]')return Object.values(counts);
|
|
return [];
|
|
}},
|
|
}};
|
|
const calls=[];
|
|
const myWork={{
|
|
counts:()=>({{all:3,filed:1,authored:1,attention:1,update:0}}),
|
|
openFirst:()=>calls.push(['openFirst']),
|
|
selectQueue:(name,options)=>calls.push(['selectQueue',name,options]),
|
|
}};
|
|
const dock=createDock({{document,myWork}}); dock.start();
|
|
queues.listeners.click({{currentTarget:queues}});
|
|
const opened=sheet.open; const initialHandoff=dock.handoff();
|
|
rows.filed.listeners.click();
|
|
work.listeners.click({{currentTarget:work}});
|
|
const known={{filed:counts.filed.textContent,attention:counts.attention.textContent}};
|
|
const loading={{
|
|
deferred:{{today:smalls.today.textContent,delivery:smalls.delivery.textContent,gate:smalls.gate.textContent,later:smalls.later.textContent}},
|
|
unavailable:{{today:rows.today.attrs['data-unavailable'],delivery:rows.delivery.attrs['data-unavailable']}},
|
|
}};
|
|
dock.stop();
|
|
console.log(JSON.stringify({{
|
|
opened,initialHandoff,calls,loading,known,
|
|
restored:{{today:smalls.today.textContent,delivery:smalls.delivery.textContent}},
|
|
cleaned:!('data-unavailable' in rows.today.attrs) && !('data-progressive-loading' in rows.today.attrs),
|
|
listeners:{{work:Object.keys(work.listeners),queues:Object.keys(queues.listeners),filed:Object.keys(rows.filed.listeners)}},
|
|
}}));
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert json.loads(result.stdout) == {
|
|
"opened": True,
|
|
"initialHandoff": {"queueSheetOpen": True, "lastTask": "queues"},
|
|
"calls": [
|
|
["selectQueue", "filed", {"openFirst": True}],
|
|
["openFirst"],
|
|
],
|
|
"known": {"filed": "1", "attention": "1"},
|
|
"loading": {
|
|
"deferred": {
|
|
"today": "Still loading",
|
|
"delivery": "Still loading",
|
|
"gate": "Still loading",
|
|
"later": "Still loading",
|
|
},
|
|
"unavailable": {"today": "true", "delivery": "true"},
|
|
},
|
|
"restored": {"today": "source label", "delivery": "source label"},
|
|
"cleaned": True,
|
|
"listeners": {"work": [], "queues": [], "filed": []},
|
|
}
|
|
|
|
|
|
def test_progressive_mobile_dock_refreshes_counts_from_live_work_and_unsubscribes_on_handoff():
|
|
harness = f"""
|
|
const createDock=require({json.dumps(str(MODULE))});
|
|
function element(dataset={{}}) {{return {{dataset,textContent:'',listeners:{{}},attrs:{{}},open:false,
|
|
addEventListener(name,cb){{this.listeners[name]=cb;}},removeEventListener(name){{delete this.listeners[name];}},
|
|
setAttribute(name,value){{this.attrs[name]=String(value);}},removeAttribute(name){{delete this.attrs[name];}},
|
|
querySelector:()=>({{textContent:''}}),showModal(){{this.open=true;}},close(){{this.open=false;}},
|
|
}};}}
|
|
const work=element({{mobileTask:'work'}}),queues=element({{mobileTask:'queues'}}),sheet=element(),close=element(),next=element();
|
|
const filed=element({{mobileQueue:'filed'}}),filedCount=element({{mobileQueueCount:'filed'}});
|
|
const document={{querySelector:s=>({{
|
|
'[data-mobile-task="work"]':work,'[data-mobile-task="queues"]':queues,
|
|
'#mobile-queue-sheet':sheet,'#close-mobile-queues':close,'#mobile-queue-next-action':next,
|
|
}}[s]||null),querySelectorAll:s=>s==='[data-mobile-queue]'?[filed]:s==='[data-mobile-queue-count]'?[filedCount]:[]}};
|
|
let counts={{all:0,filed:0}}, subscriber=null, unsubscribed=false;
|
|
const myWork={{counts:()=>counts,subscribe(cb){{subscriber=cb;return()=>{{unsubscribed=true;subscriber=null;}};}},openFirst(){{}},selectQueue(){{}}}};
|
|
const dock=createDock({{document,myWork}}); dock.start();
|
|
const before=filedCount.textContent; counts={{all:2,filed:2}}; subscriber(); const after=filedCount.textContent;
|
|
dock.stop();
|
|
console.log(JSON.stringify({{before,after,unsubscribed,subscriber:subscriber===null}}));
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert json.loads(result.stdout) == {
|
|
"before": "0", "after": "2", "unsubscribed": True, "subscriber": True,
|
|
}
|
|
|
|
|
|
def test_progressive_mobile_dock_is_shipped_before_optional_workspace_hydration():
|
|
index = INDEX.read_text()
|
|
dashboard = DASHBOARD.read_text()
|
|
css = CSS.read_text()
|
|
|
|
assert '<script src="static/progressive-mobile-dock.js"></script>' in index
|
|
assert index.index('static/progressive-my-work.js') < index.index('static/progressive-mobile-dock.js')
|
|
assert index.index('static/progressive-mobile-dock.js') < index.index('static/dashboard.js')
|
|
assert "window.stackchainProgressiveMobileDock?.handoff?.()" in dashboard
|
|
assert "window.stackchainProgressiveMobileDock?.stop?.()" in dashboard
|
|
assert "mobileTaskDock.select(progressiveMobileDockHandoff.lastTask)" in dashboard
|
|
assert "progressiveMobileDockHandoff?.queueSheetOpen && !qs('#mobile-queue-sheet').open" in dashboard
|
|
assert '[data-progressive-loading="true"]::after' in css
|
|
assert "content:'Still loading · available after workspace starts'" in css
|
|
|
|
|
|
def test_progressive_my_work_exposes_counts_and_opens_the_first_selected_queue_item():
|
|
harness = f"""
|
|
const fs=require('fs'); const vm=require('vm');
|
|
function element(extra={{}}) {{return Object.assign({{
|
|
innerHTML:'',textContent:'',hidden:true,dataset:{{}},listeners:{{}},focusCount:0,
|
|
addEventListener(name,cb){{this.listeners[name]=cb;}},removeEventListener(){{}},
|
|
setAttribute(){{}},removeAttribute(){{}},focus(){{this.focusCount+=1;}},
|
|
}},extra);}}
|
|
const list=element(); const status=element(); const detail=element();
|
|
const title=element(); const meta=element(); const reason=element(); const close=element(); const link=element();
|
|
const elements={{'#my-work-list':list,'#my-work-status':status,'#progressive-work-detail':detail,
|
|
'#progressive-work-detail-title':title,'#progressive-work-detail-meta':meta,
|
|
'#progressive-work-detail-reason':reason,'#close-progressive-work-detail':close,
|
|
'#open-progressive-work-gitea':link}};
|
|
const buttons=['all','filed','authored','attention'].map(name=>element({{dataset:{{workFilter:name}}}}));
|
|
const document={{querySelector:s=>elements[s]||null,querySelectorAll:()=>buttons}};
|
|
const lifecycleTarget=element();
|
|
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(PROGRESSIVE_MY_WORK))},'utf8'),context);
|
|
const flow=context.module.exports({{document,lifecycleTarget,fetchSnapshot:async()=>({{
|
|
user:{{login:'timmy'}},
|
|
issues:[{{number:7,title:'Filed issue',repository:'stackchain/dashboard',work_reasons:['created_by_me'],url:'https://forge.example/issues/7'}}],
|
|
pull_requests:[{{number:8,title:'Authored PR',repository:'stackchain/dashboard',work_reasons:['authored_by_me'],url:'https://forge.example/pulls/8'}}],
|
|
}})}});
|
|
(async()=>{{
|
|
let updates=0; const unsubscribe=flow.subscribe(()=>{{updates+=1;}});
|
|
await flow.start(); const counts=flow.counts();
|
|
const selected=flow.selectQueue('filed',{{openFirst:true}}); const handoff=flow.handoff();
|
|
unsubscribe(); flow.selectQueue('authored');
|
|
console.log(JSON.stringify({{counts,updates,selected,detailHidden:detail.hidden,title:title.textContent,handoff}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
result = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
state = json.loads(result.stdout)
|
|
assert state["counts"] == {"all": 2, "filed": 1, "authored": 1, "attention": 0, "update": 0, "review": 0}
|
|
assert state["updates"] == 2
|
|
assert state["selected"] == "opened"
|
|
assert state["detailHidden"] is False
|
|
assert state["title"] == "Filed issue"
|
|
assert state["handoff"]["selectedFilter"] == "filed"
|
|
assert state["handoff"]["openWork"]["key"] == "stackchain/dashboard#7"
|