fix: make mobile recent work sync generation-safe (Closes #1485)
This commit is contained in:
1 parent
5af34508b4
commit
cec5b93665
2 files changed
+176
-10
No files matched your search
@@ -159,6 +159,118 @@ process.stdout.write(JSON.stringify({{immediate,settled:recent.items(),status:st
|
||||
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))});
|
||||
|
||||
Reference in new issue
Block a user