feat: batch-plan mobile search results into Today (Closes #809)
This commit is contained in:
1 parent
cacecac865
commit
c04cba29d9
10 files changed
+315
-6
No files matched your search
@@ -185,6 +185,41 @@ flow.resume().then(result=>process.stdout.write(JSON.stringify({{result,calls}})
|
||||
}
|
||||
|
||||
|
||||
def test_search_batch_uses_an_isolated_owner_journal_and_can_resume_without_dom_mounting():
|
||||
script = f"""
|
||||
const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))});
|
||||
const values={{}};
|
||||
const calls=[];
|
||||
const storage={{
|
||||
getItem:key=>values[key] || null,
|
||||
setItem:(key,value)=>{{values[key]=value;}},
|
||||
removeItem:key=>{{delete values[key];}},
|
||||
}};
|
||||
const flow=createBatchFindWork({{
|
||||
capacity:()=>1, owner:()=> 'timmy', storage,
|
||||
journalName:'search-today-batch', autoMount:false,
|
||||
claim:item=>Promise.resolve({{...item,assigned_to_me:true}}),
|
||||
queue:()=>{{calls.push('queue');return 'sync-unavailable';}},
|
||||
}});
|
||||
flow.run([{{repository:'stackchain/dashboard',number:809}}]).then(result=>
|
||||
process.stdout.write(JSON.stringify({{result,keys:Object.keys(values),calls,pending:flow.pending()}})));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"result": {
|
||||
"status": "complete", "selected": 1, "available": 1, "queued": [],
|
||||
"failed": [{
|
||||
"key": "stackchain/dashboard#809",
|
||||
"reason": "assigned but Today sync is unavailable",
|
||||
"assigned": True,
|
||||
}],
|
||||
},
|
||||
"keys": ["stackchain.search-today-batch.v1.timmy"],
|
||||
"calls": ["queue"],
|
||||
"pending": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_time_budget_blocks_claims_until_every_estimate_fits():
|
||||
script = f"""
|
||||
const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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
|
||||
@@ -830,6 +830,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/later-picker.js",
|
||||
"/dashboard/static/pick-work.js",
|
||||
"/dashboard/static/batch-find-work.js",
|
||||
"/dashboard/static/search-batch-plan.js",
|
||||
"/dashboard/static/conversation.js",
|
||||
"/dashboard/static/comment-actions.js",
|
||||
"/dashboard/static/issue-attachment.js",
|
||||
|
||||
Reference in new issue
Block a user