Resume interrupted Today sessions #396
|
|
@ -37,7 +37,11 @@ the private content. Issue capture and authored mobile actions (issue
|
|||
comments, pull-request comments, notification replies, and reviews) persist per-draft
|
||||
idempotency keys, so retrying after a timeout, reload, process restart, or handoff to
|
||||
another worker replays a confirmed result instead of posting duplicate content. The ordered,
|
||||
five-item Today plan syncs across the operator's devices. Server revisions prevent delayed
|
||||
five-item Today plan syncs across the operator's devices. Starting a Today work session also
|
||||
stores an account-bound checkpoint on the current device. After a reload or installed-app
|
||||
restart, **Resume Today** reopens the saved item (or the next surviving item if work changed);
|
||||
finishing or choosing **End session** clears only the checkpoint and leaves the Today plan
|
||||
unchanged. Another or unconfirmed account cannot see or resume it. Server revisions prevent delayed
|
||||
responses from replacing a newer plan; same-account browser tabs exchange fresh snapshots,
|
||||
and reconnecting or returning to the dashboard refreshes server truth after replaying queued
|
||||
offline operations. Planning edits can remain offline for up to 30 days. After that, the
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.find-work-action { min-height:44px; }
|
||||
.my-work-actions { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.start-work-session { min-height:44px; }
|
||||
.resume-today-session, .end-today-session { min-height:44px; }
|
||||
.work-session-nav { position:sticky; bottom:0; z-index:5; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; padding:10px 4px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
|
||||
.work-session-nav[hidden] { display:none; }
|
||||
.work-session-nav [data-work-session-progress] { grid-column:1 / -1; text-align:center; }
|
||||
|
|
|
|||
|
|
@ -665,12 +665,30 @@
|
|||
openRoutedWork(item, null, { replace:true });
|
||||
}
|
||||
|
||||
const sessionCheckpoint = createWorkSessionCheckpoint({
|
||||
storage: localStorage,
|
||||
getLogin: () => confirmedOwnerLogin,
|
||||
onError: () => {
|
||||
qs('#my-work-action-status').textContent =
|
||||
'Session recovery could not be saved on this device. You can keep working.';
|
||||
},
|
||||
});
|
||||
function updateWorkSessionActions() {
|
||||
const active = workSession.active();
|
||||
qs('#start-work-session').hidden = active;
|
||||
qs('#resume-today-session').hidden = active || !todayMyWork.length || !workSession.resumable();
|
||||
qs('#end-today-session').hidden = !active;
|
||||
}
|
||||
|
||||
const workSession = createWorkSession({
|
||||
getItems: () => selectedWorkFilter === 'today' ? todayMyWork : activeMyWork,
|
||||
getFilter: () => selectedWorkFilter === 'today' ? 'all' : selectedWorkFilter,
|
||||
getMilestone: () => selectedWorkMilestone,
|
||||
checkpoint: sessionCheckpoint,
|
||||
checkpointEnabled: () => selectedWorkFilter === 'today',
|
||||
onOpen: openWorkSessionItem,
|
||||
onProgress: state => {
|
||||
updateWorkSessionActions();
|
||||
document.querySelectorAll('.work-session-nav').forEach(nav => { nav.hidden = false; });
|
||||
document.querySelectorAll('[data-work-session-progress]').forEach(element => {
|
||||
element.textContent = 'Item ' + state.index + ' of ' + state.total;
|
||||
|
|
@ -686,6 +704,7 @@
|
|||
closeOpenWorkSheets();
|
||||
document.querySelectorAll('.work-session-nav').forEach(nav => { nav.hidden = true; });
|
||||
qs('#my-work-action-status').textContent = 'Work session complete.';
|
||||
updateWorkSessionActions();
|
||||
qs('#start-work-session').focus();
|
||||
},
|
||||
});
|
||||
|
|
@ -1042,6 +1061,7 @@
|
|||
updateWorkPaginationControls();
|
||||
renderMyWork();
|
||||
if (reconcileSession && workSession.active()) workSession.reconcile();
|
||||
updateWorkSessionActions();
|
||||
}
|
||||
|
||||
function activeWorkStreams() {
|
||||
|
|
@ -3576,6 +3596,18 @@
|
|||
}
|
||||
workSession.start();
|
||||
});
|
||||
qs('#resume-today-session').addEventListener('click', () => {
|
||||
qs('[data-work-filter="today"]').click();
|
||||
if (!workSession.resume()) {
|
||||
qs('#my-work-action-status').textContent = 'Saved Today session is no longer available.';
|
||||
updateWorkSessionActions();
|
||||
}
|
||||
});
|
||||
qs('#end-today-session').addEventListener('click', () => {
|
||||
workSession.end();
|
||||
qs('#my-work-action-status').textContent = 'Today session ended. Your plan is unchanged.';
|
||||
qs('#start-work-session').focus();
|
||||
});
|
||||
document.querySelectorAll('[data-work-session-previous]').forEach(button =>
|
||||
button.addEventListener('click', () => workSession.previous())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
<div class="my-work-actions">
|
||||
<button class="plan-today" id="plan-today" type="button">Plan Today</button>
|
||||
<button class="start-work-session" id="start-work-session" type="button">Start work</button>
|
||||
<button class="resume-today-session" id="resume-today-session" type="button" hidden>Resume Today</button>
|
||||
<button class="end-today-session" id="end-today-session" type="button" hidden>End session</button>
|
||||
<button class="find-work-action" id="find-work" type="button">Find work</button>
|
||||
<button class="new-issue" id="new-issue" type="button">New issue</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -472,10 +472,67 @@ function workIdentity(item) {
|
|||
return [kind, repository, number, notification].join(':');
|
||||
}
|
||||
|
||||
function createWorkSession({ getItems, getFilter, getMilestone = () => 'all', onOpen, onProgress, onFinish }) {
|
||||
function createWorkSessionCheckpoint({
|
||||
storage,
|
||||
getLogin,
|
||||
onError = () => {},
|
||||
key = 'stackchain.today-session.v1',
|
||||
}) {
|
||||
const login = () => String(getLogin() || '').trim();
|
||||
let errorReported = false;
|
||||
const reportError = error => {
|
||||
if (errorReported) return;
|
||||
errorReported = true;
|
||||
onError(error);
|
||||
};
|
||||
const read = () => {
|
||||
const owner = login();
|
||||
if (!owner) return null;
|
||||
try {
|
||||
const value = JSON.parse(storage.getItem(key) || 'null');
|
||||
if (value?.version !== 1 || value.login !== owner || typeof value.identity !== 'string' ||
|
||||
!Number.isInteger(value.index) || value.index < 0) return null;
|
||||
return value;
|
||||
} catch (error) {
|
||||
reportError(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return {
|
||||
read,
|
||||
save(identity, index) {
|
||||
const owner = login();
|
||||
if (!owner) return false;
|
||||
try {
|
||||
storage.setItem(key, JSON.stringify({ version:1, login:owner, identity, index }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
reportError(error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
if (!read()) return false;
|
||||
try {
|
||||
storage.removeItem(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
reportError(error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkSession({
|
||||
getItems, getFilter, getMilestone = () => 'all', checkpoint = null,
|
||||
checkpointEnabled = () => true,
|
||||
onOpen, onProgress, onFinish,
|
||||
}) {
|
||||
let currentIdentity = '';
|
||||
let currentIndex = -1;
|
||||
let running = false;
|
||||
let durable = false;
|
||||
|
||||
const queue = () => filterMyWork(getItems() || [], getFilter(), getMilestone());
|
||||
const report = (items, index) => onProgress({
|
||||
|
|
@ -485,9 +542,12 @@ function createWorkSession({ getItems, getFilter, getMilestone = () => 'all', on
|
|||
can_next: index < items.length - 1,
|
||||
});
|
||||
const finish = () => {
|
||||
const clearCheckpoint = durable;
|
||||
running = false;
|
||||
durable = false;
|
||||
currentIdentity = '';
|
||||
currentIndex = -1;
|
||||
if (clearCheckpoint) checkpoint?.clear();
|
||||
onFinish();
|
||||
return false;
|
||||
};
|
||||
|
|
@ -495,6 +555,7 @@ function createWorkSession({ getItems, getFilter, getMilestone = () => 'all', on
|
|||
if (!items.length || index < 0 || index >= items.length) return finish();
|
||||
currentIndex = index;
|
||||
currentIdentity = workIdentity(items[index]);
|
||||
if (durable) checkpoint?.save(currentIdentity, currentIndex);
|
||||
report(items, index);
|
||||
onOpen(items[index]);
|
||||
return true;
|
||||
|
|
@ -502,10 +563,23 @@ function createWorkSession({ getItems, getFilter, getMilestone = () => 'all', on
|
|||
|
||||
return {
|
||||
active: () => running,
|
||||
end: () => finish(),
|
||||
resumable: () => Boolean(checkpoint?.read()),
|
||||
resume() {
|
||||
const saved = checkpoint?.read();
|
||||
if (!saved) return false;
|
||||
durable = true;
|
||||
const items = queue();
|
||||
if (!items.length) return finish();
|
||||
running = true;
|
||||
const exact = items.findIndex(item => workIdentity(item) === saved.identity);
|
||||
return openAt(items, exact >= 0 ? exact : Math.min(saved.index, items.length - 1));
|
||||
},
|
||||
start(item = null) {
|
||||
const items = queue();
|
||||
if (!items.length) return finish();
|
||||
running = true;
|
||||
durable = Boolean(checkpoint && checkpointEnabled());
|
||||
const requested = item ? items.findIndex(candidate => workIdentity(candidate) === workIdentity(item)) : 0;
|
||||
return openAt(items, requested >= 0 ? requested : 0);
|
||||
},
|
||||
|
|
@ -612,6 +686,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||
buildMyWork.filterMyWork = filterMyWork;
|
||||
buildMyWork.milestoneLanes = milestoneLanes;
|
||||
buildMyWork.createWorkSession = createWorkSession;
|
||||
buildMyWork.createWorkSessionCheckpoint = createWorkSessionCheckpoint;
|
||||
buildMyWork.workIdentity = workIdentity;
|
||||
buildMyWork.replaceIssueLabels = replaceIssueLabels;
|
||||
buildMyWork.replaceIssueContent = replaceIssueContent;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v59';
|
||||
const CACHE = 'stackchain-dashboard-shell-v60';
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
const SHELL = [
|
||||
|
|
|
|||
|
|
@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
|||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
|
|
|
|||
|
|
@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
|
|||
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
||||
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v59" in worker
|
||||
assert "stackchain-dashboard-shell-v60" in worker
|
||||
|
|
|
|||
|
|
@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
|
|||
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v59" in worker
|
||||
assert "stackchain-dashboard-shell-v60" in worker
|
||||
|
|
|
|||
|
|
@ -1428,6 +1428,157 @@ process.stdout.write(JSON.stringify({{finished,active:session.active()}}));
|
|||
assert json.loads(result.stdout) == {"finished": 1, "active": False}
|
||||
|
||||
|
||||
def test_today_session_checkpoint_resumes_current_item_after_reload():
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_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),
|
||||
}};
|
||||
const items = [1, 2, 3].map(number => ({{
|
||||
kind:'issue', repository:'stackchain/dashboard', number, title:'Item ' + number,
|
||||
}}));
|
||||
const opened = [];
|
||||
const firstCheckpoint = buildMyWork.createWorkSessionCheckpoint({{
|
||||
storage, getLogin: () => 'timmy',
|
||||
}});
|
||||
const first = buildMyWork.createWorkSession({{
|
||||
getItems: () => items, getFilter: () => 'all',
|
||||
checkpoint: firstCheckpoint,
|
||||
onOpen: item => opened.push('first:' + item.number),
|
||||
onProgress: () => {{}}, onFinish: () => {{}},
|
||||
}});
|
||||
first.start();
|
||||
first.next();
|
||||
|
||||
const secondCheckpoint = buildMyWork.createWorkSessionCheckpoint({{
|
||||
storage, getLogin: () => 'timmy',
|
||||
}});
|
||||
const resumed = buildMyWork.createWorkSession({{
|
||||
getItems: () => items, getFilter: () => 'all',
|
||||
checkpoint: secondCheckpoint,
|
||||
onOpen: item => opened.push('resumed:' + item.number),
|
||||
onProgress: () => {{}}, onFinish: () => {{}},
|
||||
}});
|
||||
const available = resumed.resumable();
|
||||
const didResume = resumed.resume();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
available, didResume, opened, checkpoint:JSON.parse(values.values().next().value),
|
||||
}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"available": True,
|
||||
"didResume": True,
|
||||
"opened": ["first:1", "first:2", "resumed:2"],
|
||||
"checkpoint": {
|
||||
"version": 1,
|
||||
"login": "timmy",
|
||||
"identity": "issue:stackchain/dashboard:2:",
|
||||
"index": 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_today_session_checkpoint_is_account_bound_and_end_clears_it():
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
let login = 'timmy';
|
||||
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),
|
||||
}};
|
||||
const checkpoint = buildMyWork.createWorkSessionCheckpoint({{storage, getLogin:() => login}});
|
||||
const session = buildMyWork.createWorkSession({{
|
||||
getItems: () => [{{kind:'issue',repository:'stackchain/dashboard',number:7}}],
|
||||
getFilter: () => 'all', checkpoint,
|
||||
onOpen: () => {{}}, onProgress: () => {{}}, onFinish: () => {{}},
|
||||
}});
|
||||
session.start();
|
||||
login = 'alexander';
|
||||
const visibleToOtherAccount = session.resumable();
|
||||
const retainedForOwner = values.size;
|
||||
login = 'timmy';
|
||||
session.end();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
visibleToOtherAccount, retainedForOwner, active:session.active(), stored:values.size,
|
||||
}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"visibleToOtherAccount": False,
|
||||
"retainedForOwner": 1,
|
||||
"active": False,
|
||||
"stored": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_resuming_an_empty_today_plan_clears_the_stale_checkpoint():
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
const values = new Map([['stackchain.today-session.v1', JSON.stringify({{
|
||||
version:1, login:'timmy', identity:'issue:stackchain/dashboard:7:', index:0,
|
||||
}})]]);
|
||||
const storage = {{
|
||||
getItem:key => values.get(key) || null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
const checkpoint = buildMyWork.createWorkSessionCheckpoint({{storage,getLogin:() => 'timmy'}});
|
||||
const session = buildMyWork.createWorkSession({{
|
||||
getItems:() => [], getFilter:() => 'all', checkpoint,
|
||||
onOpen:() => {{}}, onProgress:() => {{}}, onFinish:() => {{}},
|
||||
}});
|
||||
const resumed = session.resume();
|
||||
process.stdout.write(JSON.stringify({{resumed, stored:values.size, available:session.resumable()}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {"resumed": False, "stored": 0, "available": False}
|
||||
|
||||
|
||||
def test_today_session_keeps_working_when_checkpoint_storage_fails():
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
let errors = 0;
|
||||
const opened = [];
|
||||
const checkpoint = buildMyWork.createWorkSessionCheckpoint({{
|
||||
storage:{{
|
||||
getItem:() => null,
|
||||
setItem:() => {{ throw new Error('quota'); }},
|
||||
removeItem:() => {{ throw new Error('quota'); }},
|
||||
}},
|
||||
getLogin:() => 'timmy',
|
||||
onError:() => {{ errors += 1; }},
|
||||
}});
|
||||
const session = buildMyWork.createWorkSession({{
|
||||
getItems:() => [1,2].map(number => ({{kind:'issue',repository:'stackchain/dashboard',number}})),
|
||||
getFilter:() => 'all', checkpoint,
|
||||
onOpen:item => opened.push(item.number), onProgress:() => {{}}, onFinish:() => {{}},
|
||||
}});
|
||||
session.start();
|
||||
session.next();
|
||||
process.stdout.write(JSON.stringify({{errors,opened,active:session.active()}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {"errors": 1, "opened": [1, 2], "active": True}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_work_session_renders_touch_safe_controls_for_every_work_sheet():
|
||||
html = await dashboard()
|
||||
|
|
@ -1442,6 +1593,23 @@ async def test_mobile_work_session_renders_touch_safe_controls_for_every_work_sh
|
|||
assert 'aria-live="polite" data-work-session-progress' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_offers_account_safe_resume_and_end_today_controls():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="resume-today-session"' in html
|
||||
assert 'id="end-today-session"' in html
|
||||
assert 'const sessionCheckpoint = createWorkSessionCheckpoint({' in html
|
||||
assert 'getLogin: () => confirmedOwnerLogin' in html
|
||||
assert 'checkpoint: sessionCheckpoint' in html
|
||||
assert 'checkpointEnabled: () => selectedWorkFilter === \'today\'' in html
|
||||
assert "qs('#resume-today-session').addEventListener('click'" in html
|
||||
assert "qs('#end-today-session').addEventListener('click'" in html
|
||||
assert "workSession.resume()" in html
|
||||
assert "workSession.end()" in html
|
||||
assert '.resume-today-session, .end-today-session { min-height:44px;' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_wires_work_session_to_existing_sheet_flows_and_completion_actions():
|
||||
html = await dashboard()
|
||||
|
|
|
|||
|
|
@ -101,5 +101,5 @@ async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
|||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
|
|
|
|||
|
|
@ -105,10 +105,19 @@ async function dispatchNotificationClick(route) {{
|
|||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
||||
|
||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -116,14 +125,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
|||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/later-picker.js'" in source
|
||||
|
||||
|
||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
|
@ -132,21 +141,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
|||
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||
|
||||
|
||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
|
|||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v59" in source
|
||||
assert "stackchain-dashboard-shell-v60" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user