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_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