feat: queue available work in Today (#415)
This commit is contained in:
parent
4b8d0c4504
commit
ad5e50eb48
|
|
@ -1,7 +1,7 @@
|
||||||
function createAssignAndStart({ available, claim, start, recover, announce }) {
|
function createAssignAndStart({ available, claim, start, queue, recover, announce }) {
|
||||||
let request = null;
|
let request = null;
|
||||||
|
|
||||||
function run(item, { alreadyOwned = false } = {}) {
|
function run(item, { alreadyOwned = false, destination = 'start' } = {}) {
|
||||||
if (request) return request;
|
if (request) return request;
|
||||||
if (!available()) {
|
if (!available()) {
|
||||||
announce('Today is full—remove an item before assigning this issue.');
|
announce('Today is full—remove an item before assigning this issue.');
|
||||||
|
|
@ -10,15 +10,32 @@ function createAssignAndStart({ available, claim, start, recover, announce }) {
|
||||||
request = Promise.resolve()
|
request = Promise.resolve()
|
||||||
.then(() => alreadyOwned ? item : claim(item))
|
.then(() => alreadyOwned ? item : claim(item))
|
||||||
.then(confirmed => {
|
.then(confirmed => {
|
||||||
const outcome = start(confirmed);
|
const outcome = destination === 'queue' ? queue(confirmed) : start(confirmed);
|
||||||
|
if (outcome === 'queued') {
|
||||||
|
announce(alreadyOwned ? 'Queued in Today. Keep finding work when ready.' :
|
||||||
|
'Assigned and queued in Today. Keep finding work when ready.');
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
if (outcome === 'started') {
|
if (outcome === 'started') {
|
||||||
announce(alreadyOwned ? 'Added to Today and ready to work.' :
|
announce(alreadyOwned ? 'Added to Today and ready to work.' :
|
||||||
'Assigned, added to Today, and ready to work.');
|
'Assigned, added to Today, and ready to work.');
|
||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
if (destination === 'queue') {
|
||||||
|
if (outcome === 'sync-unavailable') {
|
||||||
|
announce(alreadyOwned ?
|
||||||
|
'Saved in Today on this device, but account sync is unavailable. The issue is open so you can recover.' :
|
||||||
|
'Assigned and saved in Today on this device, but account sync is unavailable. The issue is open so you can recover.');
|
||||||
|
} else {
|
||||||
|
announce(alreadyOwned ?
|
||||||
|
'Today could not be queued. The issue is open so you can recover.' :
|
||||||
|
'Assigned to you, but Today could not be queued. The issue is open so you can recover.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
announce(alreadyOwned ?
|
announce(alreadyOwned ?
|
||||||
'Today could not start. The issue is open so you can recover.' :
|
'Today could not start. The issue is open so you can recover.' :
|
||||||
'Assigned to you, but Today could not start. The issue is open so you can recover.');
|
'Assigned to you, but Today could not start. The issue is open so you can recover.');
|
||||||
|
}
|
||||||
recover(confirmed);
|
recover(confirmed);
|
||||||
return 'recovery';
|
return 'recovery';
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.find-work-card { display:grid; gap:8px; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#101f36; overflow-wrap:anywhere; }
|
.find-work-card { display:grid; gap:8px; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#101f36; overflow-wrap:anywhere; }
|
||||||
.find-work-card button { width:100%; font-weight:700; }
|
.find-work-card button { width:100%; font-weight:700; }
|
||||||
.find-work-claim-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
.find-work-claim-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
||||||
|
.find-work-claim-actions [data-claim-start-index] { grid-column:1 / -1; }
|
||||||
.find-work-detail { min-width:0; display:grid; gap:10px; padding:10px; border-radius:10px; background:#0b1526; }
|
.find-work-detail { min-width:0; display:grid; gap:10px; padding:10px; border-radius:10px; background:#0b1526; }
|
||||||
.find-work-description { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
.find-work-description { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||||
.find-work-detail a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
.find-work-detail a { display:flex; align-items:center; justify-content:center; border:1px solid #60a5fa; border-radius:10px; font-weight:700; }
|
||||||
|
|
|
||||||
|
|
@ -757,6 +757,13 @@
|
||||||
announce: message => { qs('#my-work-action-status').textContent = message; },
|
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queueToday = createQueueToday({
|
||||||
|
todayWork,
|
||||||
|
todaySync,
|
||||||
|
refresh: refreshMyWorkView,
|
||||||
|
warm: warmTodayOffline,
|
||||||
|
});
|
||||||
|
|
||||||
function acceptClaimedIssue(confirmed) {
|
function acceptClaimedIssue(confirmed) {
|
||||||
lastContextSnapshot = lastContextSnapshot || { user: {}, repos: [], issues: [], pull_requests: [] };
|
lastContextSnapshot = lastContextSnapshot || { user: {}, repos: [], issues: [], pull_requests: [] };
|
||||||
lastContextSnapshot.issues = [confirmed].concat((lastContextSnapshot.issues || []).filter(candidate =>
|
lastContextSnapshot.issues = [confirmed].concat((lastContextSnapshot.issues || []).filter(candidate =>
|
||||||
|
|
@ -777,6 +784,10 @@
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
return createAndStart.complete(claimed);
|
return createAndStart.complete(claimed);
|
||||||
},
|
},
|
||||||
|
queue: confirmed => {
|
||||||
|
const claimed = acceptClaimedIssue(confirmed);
|
||||||
|
return queueToday(claimed);
|
||||||
|
},
|
||||||
recover: confirmed => {
|
recover: confirmed => {
|
||||||
const claimed = acceptClaimedIssue(confirmed);
|
const claimed = acceptClaimedIssue(confirmed);
|
||||||
taskOverlayHistory.leave();
|
taskOverlayHistory.leave();
|
||||||
|
|
@ -1902,8 +1913,9 @@
|
||||||
'</div><button type="button" data-preview-index="' + index + '" aria-expanded="' + expanded +
|
'</div><button type="button" data-preview-index="' + index + '" aria-expanded="' + expanded +
|
||||||
'" aria-controls="' + detailId + '">' + (expanded ? 'Hide details' : 'View details') + '</button>' +
|
'" aria-controls="' + detailId + '">' + (expanded ? 'Hide details' : 'View details') + '</button>' +
|
||||||
detail + '<div class="find-work-claim-actions"><button type="button" data-claim-index="' + index +
|
detail + '<div class="find-work-claim-actions"><button type="button" data-claim-index="' + index +
|
||||||
'">Assign to me</button><button type="button" data-claim-start-index="' + index +
|
'">Assign</button><button type="button" data-claim-queue-index="' + index +
|
||||||
'">Assign & start</button></div></article>';
|
'">Queue Today</button><button type="button" data-claim-start-index="' + index +
|
||||||
|
'">Start now</button></div></article>';
|
||||||
}).join('') : '<div class="muted">No unassigned issues are available on this page.</div>';
|
}).join('') : '<div class="muted">No unassigned issues are available on this page.</div>';
|
||||||
list.querySelectorAll('[data-preview-index]').forEach(button => {
|
list.querySelectorAll('[data-preview-index]').forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
|
|
@ -1935,12 +1947,31 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
list.querySelectorAll('[data-claim-queue-index]').forEach(button => {
|
||||||
|
button.addEventListener('click', async () => {
|
||||||
|
const item = findWorkController.items()[Number(button.dataset.claimQueueIndex)];
|
||||||
|
if (!item) return;
|
||||||
|
const claimButtons = button.closest('.find-work-card')
|
||||||
|
.querySelectorAll('[data-claim-index], [data-claim-queue-index], [data-claim-start-index]');
|
||||||
|
claimButtons.forEach(action => { action.disabled = true; });
|
||||||
|
try {
|
||||||
|
const outcome = await assignAndStart.run(item, { destination:'queue' });
|
||||||
|
if (outcome === 'queued') {
|
||||||
|
(qs('[data-claim-queue-index]') || qs('#close-find-work')).focus();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
qs('#find-work-status').textContent = error.message + ' Nothing was added to Today; retry assignment.';
|
||||||
|
claimButtons.forEach(action => { action.disabled = false; });
|
||||||
|
button.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
list.querySelectorAll('[data-claim-start-index]').forEach(button => {
|
list.querySelectorAll('[data-claim-start-index]').forEach(button => {
|
||||||
button.addEventListener('click', async () => {
|
button.addEventListener('click', async () => {
|
||||||
const item = findWorkController.items()[Number(button.dataset.claimStartIndex)];
|
const item = findWorkController.items()[Number(button.dataset.claimStartIndex)];
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const claimButtons = button.closest('.find-work-card')
|
const claimButtons = button.closest('.find-work-card')
|
||||||
.querySelectorAll('[data-claim-index], [data-claim-start-index]');
|
.querySelectorAll('[data-claim-index], [data-claim-queue-index], [data-claim-start-index]');
|
||||||
claimButtons.forEach(action => { action.disabled = true; });
|
claimButtons.forEach(action => { action.disabled = true; });
|
||||||
try {
|
try {
|
||||||
await assignAndStart.run(item);
|
await assignAndStart.run(item);
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,7 @@
|
||||||
<script src="static/create-issue-sheet.js"></script>
|
<script src="static/create-issue-sheet.js"></script>
|
||||||
<script src="static/create-and-start.js"></script>
|
<script src="static/create-and-start.js"></script>
|
||||||
<script src="static/assign-and-start.js"></script>
|
<script src="static/assign-and-start.js"></script>
|
||||||
|
<script src="static/queue-today.js"></script>
|
||||||
<script src="static/pull-sheet.js"></script>
|
<script src="static/pull-sheet.js"></script>
|
||||||
<script src="static/review-sheet.js"></script>
|
<script src="static/review-sheet.js"></script>
|
||||||
<script src="static/work-route.js"></script>
|
<script src="static/work-route.js"></script>
|
||||||
|
|
|
||||||
17
frontend/queue-today.js
Normal file
17
frontend/queue-today.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
function createQueueToday({ todayWork, todaySync, refresh, warm }) {
|
||||||
|
return function queueToday(issue) {
|
||||||
|
const added = todayWork.add(issue);
|
||||||
|
if (added !== 'added' && added !== 'exists') return added;
|
||||||
|
if (added === 'added' && !todaySync.enqueue('add', todayWork.identity(issue))) {
|
||||||
|
refresh();
|
||||||
|
warm();
|
||||||
|
return 'sync-unavailable';
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
if (added === 'added') todaySync.flush();
|
||||||
|
warm();
|
||||||
|
return 'queued';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = createQueueToday;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const BASE = new URL('./', self.location.href).pathname;
|
const BASE = new URL('./', self.location.href).pathname;
|
||||||
importScripts(BASE + 'static/background-issue-sync.js');
|
importScripts(BASE + 'static/background-issue-sync.js');
|
||||||
const CACHE = 'stackchain-dashboard-shell-v66';
|
const CACHE = 'stackchain-dashboard-shell-v67';
|
||||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||||
|
|
@ -41,6 +41,7 @@ const SHELL = [
|
||||||
BASE + 'static/create-issue-sheet.js',
|
BASE + 'static/create-issue-sheet.js',
|
||||||
BASE + 'static/create-and-start.js',
|
BASE + 'static/create-and-start.js',
|
||||||
BASE + 'static/assign-and-start.js',
|
BASE + 'static/assign-and-start.js',
|
||||||
|
BASE + 'static/queue-today.js',
|
||||||
BASE + 'static/pull-sheet.js',
|
BASE + 'static/pull-sheet.js',
|
||||||
BASE + 'static/review-sheet.js',
|
BASE + 'static/review-sheet.js',
|
||||||
BASE + 'static/work-route.js',
|
BASE + 'static/work-route.js',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js"
|
ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js"
|
||||||
|
QUEUE_TODAY = Path(__file__).parents[1] / "frontend" / "queue-today.js"
|
||||||
|
HTML = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||||
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
||||||
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
||||||
|
|
@ -154,17 +156,121 @@ flow.run(issue, {{alreadyOwned:true}}).then(result=>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_queue_today_claims_once_without_starting_the_active_session():
|
||||||
|
script = f"""
|
||||||
|
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
||||||
|
const calls=[];
|
||||||
|
let release;
|
||||||
|
const claimResult=new Promise(resolve=>{{release=resolve;}});
|
||||||
|
const issue={{repository:'stackchain/dashboard',number:415}};
|
||||||
|
const flow=createAssignAndStart({{
|
||||||
|
available:()=>true,
|
||||||
|
claim:item=>{{calls.push('claim:'+item.number);return claimResult;}},
|
||||||
|
start:item=>{{calls.push('start:'+item.number);return 'started';}},
|
||||||
|
queue:item=>{{calls.push('queue:'+item.number);return 'queued';}},
|
||||||
|
recover:item=>calls.push('recover:'+item.number),
|
||||||
|
announce:message=>calls.push('announce:'+message),
|
||||||
|
}});
|
||||||
|
const first=flow.run(issue, {{destination:'queue'}});
|
||||||
|
const second=flow.run(issue, {{destination:'queue'}});
|
||||||
|
release({{...issue,assignees:['timmy']}});
|
||||||
|
Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{
|
||||||
|
calls,results,same:first===second
|
||||||
|
}})));
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"calls": [
|
||||||
|
"claim:415",
|
||||||
|
"queue:415",
|
||||||
|
"announce:Assigned and queued in Today. Keep finding work when ready.",
|
||||||
|
],
|
||||||
|
"results": ["queued", "queued"],
|
||||||
|
"same": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_queue_today_persists_syncs_and_warms_without_a_session_side_effect():
|
||||||
|
script = f"""
|
||||||
|
const fs=require('fs');
|
||||||
|
if (!fs.existsSync({json.dumps(str(QUEUE_TODAY))})) {{
|
||||||
|
process.stdout.write(JSON.stringify({{available:false}}));
|
||||||
|
}} else {{
|
||||||
|
const createQueueToday=require({json.dumps(str(QUEUE_TODAY))});
|
||||||
|
const calls=[];
|
||||||
|
const issue={{kind:'issue',repository:'stackchain/dashboard',number:415}};
|
||||||
|
const queue=createQueueToday({{
|
||||||
|
todayWork:{{identity:()=> 'issue:stackchain/dashboard:415:',add:()=>{{calls.push('add');return 'added';}}}},
|
||||||
|
todaySync:{{enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}},flush:()=>calls.push('flush')}},
|
||||||
|
refresh:()=>calls.push('refresh'),
|
||||||
|
warm:()=>calls.push('warm'),
|
||||||
|
}});
|
||||||
|
process.stdout.write(JSON.stringify({{available:true,result:queue(issue),calls}}));
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"available": True,
|
||||||
|
"result": "queued",
|
||||||
|
"calls": [
|
||||||
|
"add",
|
||||||
|
"add:issue:stackchain/dashboard:415:",
|
||||||
|
"refresh",
|
||||||
|
"flush",
|
||||||
|
"warm",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_queue_today_reports_assignment_when_local_planning_needs_recovery():
|
||||||
|
script = f"""
|
||||||
|
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
|
||||||
|
const calls=[];
|
||||||
|
const issue={{repository:'stackchain/dashboard',number:415,assignees:['timmy']}};
|
||||||
|
const flow=createAssignAndStart({{
|
||||||
|
available:()=>true,
|
||||||
|
claim:()=>Promise.resolve(issue),
|
||||||
|
start:()=> 'started',
|
||||||
|
queue:()=> 'sync-unavailable',
|
||||||
|
recover:item=>calls.push('recover:'+item.number),
|
||||||
|
announce:message=>calls.push('announce:'+message),
|
||||||
|
}});
|
||||||
|
flow.run(issue, {{destination:'queue'}}).then(result=>
|
||||||
|
process.stdout.write(JSON.stringify({{result,calls}}))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert run_node(script) == {
|
||||||
|
"result": "recovery",
|
||||||
|
"calls": [
|
||||||
|
"announce:Assigned and saved in Today on this device, but account sync is unavailable. The issue is open so you can recover.",
|
||||||
|
"recover:415",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
|
def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
|
||||||
|
html = HTML.read_text()
|
||||||
dashboard = DASHBOARD.read_text()
|
dashboard = DASHBOARD.read_text()
|
||||||
css = CSS.read_text()
|
css = CSS.read_text()
|
||||||
worker = WORKER.read_text()
|
worker = WORKER.read_text()
|
||||||
|
|
||||||
assert "data-claim-start-index" in dashboard
|
assert "data-claim-start-index" in dashboard
|
||||||
assert ">Assign & start</button>" in dashboard
|
assert "data-claim-queue-index" in dashboard
|
||||||
|
assert ">Start now</button>" in dashboard
|
||||||
|
assert ">Queue Today</button>" in dashboard
|
||||||
assert "const assignAndStart = createAssignAndStart({" in dashboard
|
assert "const assignAndStart = createAssignAndStart({" in dashboard
|
||||||
assert "assignAndStart.run(item)" in dashboard
|
assert "assignAndStart.run(item)" in dashboard
|
||||||
|
assert "assignAndStart.run(item, { destination:'queue' })" in dashboard
|
||||||
|
assert "queue: confirmed =>" in dashboard
|
||||||
|
assert "const queueToday = createQueueToday({" in dashboard
|
||||||
|
assert "return queueToday(claimed)" in dashboard
|
||||||
assert "createAndStart.available" in dashboard
|
assert "createAndStart.available" in dashboard
|
||||||
assert "createAndStart.complete" in dashboard
|
assert "createAndStart.complete" in dashboard
|
||||||
assert ".find-work-claim-actions" in css
|
assert ".find-work-claim-actions" in css
|
||||||
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
|
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
|
||||||
|
assert ".find-work-claim-actions [data-claim-start-index]" in css
|
||||||
|
assert "grid-column:1 / -1" in css
|
||||||
assert "BASE + 'static/assign-and-start.js'" in worker
|
assert "BASE + 'static/assign-and-start.js'" in worker
|
||||||
|
assert "BASE + 'static/queue-today.js'" in worker
|
||||||
|
assert '<script src="static/queue-today.js"></script>' in html
|
||||||
|
|
|
||||||
|
|
@ -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():
|
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/later-sync.js'" 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 { 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 pre { max-width:100%; overflow-x:auto;" in css
|
||||||
assert ".markdown-content a { min-height:44px;" in css
|
assert ".markdown-content a { min-height:44px;" in css
|
||||||
assert "stackchain-dashboard-shell-v66" in worker
|
assert "stackchain-dashboard-shell-v67" 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]))
|
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 local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||||
assert "stackchain-dashboard-shell-v66" in worker
|
assert "stackchain-dashboard-shell-v67" in worker
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,6 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
||||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||||
source = SERVICE_WORKER.read_text()
|
source = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/plan-today.js'" in source
|
assert "BASE + 'static/plan-today.js'" in source
|
||||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ async function dispatchNotificationClick(route) {{
|
||||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/my-work.js'" in source
|
assert "BASE + 'static/my-work.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
@ -130,7 +130,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -138,14 +138,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/later-picker.js'" in source
|
assert "BASE + 'static/later-picker.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/install-app.js'" in source
|
assert "BASE + 'static/install-app.js'" in source
|
||||||
|
|
@ -154,21 +154,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -331,6 +331,13 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
||||||
assert "batch: withSessionCsrf" in source
|
assert "batch: withSessionCsrf" in source
|
||||||
|
|
||||||
|
|
||||||
|
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||||
|
source = WORKER.read_text()
|
||||||
|
|
||||||
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_install_precaches_complete_subpath_scoped_app_shell():
|
def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
result = run_worker_scenario(
|
result = run_worker_scenario(
|
||||||
"""
|
"""
|
||||||
|
|
@ -378,6 +385,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/create-issue-sheet.js",
|
"/dashboard/static/create-issue-sheet.js",
|
||||||
"/dashboard/static/create-and-start.js",
|
"/dashboard/static/create-and-start.js",
|
||||||
"/dashboard/static/assign-and-start.js",
|
"/dashboard/static/assign-and-start.js",
|
||||||
|
"/dashboard/static/queue-today.js",
|
||||||
"/dashboard/static/pull-sheet.js",
|
"/dashboard/static/pull-sheet.js",
|
||||||
"/dashboard/static/review-sheet.js",
|
"/dashboard/static/review-sheet.js",
|
||||||
"/dashboard/static/work-route.js",
|
"/dashboard/static/work-route.js",
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
|
||||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v66" in source
|
assert "stackchain-dashboard-shell-v67" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user