import json import subprocess from pathlib import Path SEARCH_BATCH_PLAN = Path(__file__).parents[1] / "frontend" / "search-batch-plan.js" HTML = Path(__file__).parents[1] / "frontend" / "index.html" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css" def run_node(script): return json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) def test_search_batch_selection_keeps_open_issues_across_pages_and_rejects_ineligible_results(): script = f""" const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))}); const states=[]; const flow=createSearchBatchPlan({{onChange:state=>states.push(state)}}); const first={{kind:'issue',state:'open',repository:'stackchain/dashboard',number:809,title:'First'}}; const second={{kind:'issue',state:'open',repository:'stackchain/api',number:17,title:'Second'}}; const pull={{kind:'pull',state:'open',repository:'stackchain/dashboard',number:810}}; const closed={{kind:'issue',state:'closed',repository:'stackchain/dashboard',number:808}}; flow.start(); const outcomes=[flow.toggle(first),flow.toggle(pull),flow.toggle(closed),flow.toggle(second)]; process.stdout.write(JSON.stringify({{outcomes,snapshot:flow.snapshot(),states}})); """ result = run_node(script) assert result["outcomes"] == ["selected", "ineligible", "ineligible", "selected"] assert result["snapshot"] == { "active": True, "count": 2, "items": [ {"kind": "issue", "state": "open", "repository": "stackchain/dashboard", "number": 809, "title": "First"}, {"kind": "issue", "state": "open", "repository": "stackchain/api", "number": 17, "title": "Second"}, ], } assert result["states"][-1]["count"] == 2 def test_search_batch_prepares_owned_issues_without_reclaiming_and_claims_unassigned_issues(): script = f""" const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))}); const calls=[]; const details={{ 809:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:809,assigned_to_me:true}}, 810:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:810,claimable:true}}, 811:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:811,claimable:false}}, }}; const flow=createSearchBatchPlan({{ resolve:item=>{{calls.push('resolve:'+item.number);return Promise.resolve(details[item.number]);}}, claim:detail=>{{calls.push('claim:'+detail.number);return Promise.resolve({{...detail,assigned_to_me:true}});}}, }}); Promise.all([ flow.prepare({{number:809}}), flow.prepare({{number:810}}), flow.prepare({{number:811}}).catch(error=>error.message), ]).then(results=>process.stdout.write(JSON.stringify({{results,calls}}))); """ result = run_node(script) assert result["calls"] == ["resolve:809", "resolve:810", "resolve:811", "claim:810"] assert result["results"][0]["assigned_to_me"] is True assert result["results"][1]["assigned_to_me"] is True assert result["results"][2] == "This issue is no longer available to assign." def test_mobile_search_exposes_touch_safe_durable_batch_planning_controls(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() worker = (HTML.parent / "service-worker.js").read_text() controller = SEARCH_BATCH_PLAN.read_text() assert 'id="select-search-results"' in html assert 'id="search-batch-actions"' in html assert 'id="queue-selected-search-results"' in html assert 'id="resume-search-batch"' in html assert 'src="static/search-batch-plan.js"' in html assert "mountSearchBatchPlanning(" in dashboard assert "journalName:'search-today-batch'" in controller assert "autoMount:false" in controller assert "plan.toggle(result)" in controller assert "processor.run(plan.snapshot().items)" in controller assert "processor.resume()" in controller assert 'aria-label="Select ' in controller assert "result.kind === 'issue' && result.state === 'open'" in controller assert ".search-batch-actions" in css assert "position:sticky" in css assert "env(safe-area-inset-bottom)" in css assert ".cmd-select-result" in css and "min-height:44px" in css assert "BASE + 'static/search-batch-plan.js'" in worker