106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
SAVED_SEARCHES = Path(__file__).parents[1] / "frontend" / "saved-searches.js"
|
|
|
|
|
|
def run_node(script):
|
|
return json.loads(
|
|
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
|
)
|
|
|
|
|
|
def test_saved_search_controller_loads_saves_opens_renames_and_deletes():
|
|
script = f"""
|
|
const createSavedSearches=require({json.dumps(str(SAVED_SEARCHES))});
|
|
const requests=[]; const opened=[]; const states=[];
|
|
let remote={{revision:0,views:[]}}; let sequence=0;
|
|
const controller=createSavedSearches({{
|
|
fetchJson:async (_url,options={{}})=>{{
|
|
requests.push(options.method||'GET');
|
|
if(!options.method)return remote;
|
|
const body=JSON.parse(options.body);remote={{revision:remote.revision+1,views:body.views}};return remote;
|
|
}},
|
|
createId:()=> 'view-'+(++sequence),
|
|
onOpen:view=>opened.push(view),
|
|
onState:state=>states.push(state),
|
|
}});
|
|
(async()=>{{
|
|
await controller.load();
|
|
await controller.save('Release queue',{{query:'mobile',kind:'issue',state:'open',repository:'stackchain/dashboard'}});
|
|
controller.open('view-1');
|
|
await controller.rename('view-1','Morning release queue');
|
|
await controller.remove('view-1');
|
|
process.stdout.write(JSON.stringify({{requests,opened,snapshot:controller.snapshot(),statuses:states.map(x=>x.status)}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1)}});
|
|
"""
|
|
assert run_node(script) == {
|
|
"requests": ["GET", "PUT", "PUT", "PUT"],
|
|
"opened": [{
|
|
"id": "view-1",
|
|
"name": "Release queue",
|
|
"query": "mobile",
|
|
"kind": "issue",
|
|
"state": "open",
|
|
"repository": "stackchain/dashboard",
|
|
}],
|
|
"snapshot": {"revision": 3, "views": []},
|
|
"statuses": ["loading", "ready", "saving", "ready", "saving", "ready", "saving", "ready"],
|
|
}
|
|
|
|
|
|
def test_saved_search_controller_adopts_conflict_and_keeps_search_usable_on_failure():
|
|
script = f"""
|
|
const createSavedSearches=require({json.dumps(str(SAVED_SEARCHES))});
|
|
const states=[];let mode='conflict';
|
|
const server={{revision:4,views:[{{id:'remote',name:'Remote',query:'review',kind:'pull',state:'open',repository:''}}]}};
|
|
const controller=createSavedSearches({{
|
|
fetchJson:async (_url,options={{}})=>{{
|
|
if(!options.method)return {{revision:3,views:[]}};
|
|
if(mode==='conflict'){{const error=new Error('conflict');error.status=409;error.payload={{detail:{{snapshot:server}}}};throw error;}}
|
|
throw new Error('offline');
|
|
}},createId:()=> 'local',onOpen:()=>{{}},onState:state=>states.push([state.status,state.message||'']),
|
|
}});
|
|
(async()=>{{
|
|
await controller.load();
|
|
try{{await controller.save('Local',{{query:'mobile',kind:'all',state:'all'}})}}catch(_error){{}}
|
|
mode='offline';
|
|
try{{await controller.remove('remote')}}catch(_error){{}}
|
|
process.stdout.write(JSON.stringify({{snapshot:controller.snapshot(),states}}));
|
|
}})();
|
|
"""
|
|
result = run_node(script)
|
|
assert result["snapshot"] == {
|
|
"revision": 4,
|
|
"views": [{
|
|
"id": "remote", "name": "Remote", "query": "review",
|
|
"kind": "pull", "state": "open", "repository": "",
|
|
}],
|
|
}
|
|
assert result["states"][-1] == ["error", "Saved searches could not sync. Search still works."]
|
|
assert ["conflict", "Saved searches changed on another device."] in result["states"]
|
|
|
|
|
|
def test_mobile_search_renders_synced_saved_view_controls_and_runtime_wiring():
|
|
from tests.dashboard_bundle import dashboard_bundle_text
|
|
|
|
html = dashboard_bundle_text()
|
|
css = (SAVED_SEARCHES.parent / "dashboard.css").read_text()
|
|
dashboard = (SAVED_SEARCHES.parent / "dashboard.js").read_text()
|
|
|
|
assert '<section class="saved-searches" aria-labelledby="saved-searches-heading">' in html
|
|
assert 'id="saved-search-name"' in html
|
|
assert 'id="save-current-search"' in html
|
|
assert 'id="saved-search-list"' in html
|
|
assert 'id="saved-search-status"' in html
|
|
assert "createSavedSearches.mount(" in dashboard
|
|
saved_searches = SAVED_SEARCHES.read_text()
|
|
assert "search.setQuery('')" in saved_searches
|
|
assert "applyScope(view)" in saved_searches
|
|
assert "search.setQuery(view.query)" in saved_searches
|
|
assert ").load();" in dashboard
|
|
assert "static/saved-searches.js" in (SAVED_SEARCHES.parent.parent / "src" / "frontend_bundle.py").read_text()
|
|
assert ".saved-search-action { min-height:44px;" in css
|