import json import subprocess from pathlib import Path MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js" LIVE_SNAPSHOT = Path(__file__).parents[1] / "frontend" / "progressive-live-snapshot.js" MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" INDEX = Path(__file__).parents[1] / "frontend" / "index.html" CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css" def test_progressive_live_snapshot_coalesces_concurrent_cold_launch_consumers(): harness = f""" const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); let calls=0, resolveRequest; const broker=createProgressiveLiveSnapshot({{fetchSnapshot:()=>{{ calls += 1; return new Promise(resolve=>{{resolveRequest=resolve;}}); }}}}); (async()=>{{ const myWork=broker.acquire(); const humanGates=broker.acquire(); await Promise.resolve(); const callsWhilePending=calls; const snapshot={{context:{{user:{{id:7,login:'timmy'}}}},events:[],notifications:[]}}; resolveRequest(snapshot); const [first,second]=await Promise.all([myWork,humanGates]); const cached=await broker.acquire(); process.stdout.write(JSON.stringify({{ callsWhilePending,calls,same:first===second && second===cached, identity:broker.identity(),snapshot:broker.snapshot(), }})); }})().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 assert json.loads(result.stdout) == { "callsWhilePending": 1, "calls": 1, "same": True, "identity": {"login": "timmy", "accountKey": "7:timmy"}, "snapshot": { "context": {"user": {"id": 7, "login": "timmy"}}, "events": [], "notifications": [], }, } def test_progressive_live_snapshot_retries_after_a_failed_shared_flight(): harness = f""" const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); let calls=0; const snapshot={{context:{{user:{{login:'timmy'}}}},events:[],notifications:[]}}; const broker=createProgressiveLiveSnapshot({{fetchSnapshot:async()=>{{ calls += 1; if(calls===1) throw new Error('temporary outage'); return snapshot; }}}}); (async()=>{{ const first=await Promise.allSettled([broker.acquire(),broker.acquire()]); const recovered=await broker.acquire(); process.stdout.write(JSON.stringify({{ calls,first:first.map(result=>result.status),recovered:recovered===snapshot, }})); }})().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 assert json.loads(result.stdout) == { "calls": 2, "first": ["rejected", "rejected"], "recovered": True, } 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 'data-progressive-work-index="0"' in state["rendered"] assert "href=" not 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": [], }} 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"} def test_progressive_my_work_retries_a_failed_snapshot_without_reloading(): harness = f""" const fs=require('fs'); const vm=require('vm'); const list={{innerHTML:''}}; const status={{textContent:''}}; const document={{ querySelector:selector=>selector==='#my-work-list'?list:selector==='#my-work-status'?status:null, querySelectorAll:()=>[], }}; let timer=null; let attempts=0; const context={{module:{{exports:{{}}}},exports:{{}},console,URL,URLSearchParams,AbortController,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(Path(__file__).parents[1] / 'frontend' / 'context-poller.js'))},'utf8'),context); context.createContextPoller=context.module.exports; context.module={{exports:{{}}}}; vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context); const flow=context.module.exports({{ document, fetchSnapshot:async()=>{{ attempts += 1; if(attempts===1) throw new Error('temporary outage'); return {{context:{{user:{{login:'timmy'}},issues:[{{ number:9,title:'Recovered assignment',repository:'stackchain/dashboard', assignees:['timmy'],url:'https://forge.example/issues/9', }}],pull_requests:[]}},events:[],notifications:[]}}; }}, pollerOptions:{{ setTimer:callback=>{{timer=callback; return 1;}}, clearTimer:()=>{{timer=null;}}, setDeadlineTimer:()=>2, clearDeadlineTimer:()=>{{}}, intervalMs:5, }}, }}); (async()=>{{ const first=await flow.start(); const reconnecting=status.textContent; if(!timer) throw new Error('retry was not scheduled'); timer(); await Promise.resolve(); await Promise.resolve(); await new Promise(resolve=>setImmediate(resolve)); console.log(JSON.stringify({{first,reconnecting,attempts,status:status.textContent,html:list.innerHTML}})); }})().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) == { "first": False, "reconnecting": "Assigned work is reconnecting…", "attempts": 2, "status": "1 assigned work item ready.", "html": '
', } def test_progressive_my_work_refreshes_with_revisions_and_coalesces_lifecycle_recovery(): harness = f""" const fs=require('fs'); const vm=require('vm'); const list={{innerHTML:''}}; const status={{textContent:''}}; const document={{ hidden:false, querySelector:selector=>selector==='#my-work-list'?list:selector==='#my-work-status'?status:null, querySelectorAll:()=>[], }}; const handlers={{}}; const removed=[]; const lifecycleTarget={{ addEventListener:(name,callback)=>{{handlers[name]=callback;}}, removeEventListener:(name,callback)=>{{if(handlers[name]===callback){{removed.push(name);delete handlers[name];}}}}, }}; let timer=null; let calls=[]; let resolveRefresh; const initial={{ context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}},events:[],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 updated={{...initial,context:{{user:{{login:'timmy'}},issues:[{{number:10,title:'New assignment',repository:'stackchain/dashboard',assignees:['timmy']}}],pull_requests:[]}}}}; const context={{module:{{exports:{{}}}},exports:{{}},console,URL,URLSearchParams,AbortController,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(Path(__file__).parents[1] / 'frontend' / 'context-poller.js'))},'utf8'),context); context.createContextPoller=context.module.exports; context.module={{exports:{{}}}}; vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context); const flow=context.module.exports({{ document,lifecycleTarget, fetchSnapshot:(revisions,options)=>{{ calls.push({{revisions,hasSignal:Boolean(options.signal)}}); if(calls.length===1)return Promise.resolve(initial); return new Promise(resolve=>{{resolveRefresh=()=>resolve(updated);}}); }}, pollerOptions:{{ setTimer:callback=>{{timer=callback;return 1;}},clearTimer:()=>{{timer=null;}}, setDeadlineTimer:()=>2,clearDeadlineTimer:()=>{{}},intervalMs:5, }}, }}); (async()=>{{ await flow.start(); const freshnessTimer=timer; freshnessTimer(); handlers.online(); handlers.visibilitychange(); await Promise.resolve(); const callsWhilePending=calls.length; resolveRefresh(); await Promise.resolve(); await new Promise(resolve=>setImmediate(resolve)); const handoff=flow.handoff(); flow.stop(); console.log(JSON.stringify({{ callsWhilePending,calls,latestTitle:handoff.liveSnapshot.context.issues[0].title, removed:removed.sort(),status:status.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 state["callsWhilePending"] == 2 assert state["calls"][0] == {"revisions": {}, "hasSignal": True} assert state["calls"][1] == { "revisions": { "context": "0123456789abcdef.1", "events": "fedcba9876543210.2", "notifications": "0011223344556677.3", }, "hasSignal": True, } assert state["latestTitle"] == "New assignment" assert state["removed"] == ["keydown", "online", "visibilitychange"] assert state["status"] == "1 assigned work item ready." def test_progressive_card_opens_safe_in_app_detail_and_hands_it_off_once(): harness = f""" const fs=require('fs'); const vm=require('vm'); function element(extra={{}}) {{ return Object.assign({{ hidden:false,textContent:'',attrs:{{}},listeners:{{}},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];}}, focus(){{this.focusCount += 1;}}, }},extra); }} const list=element({{innerHTML:''}}); const status=element(); const sheet=element({{hidden:true}}); const title=element(); const meta=element(); const reason=element(); const close=element(); const gitea=element({{hidden:true,href:''}}); const elements={{ '#my-work-list':list,'#my-work-status':status,'#progressive-work-detail':sheet, '#progressive-work-detail-title':title,'#progressive-work-detail-meta':meta, '#progressive-work-detail-reason':reason,'#close-progressive-work-detail':close, '#open-progressive-work-gitea':gitea, }}; const document={{querySelector:s=>elements[s]||null,querySelectorAll:()=>[]}}; 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(MODULE))},'utf8'),context); const flow=context.module.exports({{document,lifecycleTarget,fetchSnapshot:async()=>({{ user:{{login:'timmy'}},issues:[{{number:7,title:'',repository:'stackchain/dashboard',assignees:['timmy'],url:'https://forge.example/issues/7'}}],pull_requests:[] }})}}); (async()=>{{ await flow.start(); const trigger=element({{dataset:{{progressiveWorkIndex:'0'}},closest:()=>trigger}}); list.listeners.click({{target:trigger,preventDefault(){{}}}}); const opened={{sheetHidden:sheet.hidden,title:title.textContent,meta:meta.textContent,reason:reason.textContent,giteaHidden:gitea.hidden,giteaHref:gitea.href,html:list.innerHTML}}; lifecycleTarget.listeners.keydown({{key:'Escape',preventDefault(){{}}}}); list.listeners.click({{target:trigger,preventDefault(){{}}}}); const first=flow.handoff(); const second=flow.handoff(); console.log(JSON.stringify({{opened,closed:sheet.hidden,focusCount:trigger.focusCount,first,second}})); }})().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["opened"] == { "sheetHidden": False, "title": "", "meta": "stackchain/dashboard#7 · Issue", "reason": "Assigned to you", "giteaHidden": False, "giteaHref": "https://forge.example/issues/7", "html": '
', } assert state["closed"] is False assert state["focusCount"] == 1 assert state["first"]["openWork"] == { "kind": "issue", "key": "stackchain/dashboard#7", "number": 7, "repository": "stackchain/dashboard", } assert "openWork" not in state["second"] def test_progressive_detail_is_mobile_safe_and_adopted_by_hydrated_workspace(): html = INDEX.read_text() css = CSS.read_text() dashboard = DASHBOARD.read_text() assert 'id="progressive-work-detail" role="dialog" aria-modal="true"' in html assert 'aria-labelledby="progressive-work-detail-title" hidden' in html assert 'id="close-progressive-work-detail"' in html assert 'id="open-progressive-work-gitea"' in html assert ".progressive-work-detail[hidden] { display:none; }" in css assert "max-height:100dvh" in css assert "overflow-x:hidden" in css assert "env(safe-area-inset-top)" in css assert "env(safe-area-inset-bottom)" in css assert ".progressive-work-detail-panel button, .progressive-work-detail-panel .button-link" in css assert "min-height:44px" in css assert "@media(max-width:320px)" in css assert "progressiveWorkHandoff?.openWork" in dashboard assert "openRoutedWork(progressiveItem, null)" in dashboard assert "qs('#progressive-work-detail').hidden = true" in dashboard