Merge pull request 'Finish Today items without changing Gitea' (#422) from timmy/421-done-today-next into main
This commit is contained in:
commit
7b1c9a608c
|
|
@ -251,7 +251,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.my-work-actions { display:flex; flex-wrap:wrap; gap:8px; }
|
.my-work-actions { display:flex; flex-wrap:wrap; gap:8px; }
|
||||||
.start-work-session { min-height:44px; }
|
.start-work-session { min-height:44px; }
|
||||||
.resume-today-session, .end-today-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 { position:sticky; bottom:0; z-index:5; display:grid; grid-template-columns:repeat(3,minmax(0,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[hidden] { display:none; }
|
||||||
.work-session-nav [data-work-session-progress] { grid-column:1 / -1; text-align:center; }
|
.work-session-nav [data-work-session-progress] { grid-column:1 / -1; text-align:center; }
|
||||||
.work-session-nav button { min-height:44px; width:100%; }
|
.work-session-nav button { min-height:44px; width:100%; }
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,9 @@
|
||||||
document.querySelectorAll('[data-work-session-next]').forEach(button => {
|
document.querySelectorAll('[data-work-session-next]').forEach(button => {
|
||||||
button.textContent = state.can_next ? 'Next work item' : 'Finish session';
|
button.textContent = state.can_next ? 'Next work item' : 'Finish session';
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('[data-work-session-complete]').forEach(button => {
|
||||||
|
button.hidden = !workSession.checkpointed();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
closeOpenWorkSheets();
|
closeOpenWorkSheets();
|
||||||
|
|
@ -764,6 +767,21 @@
|
||||||
warm: warmTodayOffline,
|
warm: warmTodayOffline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedSessionItem = kind => ({
|
||||||
|
issue: selectedIssue,
|
||||||
|
pull: selectedPull,
|
||||||
|
review: selectedReview,
|
||||||
|
update: selectedUpdate,
|
||||||
|
})[kind] || null;
|
||||||
|
const completeTodayItem = createTodayCompletion({
|
||||||
|
todayWork,
|
||||||
|
todaySync,
|
||||||
|
workSession,
|
||||||
|
refresh: () => refreshMyWorkView({ reconcileSession:false }),
|
||||||
|
warm: warmTodayOffline,
|
||||||
|
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||||
|
});
|
||||||
|
|
||||||
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 =>
|
||||||
|
|
@ -3922,6 +3940,11 @@
|
||||||
document.querySelectorAll('[data-work-session-next]').forEach(button =>
|
document.querySelectorAll('[data-work-session-next]').forEach(button =>
|
||||||
button.addEventListener('click', () => workSession.next())
|
button.addEventListener('click', () => workSession.next())
|
||||||
);
|
);
|
||||||
|
document.querySelectorAll('[data-work-session-complete]').forEach(button =>
|
||||||
|
button.addEventListener('click', () =>
|
||||||
|
completeTodayItem(selectedSessionItem(button.dataset.workSessionComplete))
|
||||||
|
)
|
||||||
|
);
|
||||||
qs('#load-more-notifications').addEventListener('click', () =>
|
qs('#load-more-notifications').addEventListener('click', () =>
|
||||||
notificationPager.loadMore(lastNotifications)
|
notificationPager.loadMore(lastNotifications)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,7 @@
|
||||||
<nav class="work-session-nav" aria-label="Work session" hidden>
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
<span class="small" aria-live="polite" data-work-session-progress></span>
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
<button type="button" data-work-session-previous>Previous</button>
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-complete="issue" hidden>Done for Today & next</button>
|
||||||
<button type="button" data-work-session-next>Next work item</button>
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
</nav>
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -448,6 +449,7 @@
|
||||||
<nav class="work-session-nav" aria-label="Work session" hidden>
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
<span class="small" aria-live="polite" data-work-session-progress></span>
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
<button type="button" data-work-session-previous>Previous</button>
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-complete="update" hidden>Done for Today & next</button>
|
||||||
<button type="button" data-work-session-next>Next work item</button>
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
</nav>
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -488,6 +490,7 @@
|
||||||
<nav class="work-session-nav" aria-label="Work session" hidden>
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
<span class="small" aria-live="polite" data-work-session-progress></span>
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
<button type="button" data-work-session-previous>Previous</button>
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-complete="pull" hidden>Done for Today & next</button>
|
||||||
<button type="button" data-work-session-next>Next work item</button>
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
</nav>
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -552,6 +555,7 @@
|
||||||
<nav class="work-session-nav" aria-label="Work session" hidden>
|
<nav class="work-session-nav" aria-label="Work session" hidden>
|
||||||
<span class="small" aria-live="polite" data-work-session-progress></span>
|
<span class="small" aria-live="polite" data-work-session-progress></span>
|
||||||
<button type="button" data-work-session-previous>Previous</button>
|
<button type="button" data-work-session-previous>Previous</button>
|
||||||
|
<button type="button" data-work-session-complete="review" hidden>Done for Today & next</button>
|
||||||
<button type="button" data-work-session-next>Next work item</button>
|
<button type="button" data-work-session-next>Next work item</button>
|
||||||
</nav>
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -596,6 +600,7 @@
|
||||||
<script src="static/my-work.js"></script>
|
<script src="static/my-work.js"></script>
|
||||||
<script src="static/card-planning.js"></script>
|
<script src="static/card-planning.js"></script>
|
||||||
<script src="static/today-work.js"></script>
|
<script src="static/today-work.js"></script>
|
||||||
|
<script src="static/today-completion.js"></script>
|
||||||
<script src="static/plan-today.js"></script>
|
<script src="static/plan-today.js"></script>
|
||||||
<script src="static/plan-today-preview.js"></script>
|
<script src="static/plan-today-preview.js"></script>
|
||||||
<script src="static/today-sync.js"></script>
|
<script src="static/today-sync.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -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-v67';
|
const CACHE = 'stackchain-dashboard-shell-v68';
|
||||||
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;
|
||||||
|
|
@ -27,6 +27,7 @@ const SHELL = [
|
||||||
BASE + 'static/my-work.js',
|
BASE + 'static/my-work.js',
|
||||||
BASE + 'static/card-planning.js',
|
BASE + 'static/card-planning.js',
|
||||||
BASE + 'static/today-work.js',
|
BASE + 'static/today-work.js',
|
||||||
|
BASE + 'static/today-completion.js',
|
||||||
BASE + 'static/plan-today.js',
|
BASE + 'static/plan-today.js',
|
||||||
BASE + 'static/plan-today-preview.js',
|
BASE + 'static/plan-today-preview.js',
|
||||||
BASE + 'static/today-sync.js',
|
BASE + 'static/today-sync.js',
|
||||||
|
|
|
||||||
17
frontend/today-completion.js
Normal file
17
frontend/today-completion.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
function createTodayCompletion({ todayWork, todaySync, workSession, refresh, warm, announce }) {
|
||||||
|
return function completeTodayItem(item) {
|
||||||
|
if (!item || !todayWork.remove(item)) {
|
||||||
|
announce('Could not update Today on this device. Try again.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
todaySync.enqueue('remove', todayWork.identity(item));
|
||||||
|
todaySync.flush();
|
||||||
|
refresh();
|
||||||
|
warm();
|
||||||
|
workSession.complete();
|
||||||
|
announce('Done for Today. The Gitea item is unchanged.');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = createTodayCompletion;
|
||||||
|
|
@ -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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in worker
|
assert "stackchain-dashboard-shell-v68" 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-v67" in worker
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" 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-v67" in source
|
assert "stackchain-dashboard-shell-v68" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -334,7 +334,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-v67" in source
|
assert "stackchain-dashboard-shell-v68" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -371,6 +371,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/my-work.js",
|
"/dashboard/static/my-work.js",
|
||||||
"/dashboard/static/card-planning.js",
|
"/dashboard/static/card-planning.js",
|
||||||
"/dashboard/static/today-work.js",
|
"/dashboard/static/today-work.js",
|
||||||
|
"/dashboard/static/today-completion.js",
|
||||||
"/dashboard/static/plan-today.js",
|
"/dashboard/static/plan-today.js",
|
||||||
"/dashboard/static/plan-today-preview.js",
|
"/dashboard/static/plan-today-preview.js",
|
||||||
"/dashboard/static/today-sync.js",
|
"/dashboard/static/today-sync.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-v67" in source
|
assert "stackchain-dashboard-shell-v68" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ from tests.dashboard_bundle import dashboard
|
||||||
|
|
||||||
|
|
||||||
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
|
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
|
||||||
|
TODAY_COMPLETION = Path(__file__).parents[1] / "frontend" / "today-completion.js"
|
||||||
|
SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
||||||
|
|
||||||
|
|
||||||
def run_node(script):
|
def run_node(script):
|
||||||
|
|
@ -87,6 +89,69 @@ process.stdout.write(JSON.stringify([
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_for_today_removes_current_plan_item_syncs_and_advances_without_gitea_mutation():
|
||||||
|
script = f"""
|
||||||
|
const createTodayCompletion = require({json.dumps(str(TODAY_COMPLETION))});
|
||||||
|
const calls = [];
|
||||||
|
const current = {{kind:'issue', repository:'stackchain/dashboard', number:421}};
|
||||||
|
const complete = createTodayCompletion({{
|
||||||
|
todayWork: {{
|
||||||
|
identity: item => `issue:${{item.repository}}:${{item.number}}:`,
|
||||||
|
remove: item => {{ calls.push(`remove:${{item.number}}`); return true; }},
|
||||||
|
}},
|
||||||
|
todaySync: {{
|
||||||
|
enqueue: (action, identity) => {{ calls.push(`sync:${{action}}:${{identity}}`); return true; }},
|
||||||
|
flush: () => calls.push('flush'),
|
||||||
|
}},
|
||||||
|
refresh: () => calls.push('refresh'),
|
||||||
|
warm: () => calls.push('warm'),
|
||||||
|
workSession: {{ complete: () => calls.push('advance') }},
|
||||||
|
announce: message => calls.push(`announce:${{message}}`),
|
||||||
|
}});
|
||||||
|
const completed = complete(current);
|
||||||
|
process.stdout.write(JSON.stringify({{completed, calls}}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert json.loads(run_node(script)) == {
|
||||||
|
"completed": True,
|
||||||
|
"calls": [
|
||||||
|
"remove:421",
|
||||||
|
"sync:remove:issue:stackchain/dashboard:421:",
|
||||||
|
"flush",
|
||||||
|
"refresh",
|
||||||
|
"warm",
|
||||||
|
"advance",
|
||||||
|
"announce:Done for Today. The Gitea item is unchanged.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_for_today_does_not_advance_when_local_plan_persistence_fails():
|
||||||
|
script = f"""
|
||||||
|
const createTodayCompletion = require({json.dumps(str(TODAY_COMPLETION))});
|
||||||
|
const calls = [];
|
||||||
|
const complete = createTodayCompletion({{
|
||||||
|
todayWork: {{identity: () => 'issue:r:1:', remove: () => false}},
|
||||||
|
todaySync: {{enqueue: () => calls.push('sync'), flush: () => calls.push('flush')}},
|
||||||
|
refresh: () => calls.push('refresh'),
|
||||||
|
warm: () => calls.push('warm'),
|
||||||
|
workSession: {{complete: () => calls.push('advance')}},
|
||||||
|
announce: message => calls.push(message),
|
||||||
|
}});
|
||||||
|
const completed = complete({{kind:'issue', repository:'r', number:1}});
|
||||||
|
process.stdout.write(JSON.stringify({{completed, calls}}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert json.loads(run_node(script)) == {
|
||||||
|
"completed": False,
|
||||||
|
"calls": ["Could not update Today on this device. Try again."],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_for_today_logic_is_available_in_the_offline_app_shell():
|
||||||
|
assert "BASE + 'static/today-completion.js'" in SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_authoritative_reconciliation_reports_retired_ids_before_local_prune():
|
def test_authoritative_reconciliation_reports_retired_ids_before_local_prune():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
|
const createTodayWork = require({json.dumps(str(TODAY_WORK))});
|
||||||
|
|
@ -183,6 +248,22 @@ async def test_dashboard_runs_the_curated_today_queue_as_a_mobile_work_flow():
|
||||||
assert "Another device filled Today · showing its saved plan." in html
|
assert "Another device filled Today · showing its saved plan." in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_today_session_sheets_offer_a_touch_safe_plan_only_completion_action():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert '<script src="static/today-completion.js"></script>' in html
|
||||||
|
assert html.count('data-work-session-complete=') == 4
|
||||||
|
assert html.count('Done for Today & next') == 4
|
||||||
|
assert "button.hidden = !workSession.checkpointed();" in html
|
||||||
|
assert "const completeTodayItem = createTodayCompletion({" in html
|
||||||
|
assert "refresh: () => refreshMyWorkView({ reconcileSession:false })," in html
|
||||||
|
assert "document.querySelectorAll('[data-work-session-complete]')" in html
|
||||||
|
assert "completeTodayItem(selectedSessionItem(button.dataset.workSessionComplete))" in html
|
||||||
|
assert ".work-session-nav button { min-height:44px; width:100%; }" in html
|
||||||
|
assert "grid-template-columns:repeat(3,minmax(0,1fr))" in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_dashboard_activates_today_lifecycle_convergence():
|
async def test_dashboard_activates_today_lifecycle_convergence():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user