Keep retained mobile planning available through refresh outages #290
|
|
@ -11,7 +11,11 @@ function createDetailDefer({
|
||||||
defer(item, preset) {
|
defer(item, preset) {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
const until = laterWork.presetUntil(preset);
|
const until = laterWork.presetUntil(preset);
|
||||||
if (!laterWork.defer(item, until)) return false;
|
const result = laterWork.defer(item, until);
|
||||||
|
if (result !== 'deferred') {
|
||||||
|
announce(result === 'invalid' ? 'Choose a valid future time.' : 'Could not save Later on this device.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const inSession = session.active();
|
const inSession = session.active();
|
||||||
if (!inSession) close();
|
if (!inSession) close();
|
||||||
refresh();
|
refresh();
|
||||||
|
|
|
||||||
|
|
@ -540,7 +540,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<button id="release-issue" type="button">Release assignment</button>
|
<button id="release-issue" type="button">Release assignment</button>
|
||||||
<button id="close-issue" type="button">Close issue</button>
|
<button id="close-issue" type="button">Close issue</button>
|
||||||
<a id="open-issue-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-issue-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
<details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today">Later today</button><button type="button" data-detail-defer-preset="tomorrow">Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details>
|
<details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today" disabled data-planning-disabled>Later today</button><button type="button" data-detail-defer-preset="tomorrow" disabled data-planning-disabled>Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|
@ -644,7 +644,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<button class="share-work-route" type="button">Share</button>
|
<button class="share-work-route" type="button">Share</button>
|
||||||
<button id="mark-update-read-next" type="button">Mark read & next</button>
|
<button id="mark-update-read-next" type="button">Mark read & next</button>
|
||||||
<a id="open-update-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-update-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
<details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today">Later today</button><button type="button" data-detail-defer-preset="tomorrow">Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details>
|
<details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today" disabled data-planning-disabled>Later today</button><button type="button" data-detail-defer-preset="tomorrow" disabled data-planning-disabled>Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|
@ -684,7 +684,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<div class="pull-sheet-actions">
|
<div class="pull-sheet-actions">
|
||||||
<button class="share-work-route" type="button">Share</button>
|
<button class="share-work-route" type="button">Share</button>
|
||||||
<a id="open-pull-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
<a id="open-pull-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||||
<details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today">Later today</button><button type="button" data-detail-defer-preset="tomorrow">Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details>
|
<details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today" disabled data-planning-disabled>Later today</button><button type="button" data-detail-defer-preset="tomorrow" disabled data-planning-disabled>Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|
@ -749,7 +749,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
</section>
|
</section>
|
||||||
<h2>Review history</h2>
|
<h2>Review history</h2>
|
||||||
<div id="review-history" class="muted"></div>
|
<div id="review-history" class="muted"></div>
|
||||||
<div class="review-sheet-actions"><details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today">Later today</button><button type="button" data-detail-defer-preset="tomorrow">Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details></div>
|
<div class="review-sheet-actions"><details class="detail-defer"><summary>Defer</summary><div class="detail-defer-options"><button type="button" data-detail-defer-preset="today" disabled data-planning-disabled>Later today</button><button type="button" data-detail-defer-preset="tomorrow" disabled data-planning-disabled>Tomorrow</button><button type="button" data-detail-defer-cancel>Cancel</button></div></details></div>
|
||||||
<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>
|
||||||
|
|
@ -902,17 +902,18 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
let reviewHandoffPending = false;
|
let reviewHandoffPending = false;
|
||||||
let editingOutboxId = null;
|
let editingOutboxId = null;
|
||||||
let confirmedOwnerLogin = '';
|
let confirmedOwnerLogin = '';
|
||||||
|
let planningOwnerLogin = '';
|
||||||
let activeFlushLogin = '';
|
let activeFlushLogin = '';
|
||||||
let activeMyWork = [];
|
let activeMyWork = [];
|
||||||
let laterMyWork = [];
|
let laterMyWork = [];
|
||||||
let todayMyWork = [];
|
let todayMyWork = [];
|
||||||
const todayWork = createTodayWork({
|
const todayWork = createTodayWork({
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
getLogin: () => confirmedOwnerLogin,
|
getLogin: () => planningOwnerLogin,
|
||||||
});
|
});
|
||||||
const laterWork = createLaterWork({
|
const laterWork = createLaterWork({
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
getLogin: () => confirmedOwnerLogin,
|
getLogin: () => planningOwnerLogin,
|
||||||
onWake: () => {
|
onWake: () => {
|
||||||
qs('#my-work-action-status').textContent = 'Deferred work is ready again.';
|
qs('#my-work-action-status').textContent = 'Deferred work is ready again.';
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
|
|
@ -1325,6 +1326,13 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updatePlanningAvailability() {
|
||||||
|
document.querySelectorAll('[data-detail-defer-preset]').forEach(button => {
|
||||||
|
button.disabled = !planningOwnerLogin;
|
||||||
|
button.toggleAttribute('data-planning-disabled', !planningOwnerLogin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderContextSnapshot(data) {
|
function renderContextSnapshot(data) {
|
||||||
liveMode = true;
|
liveMode = true;
|
||||||
hasContextSnapshot = true;
|
hasContextSnapshot = true;
|
||||||
|
|
@ -1573,14 +1581,15 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
'<button class="mark-update-read" data-notification-id="' + item.notification_id + '">Mark read</button>' : '';
|
'<button class="mark-update-read" data-notification-id="' + item.notification_id + '">Mark read</button>' : '';
|
||||||
const readUpdate = item.has_update && Number.isInteger(item.notification_id) ?
|
const readUpdate = item.has_update && Number.isInteger(item.notification_id) ?
|
||||||
'<a class="read-update" href="' + escAttr(createWorkRoute.serialize({ kind:'update', notification_id:item.notification_id })) + '" data-update-index="' + index + '">Read update</a>' : '';
|
'<a class="read-update" href="' + escAttr(createWorkRoute.serialize({ kind:'update', notification_id:item.notification_id })) + '" data-update-index="' + index + '">Read update</a>' : '';
|
||||||
|
const planningDisabled = planningOwnerLogin ? '' : ' disabled data-planning-disabled';
|
||||||
const laterActions = selectedWorkFilter === 'later' ?
|
const laterActions = selectedWorkFilter === 'later' ?
|
||||||
'<div class="later-actions"><button type="button" data-later-restore data-work-index="' + index + '">Bring back now</button></div>' :
|
'<div class="later-actions"><button type="button" data-later-restore data-work-index="' + index + '">Bring back now</button></div>' :
|
||||||
'<div class="later-actions" aria-label="Defer this work"><button type="button" data-later-preset="today" data-work-index="' + index + '">Later today</button><button type="button" data-later-preset="tomorrow" data-work-index="' + index + '">Tomorrow</button></div>';
|
'<div class="later-actions" aria-label="Defer this work"><button type="button" data-later-preset="today" data-work-index="' + index + '"' + planningDisabled + '>Later today</button><button type="button" data-later-preset="tomorrow" data-work-index="' + index + '"' + planningDisabled + '>Tomorrow</button></div>';
|
||||||
const alreadyToday = todayWork.contains(item);
|
const alreadyToday = todayWork.contains(item);
|
||||||
const todayPosition = todayWork.position(item);
|
const todayPosition = todayWork.position(item);
|
||||||
const todayActions = selectedWorkFilter === 'today' ?
|
const todayActions = selectedWorkFilter === 'today' ?
|
||||||
'<div class="today-actions" aria-label="Reorder Today"><button type="button" data-today-move="up" data-work-index="' + index + '"' + (todayPosition.can_up ? '' : ' disabled') + '>Move up</button><button type="button" data-today-move="down" data-work-index="' + index + '"' + (todayPosition.can_down ? '' : ' disabled') + '>Move down</button><button type="button" data-today-remove data-work-index="' + index + '">Remove from Today</button></div>' :
|
'<div class="today-actions" aria-label="Reorder Today"><button type="button" data-today-move="up" data-work-index="' + index + '"' + (todayPosition.can_up ? '' : ' disabled') + '>Move up</button><button type="button" data-today-move="down" data-work-index="' + index + '"' + (todayPosition.can_down ? '' : ' disabled') + '>Move down</button><button type="button" data-today-remove data-work-index="' + index + '">Remove from Today</button></div>' :
|
||||||
'<div class="today-actions"><button type="button" data-today-add data-work-index="' + index + '"' + (alreadyToday ? ' disabled' : '') + '>' + (alreadyToday ? 'Added to Today' : 'Add to Today') + '</button></div>';
|
'<div class="today-actions"><button type="button" data-today-add data-work-index="' + index + '"' + (alreadyToday ? ' disabled' : planningDisabled) + '>' + (alreadyToday ? 'Added to Today' : 'Add to Today') + '</button></div>';
|
||||||
const planningActions = todayActions + laterActions;
|
const planningActions = todayActions + laterActions;
|
||||||
if (item.is_review) {
|
if (item.is_review) {
|
||||||
return '<article class="my-work-card"><a class="my-work-card-main review-trigger" href="' + escAttr(routeHref) + '" data-review-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + planningActions + '</article>';
|
return '<article class="my-work-card"><a class="my-work-card-main review-trigger" href="' + escAttr(routeHref) + '" data-review-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + planningActions + '</article>';
|
||||||
|
|
@ -1629,8 +1638,17 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
document.querySelectorAll('[data-later-preset]').forEach(button => {
|
document.querySelectorAll('[data-later-preset]').forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const item = lastMyWork[Number(button.dataset.workIndex)];
|
const item = lastMyWork[Number(button.dataset.workIndex)];
|
||||||
if (!item || !laterWork.defer(item, laterWork.presetUntil(button.dataset.laterPreset))) return;
|
if (!item) return;
|
||||||
qs('#my-work-action-status').textContent = 'Deferred work stays unread and unchanged in Gitea.';
|
if (!planningOwnerLogin) {
|
||||||
|
qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const until = laterWork.presetUntil(button.dataset.laterPreset);
|
||||||
|
const result = laterWork.defer(item, until);
|
||||||
|
qs('#my-work-action-status').textContent = result === 'deferred' ?
|
||||||
|
'Deferred until ' + fmt(until) + '; work stays unread and unchanged in Gitea.' :
|
||||||
|
(result === 'invalid' ? 'Choose a valid future time.' : 'Could not save Later on this device.');
|
||||||
|
if (result !== 'deferred') return;
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1644,10 +1662,15 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[data-today-add]').forEach(button => {
|
document.querySelectorAll('[data-today-add]').forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
|
if (!planningOwnerLogin) {
|
||||||
|
qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = todayWork.add(lastMyWork[Number(button.dataset.workIndex)]);
|
const result = todayWork.add(lastMyWork[Number(button.dataset.workIndex)]);
|
||||||
qs('#my-work-action-status').textContent = result === 'full' ?
|
qs('#my-work-action-status').textContent = result === 'full' ?
|
||||||
'Today is limited to 5 items. Remove one before adding more.' :
|
'Today is limited to 5 items. Remove one before adding more.' :
|
||||||
(result === 'added' ? 'Added to Today without changing Gitea.' : 'This item is already in Today.');
|
(result === 'added' ? 'Added to Today without changing Gitea.' :
|
||||||
|
(result === 'exists' ? 'This item is already in Today.' : 'Could not save Today on this device.'));
|
||||||
refreshMyWorkView();
|
refreshMyWorkView();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -2486,6 +2509,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
}
|
}
|
||||||
if (snapshot.context && workChanged) {
|
if (snapshot.context && workChanged) {
|
||||||
setOfflineWorkMode(false);
|
setOfflineWorkMode(false);
|
||||||
|
const retainedPlanningLogin = !snapshot.context.error ?
|
||||||
|
String(snapshot.context.user?.login || '').trim() : '';
|
||||||
|
planningOwnerLogin = retainedPlanningLogin;
|
||||||
|
updatePlanningAvailability();
|
||||||
const contextIdentityFresh = !snapshot.context.error && !contextFreshness?.stale &&
|
const contextIdentityFresh = !snapshot.context.error && !contextFreshness?.stale &&
|
||||||
!contextFreshness?.degraded && !contextFreshness?.revalidating;
|
!contextFreshness?.degraded && !contextFreshness?.revalidating;
|
||||||
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
|
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,11 @@ function createLaterWork({ storage, getLogin, now = () => new Date(), setTimer =
|
||||||
const key = storageKey();
|
const key = storageKey();
|
||||||
const id = identity(item);
|
const id = identity(item);
|
||||||
const wake = new Date(until);
|
const wake = new Date(until);
|
||||||
if (!key || !id || Number.isNaN(wake.getTime()) || wake <= now()) return false;
|
if (!key) return 'unavailable';
|
||||||
|
if (!id || Number.isNaN(wake.getTime()) || wake <= now()) return 'invalid';
|
||||||
const records = read();
|
const records = read();
|
||||||
records[id] = wake.toISOString();
|
records[id] = wake.toISOString();
|
||||||
write(records);
|
return write(records) ? 'deferred' : 'unavailable';
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function presetUntil(preset) {
|
function presetUntil(preset) {
|
||||||
|
|
|
||||||
|
|
@ -825,7 +825,7 @@ process.stdout.write(JSON.stringify({{
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json.loads(result.stdout) == {
|
assert json.loads(result.stdout) == {
|
||||||
"deferred": True,
|
"deferred": "deferred",
|
||||||
"timmy": {
|
"timmy": {
|
||||||
"active": 0,
|
"active": 0,
|
||||||
"later": [{
|
"later": [{
|
||||||
|
|
@ -838,6 +838,33 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_later_queue_reports_invalid_identity_and_failed_storage_without_false_success():
|
||||||
|
script = f"""
|
||||||
|
const createLaterWork = require({json.dumps(str(LATER_WORK))});
|
||||||
|
let login = '';
|
||||||
|
const item = {{kind:'issue',repository:'stackchain/api',number:17}};
|
||||||
|
const unavailable = createLaterWork({{
|
||||||
|
storage: {{getItem:() => null,setItem:() => {{ throw new Error('quota'); }},removeItem:() => {{}}}},
|
||||||
|
getLogin:() => login, now:() => new Date('2026-08-08T12:00:00Z'),
|
||||||
|
}});
|
||||||
|
const noIdentity = unavailable.defer(item, new Date('2026-08-08T16:00:00Z'));
|
||||||
|
login = 'timmy';
|
||||||
|
const invalid = unavailable.defer(item, new Date('2026-08-08T11:00:00Z'));
|
||||||
|
const failedWrite = unavailable.defer(item, new Date('2026-08-08T16:00:00Z'));
|
||||||
|
process.stdout.write(JSON.stringify({{noIdentity, invalid, failedWrite}}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"noIdentity": "unavailable",
|
||||||
|
"invalid": "invalid",
|
||||||
|
"failedWrite": "unavailable",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_later_queue_prunes_missing_work_and_wakes_expired_items_without_reload():
|
def test_later_queue_prunes_missing_work_and_wakes_expired_items_without_reload():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createLaterWork = require({json.dumps(str(LATER_WORK))});
|
const createLaterWork = require({json.dumps(str(LATER_WORK))});
|
||||||
|
|
@ -964,7 +991,7 @@ let sessionActive = false;
|
||||||
const controller = createDetailDefer({{
|
const controller = createDetailDefer({{
|
||||||
laterWork: {{
|
laterWork: {{
|
||||||
presetUntil:preset => new Date(preset === 'today' ? '2026-08-08T16:00:00Z' : '2026-08-09T09:00:00Z'),
|
presetUntil:preset => new Date(preset === 'today' ? '2026-08-08T16:00:00Z' : '2026-08-09T09:00:00Z'),
|
||||||
defer:(item, until) => {{ calls.push(['defer', item.title, until.toISOString()]); return true; }},
|
defer:(item, until) => {{ calls.push(['defer', item.title, until.toISOString()]); return 'deferred'; }},
|
||||||
}},
|
}},
|
||||||
session: {{ active:() => sessionActive }},
|
session: {{ active:() => sessionActive }},
|
||||||
close:() => calls.push(['close']),
|
close:() => calls.push(['close']),
|
||||||
|
|
@ -1000,6 +1027,32 @@ process.stdout.write(JSON.stringify({{outside,session,calls}}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_detail_defer_reports_storage_failure_without_closing_or_refreshing():
|
||||||
|
script = f"""
|
||||||
|
const createDetailDefer = require({json.dumps(str(DETAIL_DEFER))});
|
||||||
|
const calls = [];
|
||||||
|
const controller = createDetailDefer({{
|
||||||
|
laterWork: {{
|
||||||
|
presetUntil:() => new Date('2026-08-08T16:00:00Z'),
|
||||||
|
defer:() => 'unavailable',
|
||||||
|
}},
|
||||||
|
session: {{active:() => false}},
|
||||||
|
close:() => calls.push('close'), refresh:() => calls.push('refresh'),
|
||||||
|
focus:() => calls.push('focus'), announce:message => calls.push(message),
|
||||||
|
}});
|
||||||
|
const saved = controller.defer({{kind:'issue',repository:'stackchain/api',number:17}}, 'today');
|
||||||
|
process.stdout.write(JSON.stringify({{saved,calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"saved": False,
|
||||||
|
"calls": ["Could not save Later on this device."],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_mobile_detail_sheets_offer_touch_safe_defer_without_a_gitea_mutation():
|
async def test_mobile_detail_sheets_offer_touch_safe_defer_without_a_gitea_mutation():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
@ -1023,16 +1076,16 @@ async def test_mobile_my_work_wires_touch_safe_non_mutating_later_actions():
|
||||||
assert 'data-work-filter="later"' in html
|
assert 'data-work-filter="later"' in html
|
||||||
assert 'data-work-count="later"' in html
|
assert 'data-work-count="later"' in html
|
||||||
assert 'const laterWork = createLaterWork({' in html
|
assert 'const laterWork = createLaterWork({' in html
|
||||||
assert 'getLogin: () => confirmedOwnerLogin' in html
|
assert 'getLogin: () => planningOwnerLogin' in html
|
||||||
assert 'laterWork.partition(lastMyWork,' in html
|
assert 'laterWork.partition(lastMyWork,' in html
|
||||||
assert 'data-later-preset="today"' in html
|
assert 'data-later-preset="today"' in html
|
||||||
assert 'data-later-preset="tomorrow"' in html
|
assert 'data-later-preset="tomorrow"' in html
|
||||||
assert 'data-later-restore' in html
|
assert 'data-later-restore' in html
|
||||||
assert "Deferred until ' + escapeHtml(fmt(item.deferred_until))" in html
|
assert "Deferred until ' + escapeHtml(fmt(item.deferred_until))" in html
|
||||||
assert "laterWork.defer(item, laterWork.presetUntil(button.dataset.laterPreset))" in html
|
assert "const result = laterWork.defer(item, until)" in html
|
||||||
assert 'laterWork.restore(item)' in html
|
assert 'laterWork.restore(item)' in html
|
||||||
assert '.later-actions button { min-height:44px;' in html
|
assert '.later-actions button { min-height:44px;' in html
|
||||||
assert 'Deferred work stays unread and unchanged in Gitea.' in html
|
assert "'Deferred until ' + fmt(until)" in html
|
||||||
|
|
||||||
|
|
||||||
def test_milestone_lane_composes_with_type_filter_and_updates_confirmed_snapshot():
|
def test_milestone_lane_composes_with_type_filter_and_updates_confirmed_snapshot():
|
||||||
|
|
|
||||||
|
|
@ -94,3 +94,20 @@ async def test_dashboard_runs_the_curated_today_queue_as_a_mobile_work_flow():
|
||||||
assert 'data-today-move="down"' in html
|
assert 'data-today-move="down"' in html
|
||||||
assert 'Today is limited to 5 items' in html
|
assert 'Today is limited to 5 items' in html
|
||||||
assert '.today-actions button' in html and 'min-height:44px' in html
|
assert '.today-actions button' in html and 'min-height:44px' in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_retained_authenticated_context_keeps_local_planning_separate_from_fresh_delivery():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
assert "let planningOwnerLogin = '';" in html
|
||||||
|
assert html.count("getLogin: () => planningOwnerLogin") == 2
|
||||||
|
assert "const retainedPlanningLogin = !snapshot.context.error ?" in html
|
||||||
|
assert "planningOwnerLogin = retainedPlanningLogin;" in html
|
||||||
|
assert "button.disabled = !planningOwnerLogin;" in html
|
||||||
|
assert "activeFlushLogin = contextIdentityFresh ?" in html
|
||||||
|
assert "getOwnerLogin: () => confirmedOwnerLogin" in html
|
||||||
|
assert "Planning is unavailable until your operator identity is restored." in html
|
||||||
|
assert "Could not save Today on this device." in html
|
||||||
|
assert "Could not save Later on this device." in html
|
||||||
|
assert "data-planning-disabled" in html
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user