feat: work Today blockers in-app (#443)
This commit is contained in:
parent
60bc7cd686
commit
a0aa3c0a22
|
|
@ -226,6 +226,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.issue-blocker-list { display:grid; gap:8px; }
|
.issue-blocker-list { display:grid; gap:8px; }
|
||||||
.issue-blocker { min-width:0; overflow-wrap:anywhere; display:grid; gap:4px; padding:10px; border:1px solid #92400e; border-radius:10px; color:#fef3c7; text-decoration:none; }
|
.issue-blocker { min-width:0; overflow-wrap:anywhere; display:grid; gap:4px; padding:10px; border:1px solid #92400e; border-radius:10px; color:#fef3c7; text-decoration:none; }
|
||||||
.issue-blocker:hover, .issue-blocker:focus-visible { border-color:#f59e0b; }
|
.issue-blocker:hover, .issue-blocker:focus-visible { border-color:#f59e0b; }
|
||||||
|
.today-readiness-blocker { width:100%; min-height:44px; text-align:left; background:#291b0c; }
|
||||||
.issue-planning { max-width:100%; margin-top:16px; border:1px solid #2a496e; border-radius:12px; padding:0 12px 12px; overflow-x:hidden; }
|
.issue-planning { max-width:100%; margin-top:16px; border:1px solid #2a496e; border-radius:12px; padding:0 12px 12px; overflow-x:hidden; }
|
||||||
.issue-planning > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; font-weight:700; }
|
.issue-planning > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; font-weight:700; }
|
||||||
.issue-planning-retry { min-height:44px; width:100%; }
|
.issue-planning-retry { min-height:44px; width:100%; }
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let todayReadinessTrigger = null;
|
let todayReadinessTrigger = null;
|
||||||
|
let todayReadinessBlockerFocus = null;
|
||||||
|
let searchPreviewReturnKind = null;
|
||||||
function inspectTodayDependencies(item) {
|
function inspectTodayDependencies(item) {
|
||||||
if (offlineWorkMode) {
|
if (offlineWorkMode) {
|
||||||
const login = confirmedOwnerLogin || String(offlineWorkStore.load()?.user?.login || '').trim();
|
const login = confirmedOwnerLogin || String(offlineWorkStore.load()?.user?.login || '').trim();
|
||||||
|
|
@ -746,12 +748,17 @@
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTodayReadiness(navigate = true) {
|
function suspendTodayReadiness() {
|
||||||
qs('#today-readiness-sheet').hidden = true;
|
qs('#today-readiness-sheet').hidden = true;
|
||||||
document.body.classList.remove('task-overlay-open');
|
document.body.classList.remove('task-overlay-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTodayReadiness(navigate = true) {
|
||||||
|
suspendTodayReadiness();
|
||||||
if (navigate) taskOverlayHistory.close();
|
if (navigate) taskOverlayHistory.close();
|
||||||
else {
|
else {
|
||||||
todayReadiness.cancel();
|
todayReadiness.cancel();
|
||||||
|
todayReadinessBlockerFocus = null;
|
||||||
requestAnimationFrame(() => todayReadinessTrigger?.focus());
|
requestAnimationFrame(() => todayReadinessTrigger?.focus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -765,18 +772,32 @@
|
||||||
: state.dependencies.length + (state.dependencies.length === 1 ? ' open dependency must finish first.' : ' open dependencies must finish first.');
|
: state.dependencies.length + (state.dependencies.length === 1 ? ' open dependency must finish first.' : ' open dependencies must finish first.');
|
||||||
qs('#today-readiness-item').innerHTML = '<strong>' + escapeHtml(state.target.title || 'Untitled issue') +
|
qs('#today-readiness-item').innerHTML = '<strong>' + escapeHtml(state.target.title || 'Untitled issue') +
|
||||||
'</strong><div class="small">' + escapeHtml(state.target.repository + '#' + state.target.number) + '</div>';
|
'</strong><div class="small">' + escapeHtml(state.target.repository + '#' + state.target.number) + '</div>';
|
||||||
qs('#today-readiness-blockers').innerHTML = state.dependencies.map(blocker =>
|
const blockers = qs('#today-readiness-blockers');
|
||||||
'<a class="issue-blocker" href="' + escAttr(blocker.url || '#') + '" target="_blank" rel="noopener noreferrer"><strong>' +
|
blockers.innerHTML = state.dependencies.map((blocker, index) =>
|
||||||
|
'<button class="issue-blocker today-readiness-blocker" type="button" data-today-blocker-index="' + index + '"><strong>' +
|
||||||
escapeHtml(blocker.repository + '#' + blocker.number) + ' · ' + escapeHtml(blocker.title || 'Untitled blocker') +
|
escapeHtml(blocker.repository + '#' + blocker.number) + ' · ' + escapeHtml(blocker.title || 'Untitled blocker') +
|
||||||
'</strong><span class="small">State: ' + escapeHtml(blocker.state || 'open') + '</span></a>'
|
'</strong><span class="small">State: ' + escapeHtml(blocker.state || 'open') + ' · Preview and work blocker</span></button>'
|
||||||
).join('');
|
).join('');
|
||||||
|
blockers.querySelectorAll('[data-today-blocker-index]').forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const blocker = state.dependencies[Number(button.dataset.todayBlockerIndex)];
|
||||||
|
todayReadinessBlockerFocus = blocker?.repository + '#' + blocker?.number;
|
||||||
|
todayReadiness.previewBlocker(blocker);
|
||||||
|
});
|
||||||
|
});
|
||||||
qs('#today-readiness-next').hidden = !state.nextReady;
|
qs('#today-readiness-next').hidden = !state.nextReady;
|
||||||
if (state.nextReady) qs('#today-readiness-next').textContent = 'Start next ready · ' +
|
if (state.nextReady) qs('#today-readiness-next').textContent = 'Start next ready · ' +
|
||||||
(state.nextReady.title || state.nextReady.key || 'work item');
|
(state.nextReady.title || state.nextReady.key || 'work item');
|
||||||
qs('#today-readiness-retry').hidden = !unknown;
|
qs('#today-readiness-retry').hidden = !unknown;
|
||||||
qs('#today-readiness-sheet').hidden = false;
|
qs('#today-readiness-sheet').hidden = false;
|
||||||
document.body.classList.add('task-overlay-open');
|
document.body.classList.add('task-overlay-open');
|
||||||
requestAnimationFrame(() => (state.nextReady ? qs('#today-readiness-next') : qs('#today-readiness-anyway')).focus());
|
requestAnimationFrame(() => {
|
||||||
|
const blocker = state.dependencies.findIndex(candidate =>
|
||||||
|
candidate?.repository + '#' + candidate?.number === todayReadinessBlockerFocus
|
||||||
|
);
|
||||||
|
if (blocker >= 0) blockers.querySelector('[data-today-blocker-index="' + blocker + '"]')?.focus();
|
||||||
|
else (state.nextReady ? qs('#today-readiness-next') : qs('#today-readiness-anyway')).focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function performTodayTransition(action, item = null) {
|
function performTodayTransition(action, item = null) {
|
||||||
|
|
@ -804,6 +825,11 @@
|
||||||
renderTodayReadiness(state);
|
renderTodayReadiness(state);
|
||||||
taskOverlayHistory.open('today-readiness');
|
taskOverlayHistory.open('today-readiness');
|
||||||
},
|
},
|
||||||
|
onPreview:blocker => {
|
||||||
|
searchPreviewReturnKind = 'today-readiness';
|
||||||
|
searchPreview.open({ ...blocker, kind:'issue' }).catch(() => {});
|
||||||
|
taskOverlayHistory.open('search-preview');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function runTodayTransition(action) {
|
function runTodayTransition(action) {
|
||||||
|
|
@ -2892,6 +2918,8 @@
|
||||||
const status = qs('#search-preview-status');
|
const status = qs('#search-preview-status');
|
||||||
const claimButton = qs('#claim-search-result');
|
const claimButton = qs('#claim-search-result');
|
||||||
const startButton = qs('#start-search-result');
|
const startButton = qs('#start-search-result');
|
||||||
|
qs('#close-search-preview').textContent = searchPreviewReturnKind === 'today-readiness'
|
||||||
|
? 'Back to blockers' : 'Back to search';
|
||||||
if (state.status === 'closed') {
|
if (state.status === 'closed') {
|
||||||
sheet.classList.remove('open');
|
sheet.classList.remove('open');
|
||||||
return;
|
return;
|
||||||
|
|
@ -3003,6 +3031,7 @@
|
||||||
qs('#cmd-input').value = '';
|
qs('#cmd-input').value = '';
|
||||||
} else {
|
} else {
|
||||||
mobileSearchViewport.rememberScroll();
|
mobileSearchViewport.rememberScroll();
|
||||||
|
searchPreviewReturnKind = 'search';
|
||||||
searchPreview.open(item.result).catch(() => {});
|
searchPreview.open(item.result).catch(() => {});
|
||||||
taskOverlayHistory.open('search-preview');
|
taskOverlayHistory.open('search-preview');
|
||||||
}
|
}
|
||||||
|
|
@ -3062,6 +3091,7 @@
|
||||||
searchPreview.close();
|
searchPreview.close();
|
||||||
mobileSearchViewport.close();
|
mobileSearchViewport.close();
|
||||||
}
|
}
|
||||||
|
searchPreviewReturnKind = null;
|
||||||
}
|
}
|
||||||
if (previous === 'search' && kind !== 'search' && kind !== 'search-preview') {
|
if (previous === 'search' && kind !== 'search' && kind !== 'search-preview') {
|
||||||
qs('#cmd-palette').classList.remove('open');
|
qs('#cmd-palette').classList.remove('open');
|
||||||
|
|
@ -3077,7 +3107,10 @@
|
||||||
addPlanPreviewOverride = false;
|
addPlanPreviewOverride = false;
|
||||||
}
|
}
|
||||||
if (previous === 'plan-today' && kind !== 'plan-today' && kind !== 'plan-today-preview') closePlanToday(false);
|
if (previous === 'plan-today' && kind !== 'plan-today' && kind !== 'plan-today-preview') closePlanToday(false);
|
||||||
if (previous === 'today-readiness' && kind !== 'today-readiness') closeTodayReadiness(false);
|
if (previous === 'today-readiness' && kind !== 'today-readiness') {
|
||||||
|
if (kind === 'search-preview') suspendTodayReadiness();
|
||||||
|
else closeTodayReadiness(false);
|
||||||
|
}
|
||||||
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
|
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
|
||||||
if (kind === 'find' && previous !== 'find') openFindWorkSheet(false);
|
if (kind === 'find' && previous !== 'find') openFindWorkSheet(false);
|
||||||
if (kind === 'search' && previous !== 'search-preview') openCommandPalette(false);
|
if (kind === 'search' && previous !== 'search-preview') openCommandPalette(false);
|
||||||
|
|
|
||||||
|
|
@ -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-v74';
|
const CACHE = 'stackchain-dashboard-shell-v75';
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
return function createTodayReadiness({ inspect, onOpen = () => {}, onGate = () => {} }) {
|
return function createTodayReadiness({ inspect, onOpen = () => {}, onGate = () => {}, onPreview = () => {} }) {
|
||||||
let pending = null;
|
let pending = null;
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
|
|
||||||
|
|
@ -67,9 +67,20 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function previewBlocker(blocker) {
|
||||||
|
if (!pending || !blocker) return false;
|
||||||
|
const current = pending.dependencies.find(dependency =>
|
||||||
|
dependency?.repository === blocker.repository && Number(dependency?.number) === Number(blocker.number)
|
||||||
|
);
|
||||||
|
if (!current) return false;
|
||||||
|
onPreview(current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
run,
|
run,
|
||||||
retry: () => pending ? run(pending.action, pending.items, pending.target) : Promise.resolve('closed'),
|
retry: () => pending ? run(pending.action, pending.items, pending.target) : Promise.resolve('closed'),
|
||||||
|
previewBlocker,
|
||||||
startNextReady: () => open(pending?.nextReady),
|
startNextReady: () => open(pending?.nextReady),
|
||||||
workAnyway: () => open(pending?.target),
|
workAnyway: () => open(pending?.target),
|
||||||
cancel() {
|
cancel() {
|
||||||
|
|
|
||||||
|
|
@ -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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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-v74" in worker
|
assert "stackchain-dashboard-shell-v75" 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-v74" in worker
|
assert "stackchain-dashboard-shell-v75" in worker
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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
|
||||||
|
|
@ -131,7 +131,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_offline_review_next_ships_today_completion_atomically():
|
def test_offline_review_next_ships_today_completion_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v74" in source
|
assert "stackchain-dashboard-shell-v75" in source
|
||||||
assert "BASE + 'static/today-completion.js'" in source
|
assert "BASE + 'static/today-completion.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||||
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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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
|
||||||
|
|
||||||
|
|
@ -147,14 +147,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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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
|
||||||
|
|
@ -163,21 +163,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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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-v74" in source
|
assert "stackchain-dashboard-shell-v75" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -358,7 +358,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
||||||
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v74" in source
|
assert "stackchain-dashboard-shell-v75" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,41 @@ readiness.run('resume', items, items[0]).then(async result => {{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_readiness_previews_only_a_current_blocker_without_closing_or_reordering_gate():
|
||||||
|
script = f"""
|
||||||
|
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
||||||
|
const previews = [];
|
||||||
|
const items = [1, 2].map(number => ({{kind:'issue', repository:'r', number}}));
|
||||||
|
const blocker = {{repository:'r', number:99, title:'Prerequisite', state:'open'}};
|
||||||
|
const readiness = createTodayReadiness({{
|
||||||
|
inspect: async candidate => candidate.number === 1
|
||||||
|
? {{available:true, dependencies:[blocker]}}
|
||||||
|
: {{available:true, dependencies:[]}},
|
||||||
|
onPreview: candidate => previews.push(candidate.number),
|
||||||
|
}});
|
||||||
|
(async () => {{
|
||||||
|
await readiness.run('start', items, items[0]);
|
||||||
|
const accepted = readiness.previewBlocker(blocker);
|
||||||
|
const rejected = readiness.previewBlocker({{repository:'r', number:100}});
|
||||||
|
const snapshot = readiness.snapshot();
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
accepted, rejected, previews,
|
||||||
|
open:snapshot.status === 'blocked',
|
||||||
|
target:snapshot.target.number,
|
||||||
|
order:snapshot.items.map(item => item.number),
|
||||||
|
}}));
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
assert json.loads(run_node(script)) == {
|
||||||
|
"accepted": True,
|
||||||
|
"rejected": False,
|
||||||
|
"previews": [99],
|
||||||
|
"open": True,
|
||||||
|
"target": 1,
|
||||||
|
"order": [1, 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_readiness_never_calls_unknown_ready_and_supports_retry_or_explicit_override():
|
def test_readiness_never_calls_unknown_ready_and_supports_retry_or_explicit_override():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
||||||
|
|
@ -170,10 +205,23 @@ async def test_dashboard_exposes_mobile_today_readiness_gate_and_runtime():
|
||||||
assert "runTodayTransition('complete'" in html
|
assert "runTodayTransition('complete'" in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert 'data-today-blocker-index="' in html
|
||||||
|
assert "todayReadiness.previewBlocker(blocker)" in html
|
||||||
|
assert "searchPreview.open({ ...blocker, kind:'issue' })" in html
|
||||||
|
assert "taskOverlayHistory.open('search-preview')" in html
|
||||||
|
assert "suspendTodayReadiness()" in html
|
||||||
|
assert "todayReadiness.cancel()" in html
|
||||||
|
assert "todayReadinessBlockerFocus" in html
|
||||||
|
|
||||||
|
|
||||||
def test_readiness_runtime_is_available_in_offline_shell():
|
def test_readiness_runtime_is_available_in_offline_shell():
|
||||||
service_worker = SERVICE_WORKER.read_text()
|
service_worker = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "const CACHE = 'stackchain-dashboard-shell-v74';" in service_worker
|
assert "const CACHE = 'stackchain-dashboard-shell-v75';" in service_worker
|
||||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -185,3 +233,5 @@ def test_mobile_readiness_gate_has_touch_safe_wrapping_actions():
|
||||||
assert ".today-readiness-item" in css
|
assert ".today-readiness-item" in css
|
||||||
assert "overflow-wrap:anywhere" in css
|
assert "overflow-wrap:anywhere" in css
|
||||||
assert "env(safe-area-inset-bottom)" in css
|
assert "env(safe-area-inset-bottom)" in css
|
||||||
|
assert ".today-readiness-blocker" in css
|
||||||
|
assert "min-height:44px" in css
|
||||||
|
|
|
||||||
|
|
@ -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-v74" in source
|
assert "stackchain-dashboard-shell-v75" 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