import json import subprocess from pathlib import Path RECENT_WORK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-recent-work.js" INDEX = Path(__file__).resolve().parents[1] / "frontend" / "index.html" DASHBOARD = Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js" CSS = Path(__file__).resolve().parents[1] / "frontend" / "dashboard.css" def run_node(script: str) -> dict: result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_recent_work_is_account_scoped_deduplicated_and_bounded(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key, value) => values.set(key, value), removeItem:key => values.delete(key), }}; let login = ' Alice '; const recent = createRecentWork({{storage, getLogin:() => login, limit:5}}); for (let number=1; number<=6; number += 1) {{ recent.record({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}}); }} recent.record({{kind:'issue', repository:'stackchain/dashboard', number:3, title:'Issue 3 updated'}}); const alice = recent.items(); login = 'bob'; recent.record({{kind:'pull', repository:'stackchain/api', number:9, title:'Ship API'}}); const bob = recent.items(); login = ''; const anonymousRecord = recent.record({{kind:'issue', repository:'stackchain/dashboard', number:99, title:'Private'}}); const anonymous = recent.items(); process.stdout.write(JSON.stringify({{alice,bob,anonymousRecord,anonymous,keys:Array.from(values.keys()).sort()}})); """ payload = run_node(script) assert [item["number"] for item in payload["alice"]] == [3, 6, 5, 4, 2] assert payload["alice"][0] == { "kind": "issue", "repository": "stackchain/dashboard", "number": 3, "title": "Issue 3 updated", "route": "#/my-work/issue/stackchain/dashboard/3", } assert payload["bob"] == [ { "kind": "pull", "repository": "stackchain/api", "number": 9, "title": "Ship API", "route": "#/my-work/pull/stackchain/api/9", } ] assert payload["anonymousRecord"] is False assert payload["anonymous"] == [] assert payload["keys"] == [ "stackchain.mobile-recent-work.v1.alice", "stackchain.mobile-recent-work.v1.bob", ] def test_recent_work_renders_safe_rows_and_opens_the_selected_route(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); const values = new Map([ ['stackchain.mobile-recent-work.v1.alice', JSON.stringify([ {{kind:'issue',repository:'stackchain/dashboard',number:7,title:'Fix mobile queue',route:'#/wrong'}}, {{kind:'update',number:42,title:'Review release status'}}, {{kind:'pull',repository:'bad/repo/extra',number:1,title:'Unsafe'}}, {{kind:'issue',repository:'stackchain/dashboard',number:0,title:'Invalid'}}, ])], ]); function node(tag) {{ return {{tag,children:[],attributes:{{}},listeners:{{}},hidden:false,textContent:'', appendChild(child){{this.children.push(child);return child;}}, replaceChildren(...children){{this.children=children;}}, setAttribute(name,value){{this.attributes[name]=String(value);}}, addEventListener(name,callback){{this.listeners[name]=callback;}}, click(){{this.listeners.click?.();}}, }}; }} const list=node('div'); const section=node('section'); const opened=[]; const recent=createRecentWork({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}, getLogin:()=>'alice', document:{{createElement:node}}, list, section, openRoute:route=>opened.push(route), }}); const rendered=recent.render(); list.children[1].children[0].click(); process.stdout.write(JSON.stringify({{ rendered,hidden:section.hidden,rows:list.children.map(row=>{{const button=row.children[0];return ({{ label:button.attributes['aria-label'],route:button.attributes['data-recent-work-route'], primary:button.children[0].children[0].textContent, secondary:button.children[0].children[1].textContent, pin:row.children[1].attributes['data-recent-work-pin'], }});}}),opened, }})); """ payload = run_node(script) assert payload == { "rendered": 2, "hidden": False, "rows": [ { "label": "Open Fix mobile queue, issue stackchain/dashboard #7", "route": "#/my-work/issue/stackchain/dashboard/7", "primary": "Fix mobile queue", "secondary": "Issue · stackchain/dashboard #7", "pin": "pin", }, { "label": "Open Review release status, update #42", "route": "#/my-work/update/42", "primary": "Review release status", "secondary": "Update · #42", "pin": "pin", }, ], "opened": ["#/my-work/update/42"], } def test_recent_work_records_offline_first_then_merges_the_server_snapshot(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); (async()=>{{ const values = new Map(); const calls=[]; const status={{textContent:''}}; const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}; const remote={{kind:'pull',repository:'stackchain/api',number:9,title:'Ship API',route:'#/my-work/pull/stackchain/api/9'}}; const local={{kind:'issue',repository:'stackchain/dashboard',number:7,title:'Fix queue',route:'#/my-work/issue/stackchain/dashboard/7'}}; const recent=createRecentWork({{ storage,getLogin:()=>'alice',status,debounceMs:99999, fetchJson:async (url, options={{}})=>{{ calls.push([url,options.method||'GET']); return options.method==='POST' ? {{items:[local,remote]}} : {{items:[remote]}}; }}, }}); recent.record(local); const immediate={{items:recent.items(),status:status.textContent,state:recent.state()}}; await recent.sync(); process.stdout.write(JSON.stringify({{immediate,settled:recent.items(),status:status.textContent,calls}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ payload = run_node(script) assert payload["immediate"]["items"][0]["number"] == 7 assert payload["immediate"]["status"] == "Sync pending." assert payload["immediate"]["state"]["pending"] is True assert [item["number"] for item in payload["settled"]] == [7, 9] assert payload["status"] == "" assert payload["calls"] == [["api/v1/recent-work", "POST"]] def test_recent_work_drains_a_newer_same_route_generation_after_an_inflight_response(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); (async()=>{{ const values = new Map(); const calls=[]; const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}; const oldItem={{kind:'issue',repository:'stackchain/dashboard',number:7,title:'Old title'}}; const newItem={{kind:'issue',repository:'stackchain/dashboard',number:7,title:'New title'}}; let releaseFirst; const firstResponse=new Promise(resolve=>{{releaseFirst=resolve;}}); const recent=createRecentWork({{ storage,getLogin:()=>'alice',debounceMs:99999, fetchJson:async (_url, options)=>{{ const sent=JSON.parse(options.body); calls.push(sent.title); if (calls.length === 1) return firstResponse; return {{items:[{{...newItem,route:'#/my-work/issue/stackchain/dashboard/7'}}],pinned:[]}}; }}, }}); recent.record(oldItem); const syncing=recent.sync(); await Promise.resolve(); recent.record(newItem); releaseFirst({{items:[{{...oldItem,route:'#/my-work/issue/stackchain/dashboard/7'}}],pinned:[]}}); await syncing; process.stdout.write(JSON.stringify({{calls,items:recent.items(),state:recent.state()}})); process.exit(0); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ payload = run_node(script) assert payload["calls"] == ["Old title", "New title"] assert payload["items"][0]["title"] == "New title" assert payload["state"] == {"pending": False, "pendingCount": 0} def test_recent_work_retries_a_transient_sync_failure_without_a_lifecycle_event(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); (async()=>{{ const values=new Map(); const timers=[]; let calls=0; const recent=createRecentWork({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}, getLogin:()=>'alice',debounceMs:99999,retryMs:25, setTimeout:(callback,delay)=>{{const timer={{callback,delay,cleared:false}};timers.push(timer);return timer;}}, clearTimeout:timer=>{{timer.cleared=true;}}, fetchJson:async (_url,options)=>{{ calls += 1; if (calls === 1) throw new Error('temporary outage'); return {{items:[JSON.parse(options.body)],pinned:[]}}; }}, }}); recent.record({{kind:'issue',repository:'stackchain/dashboard',number:7,title:'Keep me'}}); await recent.sync(); const afterFailure={{calls,state:recent.state(),active:timers.filter(timer=>!timer.cleared).map(timer=>timer.delay)}}; const retryTimer=timers.find(timer=>!timer.cleared); retryTimer?.callback(); await new Promise(resolve=>setImmediate(resolve)); const settled={{calls,state:recent.state(),active:timers.filter(timer=>!timer.cleared).length}}; process.stdout.write(JSON.stringify({{afterFailure,settled}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ payload = run_node(script) assert payload["afterFailure"] == { "calls": 1, "state": {"pending": True, "pendingCount": 1}, "active": [25], } assert payload["settled"] == { "calls": 2, "state": {"pending": False, "pendingCount": 0}, "active": 0, } def test_recent_work_drains_a_newer_pin_generation_after_an_inflight_response(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); (async()=>{{ const values=new Map(); const calls=[]; const oldItem={{kind:'issue',repository:'stackchain/dashboard',number:7,title:'Old pin'}}; const newItem={{kind:'issue',repository:'stackchain/dashboard',number:7,title:'New pin'}}; const route='#/my-work/issue/stackchain/dashboard/7'; let releaseFirst; const firstResponse=new Promise(resolve=>{{releaseFirst=resolve;}}); const recent=createRecentWork({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}, getLogin:()=>'alice',debounceMs:99999, fetchJson:async (_url,options)=>{{ const sent=JSON.parse(options.body); calls.push(sent.title); if (calls.length === 1) return firstResponse; return {{items:[],pinned:[{{...newItem,route}}]}}; }}, }}); recent.pin(oldItem); const syncing=recent.sync(); await Promise.resolve(); recent.pin(newItem); releaseFirst({{items:[],pinned:[{{...oldItem,route}}]}}); await syncing; process.stdout.write(JSON.stringify({{calls,pinned:recent.pinned(),state:recent.state()}})); process.exit(0); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ payload = run_node(script) assert payload["calls"] == ["Old pin", "New pin"] assert payload["pinned"][0]["title"] == "New pin" assert payload["state"] == {"pending": False, "pendingCount": 0} def test_recent_work_pins_offline_first_syncs_and_renders_separate_touch_actions(): script = f""" const createRecentWork = require({json.dumps(str(RECENT_WORK))}); (async()=>{{ const item={{kind:'issue',repository:'stackchain/dashboard',number:1477,title:'Pin frequent work',route:'#/my-work/issue/stackchain/dashboard/1477'}}; const storageKey='stackchain.mobile-recent-work.v1.alice'; const values=new Map([[storageKey,JSON.stringify({{items:[item],pinned:[],pending:[],pinOps:[]}})]]); const status={{textContent:''}}; const calls=[]; const opened=[]; function node(tag) {{ return {{tag,children:[],attributes:{{}},listeners:{{}},hidden:false,textContent:'', appendChild(child){{this.children.push(child);return child;}}, replaceChildren(...children){{this.children=children;}}, setAttribute(name,value){{this.attributes[name]=String(value);}}, addEventListener(name,callback){{this.listeners[name]=callback;}}, click(){{this.listeners.click?.();}}, focus(){{this.focused=true;}}, }}; }} const recentList=node('div'); const recentSection=node('section'); const pinnedList=node('div'); const pinnedSection=node('section'); let remote={{items:[item],pinned:[]}}; const recent=createRecentWork({{ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}, getLogin:()=>'alice',status,debounceMs:99999,document:{{createElement:node}}, list:recentList,section:recentSection,pinnedList,pinnedSection, openRoute:route=>opened.push(route), fetchJson:async (url,options={{}})=>{{ calls.push([url,options.method||'GET',JSON.parse(options.body||'null')]); if (options.method==='PUT') remote={{items:[item],pinned:[item]}}; if (options.method==='DELETE') remote={{items:[item],pinned:[]}}; return remote; }}, }}); const pinnedImmediately=recent.pin(item); recent.render(); const immediate={{pinned:recent.pinned(),status:status.textContent,recentHidden:recentSection.hidden,pinnedHidden:pinnedSection.hidden, recentActions:recentList.children[0].children.map(child=>child.attributes), pinnedActions:pinnedList.children[0].children.map(child=>child.attributes)}}; await recent.sync(); pinnedList.children[0].children[0].click(); const unpinnedImmediately=recent.unpin(item.route); await recent.sync(); process.stdout.write(JSON.stringify({{pinnedImmediately,immediate,opened,unpinnedImmediately,settled:recent.pinned(),calls}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ payload = run_node(script) assert payload["pinnedImmediately"] is True assert payload["immediate"]["pinned"][0]["number"] == 1477 assert payload["immediate"]["status"] == "Sync pending." assert payload["immediate"]["recentHidden"] is False assert payload["immediate"]["pinnedHidden"] is False assert payload["immediate"]["recentActions"][0]["data-recent-work-route"].endswith("/1477") assert payload["immediate"]["recentActions"][1]["data-recent-work-pin"] == "pin" assert payload["immediate"]["pinnedActions"][1]["data-recent-work-pin"] == "unpin" assert payload["opened"] == ["#/my-work/issue/stackchain/dashboard/1477"] assert payload["unpinnedImmediately"] is True assert payload["settled"] == [] assert payload["calls"] == [ ["api/v1/recent-work/pin", "PUT", payload["immediate"]["pinned"][0]], [ "api/v1/recent-work/pin", "DELETE", {"route": "#/my-work/issue/stackchain/dashboard/1477"}, ], ] def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes(): html = INDEX.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() assert 'id="mobile-recent-work"' in html assert 'id="mobile-recent-work-list"' in html assert 'id="mobile-pinned-work"' in html assert 'id="mobile-pinned-work-list"' in html assert 'id="mobile-recent-work-status" role="status" aria-live="polite"' in html assert '' in html assert "createMobileRecentWork({" in dashboard assert "fetchJson:fetchReviewJson" in dashboard assert "status:qs('#mobile-recent-work-status')" in dashboard assert "pinnedList:qs('#mobile-pinned-work-list')" in dashboard assert "pinnedSection:qs('#mobile-pinned-work')" in dashboard assert "mobileRecentWork.startLifecycle({window, document})" in dashboard assert "void mobileRecentWork.load();" in dashboard assert "mobileRecentWork.record(item)" in dashboard assert "mobileRecentWork.render()" in dashboard assert "workRoute.sync()" in dashboard assert "[data-recent-work-route]" in css assert "[data-recent-work-pin]" in css assert "min-height:44px" in css assert "min-width:0" in css