feat: surface Today issue blockers (#435)
This commit is contained in:
parent
e0de45ea90
commit
05f9e06a10
|
|
@ -37,7 +37,10 @@ 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. Starting a Today work session also
|
||||
five-item Today plan syncs across the operator's devices. Adding an issue through **Plan Today**
|
||||
first previews its Gitea dependencies: unresolved blockers are listed with links and require the
|
||||
explicit **Add blocked item anyway** override, while an unavailable dependency lookup is reported
|
||||
as unknown rather than unblocked. 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);
|
||||
**Comment & next** on that current issue or pull request posts the handoff online or admits it
|
||||
|
|
|
|||
|
|
@ -213,6 +213,11 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||
.issue-sheet-header button { min-height:44px; }
|
||||
.issue-sheet-content { overflow-wrap:anywhere; white-space:pre-wrap; }
|
||||
.issue-blockers { max-width:100%; overflow-x:hidden; margin:16px 0; padding:12px; border:1px solid #b45309; border-radius:12px; background:#291b0c; }
|
||||
.issue-blockers h2 { margin-top:0; }
|
||||
.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:hover, .issue-blocker:focus-visible { border-color:#f59e0b; }
|
||||
.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-retry { min-height:44px; width:100%; }
|
||||
|
|
|
|||
|
|
@ -872,7 +872,9 @@
|
|||
const title = escapeHtml(item.title || 'Untitled work');
|
||||
const controls = selected ?
|
||||
'<div class="plan-today-item-actions"><button type="button" data-plan-move="up" data-plan-id="' + escAttr(id) + '"' + (index === 0 ? ' disabled' : '') + '>Up</button><button type="button" data-plan-move="down" data-plan-id="' + escAttr(id) + '"' + (index === planToday.snapshot().count - 1 ? ' disabled' : '') + '>Down</button><button type="button" data-plan-remove="' + escAttr(id) + '">Remove</button></div>' :
|
||||
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview</button><button type="button" data-plan-add="' + escAttr(id) + '">Add</button></div>';
|
||||
(item.kind === 'issue' ?
|
||||
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview to add</button></div>' :
|
||||
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview</button><button type="button" data-plan-add="' + escAttr(id) + '">Add</button></div>');
|
||||
return '<article class="plan-today-item"><div class="plan-today-item-copy"><span class="small">' + key + '</span><strong class="my-work-card-title">' + title + '</strong></div>' + controls + '</article>';
|
||||
}
|
||||
|
||||
|
|
@ -955,6 +957,7 @@
|
|||
function openPlanPreviewDetail(item, trigger) {
|
||||
qs('#plan-today-sheet').hidden = true;
|
||||
qs('#plan-preview-actions').hidden = false;
|
||||
if (item.kind === 'issue') renderPlanIssueDependencies(null, true);
|
||||
if (offlineWorkMode) {
|
||||
openRoutedWork(item, trigger);
|
||||
} else if (item.kind === 'update' && item.has_update) {
|
||||
|
|
@ -981,19 +984,68 @@
|
|||
onClose: (_item, trigger) => {
|
||||
closeOpenWorkSheets();
|
||||
qs('#plan-preview-actions').hidden = true;
|
||||
qs('#issue-blockers').hidden = true;
|
||||
qs('#issue-blocker-list').textContent = '';
|
||||
qs('#add-plan-preview').disabled = false;
|
||||
qs('#add-plan-preview').textContent = 'Add to Today & back';
|
||||
qs('#plan-today-sheet').hidden = false;
|
||||
renderPlanToday();
|
||||
requestAnimationFrame(() => trigger?.focus());
|
||||
},
|
||||
});
|
||||
|
||||
function renderPlanIssueDependencies(detail, loading = false) {
|
||||
const preview = planTodayPreview.snapshot();
|
||||
if (!preview.open || preview.item?.kind !== 'issue') return;
|
||||
const panel = qs('#issue-blockers');
|
||||
const list = qs('#issue-blocker-list');
|
||||
const status = qs('#issue-blocker-status');
|
||||
const addButton = qs('#add-plan-preview');
|
||||
panel.hidden = false;
|
||||
list.innerHTML = '';
|
||||
addButton.disabled = loading;
|
||||
addButton.dataset.planOverride = '';
|
||||
if (loading) {
|
||||
status.textContent = 'Checking unresolved blockers…';
|
||||
addButton.textContent = 'Checking blockers…';
|
||||
return;
|
||||
}
|
||||
const available = detail?.dependencies_available === true;
|
||||
const dependencies = Array.isArray(detail?.dependencies) ? detail.dependencies : [];
|
||||
planTodayPreview.setDependencies({ available, dependencies });
|
||||
if (!available) {
|
||||
status.textContent = 'Blocker status unavailable. Adding requires an explicit override.';
|
||||
addButton.textContent = 'Add without blocker status anyway & back';
|
||||
addButton.dataset.planOverride = 'true';
|
||||
return;
|
||||
}
|
||||
if (dependencies.length) {
|
||||
list.innerHTML = dependencies.map(blocker =>
|
||||
'<a class="issue-blocker" href="' + escAttr(blocker.url || '#') + '" target="_blank" rel="noopener noreferrer"><strong>' +
|
||||
escapeHtml(blocker.repository + '#' + blocker.number) + ' · ' + escapeHtml(blocker.title || 'Untitled blocker') +
|
||||
'</strong><span class="small">State: ' + escapeHtml(blocker.state || 'open') + '</span></a>'
|
||||
).join('');
|
||||
status.textContent = dependencies.length + (dependencies.length === 1 ? ' unresolved blocker.' : ' unresolved blockers.');
|
||||
addButton.textContent = 'Add blocked item anyway & back';
|
||||
addButton.dataset.planOverride = 'true';
|
||||
return;
|
||||
}
|
||||
panel.hidden = true;
|
||||
status.textContent = '';
|
||||
addButton.textContent = 'Add to Today & back';
|
||||
}
|
||||
|
||||
let addPlanPreviewOnReturn = false;
|
||||
let addPlanPreviewOverride = false;
|
||||
|
||||
qs('#back-to-plan').addEventListener('click', () => {
|
||||
addPlanPreviewOnReturn = false;
|
||||
addPlanPreviewOverride = false;
|
||||
taskOverlayHistory.close();
|
||||
});
|
||||
qs('#add-plan-preview').addEventListener('click', () => {
|
||||
addPlanPreviewOnReturn = true;
|
||||
addPlanPreviewOverride = qs('#add-plan-preview').dataset.planOverride === 'true';
|
||||
taskOverlayHistory.close();
|
||||
});
|
||||
|
||||
|
|
@ -1783,6 +1835,7 @@
|
|||
const detail = offlineDetail || await issueController.load(item);
|
||||
if (selectedIssue !== item) return;
|
||||
selectedIssueDetail = detail;
|
||||
renderPlanIssueDependencies(detail);
|
||||
issueConversation = issueController.conversation(item, detail.conversation);
|
||||
qs('#issue-sheet-title').textContent = detail.title || 'Assigned issue';
|
||||
qs('#issue-sheet-body').innerHTML = renderMarkdown(detail.body || 'No description provided.');
|
||||
|
|
@ -2936,9 +2989,11 @@
|
|||
qs('#open-palette').focus();
|
||||
}
|
||||
if (previous === 'plan-today-preview' && kind !== 'plan-today-preview') {
|
||||
if (addPlanPreviewOnReturn) planTodayPreview.close({ add:true });
|
||||
if (addPlanPreviewOnReturn && addPlanPreviewOverride) planTodayPreview.close({ add:true, override:true });
|
||||
else if (addPlanPreviewOnReturn) planTodayPreview.close({ add:true });
|
||||
else planTodayPreview.close();
|
||||
addPlanPreviewOnReturn = false;
|
||||
addPlanPreviewOverride = false;
|
||||
}
|
||||
if (previous === 'plan-today' && kind !== 'plan-today' && kind !== 'plan-today-preview') closePlanToday(false);
|
||||
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
|
||||
|
|
|
|||
|
|
@ -262,6 +262,11 @@
|
|||
<button class="issue-retry" id="retry-issue-load" type="button" hidden>Reload latest issue</button>
|
||||
<div class="row"><span id="issue-labels"></span><span class="small" id="issue-assignees"></span></div>
|
||||
<div class="issue-sheet-content markdown-content" id="issue-sheet-body"></div>
|
||||
<section class="issue-blockers" id="issue-blockers" aria-labelledby="issue-blockers-title" hidden>
|
||||
<h2 id="issue-blockers-title">Blocked by</h2>
|
||||
<div class="issue-blocker-list" id="issue-blocker-list"></div>
|
||||
<div id="issue-blocker-status" class="small" aria-live="polite"></div>
|
||||
</section>
|
||||
<h2>Full conversation</h2>
|
||||
<div id="issue-comments"></div>
|
||||
<button class="conversation-more" id="load-older-issue-comments" type="button" hidden>Load older messages</button>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,19 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function close({ add = false } = {}) {
|
||||
function setDependencies({ available, dependencies } = {}) {
|
||||
if (!current) return false;
|
||||
current.dependencies_available = available === true;
|
||||
current.dependencies = Array.isArray(dependencies) ? dependencies.filter(item => item?.state === 'open') : [];
|
||||
current.requires_override = !current.dependencies_available || current.dependencies.length > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
function close({ add = false, override = false } = {}) {
|
||||
if (!current) return 'closed';
|
||||
if (add && current.requires_override && !override) {
|
||||
return current.dependencies_available ? 'blocked' : 'dependencies-unavailable';
|
||||
}
|
||||
const { item, trigger, scroll } = current;
|
||||
let result = 'returned';
|
||||
if (add) {
|
||||
|
|
@ -37,9 +48,16 @@
|
|||
}
|
||||
|
||||
function snapshot() {
|
||||
return current ? { open:true, item:current.item, trigger:current.trigger, scroll:current.scroll } : { open:false };
|
||||
if (!current) return { open:false };
|
||||
const state = { open:true, item:current.item, trigger:current.trigger, scroll:current.scroll };
|
||||
if (Object.prototype.hasOwnProperty.call(current, 'dependencies_available')) {
|
||||
state.dependencies_available = current.dependencies_available;
|
||||
state.dependencies = current.dependencies;
|
||||
state.requires_override = current.requires_override;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
return { open, close, snapshot };
|
||||
return { open, close, setDependencies, snapshot };
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v72';
|
||||
const CACHE = 'stackchain-dashboard-shell-v73';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
|
|
|||
|
|
@ -1343,9 +1343,19 @@ async def issue_conversation_page(
|
|||
|
||||
async def issue_detail(repository: str, number: int) -> dict:
|
||||
base = f"repos/{repository}/issues/{number}"
|
||||
issue, conversation = await asyncio.gather(
|
||||
fetch(base), issue_conversation_page(repository, number)
|
||||
|
||||
async def load_dependencies() -> tuple[bool, list[dict]]:
|
||||
try:
|
||||
return True, await issue_dependencies(repository, number)
|
||||
except Exception:
|
||||
return False, []
|
||||
|
||||
issue, conversation, dependency_result = await asyncio.gather(
|
||||
fetch(base),
|
||||
issue_conversation_page(repository, number),
|
||||
load_dependencies(),
|
||||
)
|
||||
dependencies_available, dependencies = dependency_result
|
||||
if not isinstance(issue, dict):
|
||||
raise ValueError("Gitea issue response was not an object")
|
||||
labels_value = issue.get("labels")
|
||||
|
|
@ -1377,11 +1387,43 @@ async def issue_detail(repository: str, number: int) -> dict:
|
|||
assignee["login"] for assignee in assignees
|
||||
if isinstance(assignee, dict) and isinstance(assignee.get("login"), str)
|
||||
],
|
||||
"dependencies_available": dependencies_available,
|
||||
"dependencies": dependencies,
|
||||
"comments": normalized_comments,
|
||||
"conversation": conversation,
|
||||
}
|
||||
|
||||
|
||||
async def issue_dependencies(repository: str, number: int, limit: int = 20) -> list[dict]:
|
||||
"""Return bounded open prerequisites for an issue."""
|
||||
response = await _get_client().get(
|
||||
f"/api/v1/repos/{repository}/issues/{number}/dependencies",
|
||||
headers=_auth(),
|
||||
params={"limit": limit},
|
||||
)
|
||||
response.raise_for_status()
|
||||
value = response.json()
|
||||
items = value if isinstance(value, list) else []
|
||||
dependencies = []
|
||||
for item in items[:limit]:
|
||||
if not isinstance(item, dict) or item.get("state") != "open":
|
||||
continue
|
||||
repo_value = item.get("repository")
|
||||
repo = repo_value if isinstance(repo_value, dict) else {}
|
||||
dependency_repository = repo.get("full_name")
|
||||
dependency_number = item.get("number")
|
||||
if not isinstance(dependency_repository, str) or not isinstance(dependency_number, int):
|
||||
continue
|
||||
dependencies.append({
|
||||
"repository": dependency_repository,
|
||||
"number": dependency_number,
|
||||
"title": item.get("title", "") if isinstance(item.get("title"), str) else "",
|
||||
"state": "open",
|
||||
"url": _safe_web_url(item.get("html_url")),
|
||||
})
|
||||
return dependencies
|
||||
|
||||
|
||||
async def update_assigned_issue(
|
||||
repository: str,
|
||||
number: int,
|
||||
|
|
|
|||
|
|
@ -1543,6 +1543,25 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
|
||||
async def handler(request):
|
||||
requests.append((request.method, request.url.path, request.url.query.decode()))
|
||||
if request.url.path.endswith("/dependencies"):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json=[
|
||||
{
|
||||
"number": 3,
|
||||
"title": "Restore signing service",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/platform/issues/3",
|
||||
"repository": {"full_name": "stackchain/platform"},
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "Completed prerequisite",
|
||||
"state": "closed",
|
||||
"repository": {"full_name": "stackchain/platform"},
|
||||
},
|
||||
],
|
||||
)
|
||||
if request.url.path.endswith("/comments"):
|
||||
return httpx.Response(
|
||||
200,
|
||||
|
|
@ -1580,6 +1599,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
assert requests == [
|
||||
("GET", "/api/v1/repos/stackchain/api/issues/7", ""),
|
||||
("GET", "/api/v1/repos/stackchain/api/issues/7/comments", "limit=20&page=1"),
|
||||
("GET", "/api/v1/repos/stackchain/api/issues/7/dependencies", "limit=20"),
|
||||
]
|
||||
assert result == {
|
||||
"repository": "stackchain/api",
|
||||
|
|
@ -1593,6 +1613,16 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"url": "https://forge.example/stackchain/api/issues/7",
|
||||
"labels": ["P1"],
|
||||
"assignees": ["timmy"],
|
||||
"dependencies_available": True,
|
||||
"dependencies": [
|
||||
{
|
||||
"repository": "stackchain/platform",
|
||||
"number": 3,
|
||||
"title": "Restore signing service",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/platform/issues/3",
|
||||
}
|
||||
],
|
||||
"comments": [
|
||||
{
|
||||
"id": 81,
|
||||
|
|
@ -1619,6 +1649,31 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_gitea_issue_detail_keeps_planning_usable_when_dependencies_are_unavailable():
|
||||
async def handler(request):
|
||||
if request.url.path.endswith("/dependencies"):
|
||||
return httpx.Response(503, json={"message": "upstream timeout"})
|
||||
if request.url.path.endswith("/comments"):
|
||||
return httpx.Response(200, json=[])
|
||||
return httpx.Response(200, json={
|
||||
"number": 7,
|
||||
"title": "Fix mobile flow",
|
||||
"state": "open",
|
||||
"assignees": [{"login": "timmy"}],
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.issue_detail("stackchain/api", 7)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert result["title"] == "Fix mobile flow"
|
||||
assert result["dependencies_available"] is False
|
||||
assert result["dependencies"] == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_gitea_conversation_page_opens_newest_page_and_reports_older_history():
|
||||
requests = []
|
||||
|
|
|
|||
|
|
@ -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-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" 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-v72" in worker
|
||||
assert "stackchain-dashboard-shell-v73" 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-v72" in worker
|
||||
assert "stackchain-dashboard-shell-v73" in worker
|
||||
|
|
|
|||
|
|
@ -126,6 +126,64 @@ process.stdout.write(JSON.stringify({{first, second, scroll, events, planner:pla
|
|||
}
|
||||
|
||||
|
||||
def test_plan_today_preview_requires_explicit_override_for_blocked_or_unknown_issue():
|
||||
script = f"""
|
||||
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
|
||||
const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))});
|
||||
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
|
||||
const planner = createPlanToday({{
|
||||
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
|
||||
save: () => true,
|
||||
}});
|
||||
planner.open([], [item(2), item(3)]);
|
||||
const preview = createPlanTodayPreview({{
|
||||
planner,
|
||||
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
|
||||
}});
|
||||
preview.open(item(2));
|
||||
preview.setDependencies({{available:true, dependencies:[{{repository:'stackchain/api', number:9, title:'Restore API', state:'open'}}]}});
|
||||
const blockedState = preview.snapshot();
|
||||
const blocked = preview.close({{add:true}});
|
||||
const overridden = preview.close({{add:true, override:true}});
|
||||
preview.open(item(3));
|
||||
preview.setDependencies({{available:false, dependencies:[]}});
|
||||
const unavailableState = preview.snapshot();
|
||||
const unavailable = preview.close({{add:true}});
|
||||
const unavailableOverride = preview.close({{add:true, override:true}});
|
||||
process.stdout.write(JSON.stringify({{blockedState, blocked, overridden, unavailableState, unavailable, unavailableOverride, planner:planner.snapshot()}}));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"blockedState": {
|
||||
"open": True,
|
||||
"item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 2, "title": "Issue 2"},
|
||||
"trigger": None,
|
||||
"scroll": 0,
|
||||
"dependencies_available": True,
|
||||
"dependencies": [{"repository": "stackchain/api", "number": 9, "title": "Restore API", "state": "open"}],
|
||||
"requires_override": True,
|
||||
},
|
||||
"blocked": "blocked",
|
||||
"overridden": "added",
|
||||
"unavailableState": {
|
||||
"open": True,
|
||||
"item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 3, "title": "Issue 3"},
|
||||
"trigger": None,
|
||||
"scroll": 0,
|
||||
"dependencies_available": False,
|
||||
"dependencies": [],
|
||||
"requires_override": True,
|
||||
},
|
||||
"unavailable": "dependencies-unavailable",
|
||||
"unavailableOverride": "added",
|
||||
"planner": {
|
||||
"open": True,
|
||||
"ids": ["issue:stackchain/dashboard:2:", "issue:stackchain/dashboard:3:"],
|
||||
"count": 2,
|
||||
"limit": 5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
||||
html = await dashboard()
|
||||
|
|
@ -150,6 +208,12 @@ async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
|||
assert 'id="add-plan-preview"' in html
|
||||
assert "taskOverlayHistory.open('plan-today-preview')" in html
|
||||
assert "planTodayPreview.close({ add:true })" in html
|
||||
assert 'id="issue-blockers"' in html
|
||||
assert "planTodayPreview.setDependencies({" in html
|
||||
assert "Add blocked item anyway & back" in html
|
||||
assert "Blocker status unavailable" in html
|
||||
assert ".issue-blockers { max-width:100%; overflow-x:hidden;" in html
|
||||
assert ".issue-blocker { min-width:0; overflow-wrap:anywhere;" in html
|
||||
assert "Preview unavailable offline" in html
|
||||
|
||||
|
||||
|
|
@ -168,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():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/plan-today.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():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" 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():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/today-completion.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():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/create-issue-sheet.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():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" 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-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.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():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" 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-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" 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-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" 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():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/queue-today.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-v72" in source
|
||||
assert "stackchain-dashboard-shell-v73" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user