fix: foreground mobile delivery attention handoffs (Closes #1018)
This commit is contained in:
parent
6913fc8257
commit
3fde7cf3ad
|
|
@ -3205,14 +3205,14 @@
|
|||
const button = event.currentTarget;
|
||||
if (!activeFlushLogin || !deliveryCenter.retryable.length) return;
|
||||
button.disabled = true;
|
||||
qs('#my-work-action-status').textContent = 'Retrying safe waiting deliveries…';
|
||||
qs('#my-work-action-status').textContent = 'Retrying waiting deliveries…';
|
||||
const [issueResult, authoredResult] = await Promise.all([issueOutbox.flush(activeFlushLogin), authoredOutbox.flush(activeFlushLogin)]);
|
||||
applyOutboxResult(issueResult);
|
||||
applyAuthoredOutboxResult(authoredResult);
|
||||
qs('#my-work-action-status').textContent = 'Waiting deliveries retried. Items needing attention were skipped.';
|
||||
qs('#my-work-action-status').textContent = 'Waiting deliveries retried; attention stayed queued.';
|
||||
});
|
||||
list.querySelectorAll('.draft-resume').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
button.onclick=async()=>{
|
||||
const item = lastDrafts[Number(button.dataset.draftIndex)];
|
||||
if (!item) return;
|
||||
if (item.kind === 'unfiled-issue') {
|
||||
|
|
@ -3221,7 +3221,7 @@
|
|||
} catch (error) { qs('#my-work-action-status').textContent = error.message; }
|
||||
} else if (item.kind === 'new-issue') openCreateIssueSheet();
|
||||
else if (item.route) workRoute.open(item.route);
|
||||
});
|
||||
};
|
||||
});
|
||||
list.querySelectorAll('.draft-continue').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
|
|
@ -3268,7 +3268,7 @@
|
|||
});
|
||||
});
|
||||
list.querySelectorAll('.draft-review-checklist').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
button.onclick=async()=>{
|
||||
const item = lastDrafts[Number(button.dataset.draftIndex)];
|
||||
const queued = authoredOutbox.list().find(candidate => candidate.id === item?.outbox_id);
|
||||
if (!item?.checklist_conflict || !queued || !activeFlushLogin) return;
|
||||
|
|
@ -3301,7 +3301,7 @@
|
|||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
list.querySelectorAll('.draft-authorize').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
|
|
@ -3322,12 +3322,12 @@
|
|||
});
|
||||
});
|
||||
list.querySelectorAll('.draft-copy').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
button.onclick=async()=>{
|
||||
const item = lastDrafts[Number(button.dataset.draftIndex)];
|
||||
if (!item?.copy_text) return;
|
||||
await navigator.clipboard.writeText(item.copy_text);
|
||||
qs('#my-work-action-status').textContent = 'Queued content copied without sending it.';
|
||||
});
|
||||
};
|
||||
});
|
||||
list.querySelectorAll('.draft-discard').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
|
|
|
|||
|
|
@ -109,14 +109,15 @@
|
|||
}
|
||||
|
||||
async function attend(item) {
|
||||
if (typeof document === 'undefined') return false;
|
||||
const index = options.getIndex ? options.getIndex(item) : -1;
|
||||
const selector = item.quarantined ? '.draft-copy' :
|
||||
item.checklist_conflict ? '.draft-review-checklist' : '.draft-resume';
|
||||
const action = document.querySelector('#my-work-list ' + selector + '[data-draft-index="' + index + '"]');
|
||||
if (!action) throw new Error('This delivery changed. Reopen recovery to load its current action.');
|
||||
action.click();
|
||||
return true;
|
||||
if (!action) throw new Error('Delivery changed.');
|
||||
elements.dialog.close();
|
||||
action.scrollIntoView();
|
||||
action.focus();
|
||||
return await action.onclick();
|
||||
}
|
||||
|
||||
function activate() {
|
||||
|
|
@ -138,7 +139,8 @@
|
|||
}
|
||||
const result = render();
|
||||
if (failure && elements?.status) {
|
||||
elements.status.textContent = failure + ' Try again when the connection is ready.';
|
||||
if (active && !elements.dialog.open) elements.dialog.showModal();
|
||||
elements.status.textContent = failure + ' Try again.';
|
||||
}
|
||||
return result;
|
||||
})().finally(() => { inFlight = null; });
|
||||
|
|
|
|||
|
|
@ -194,6 +194,40 @@ def test_release_artifact_keeps_mobile_delivery_recovery_single_flight(tmp_path:
|
|||
page.evaluate("Promise.all([window.__firstRecovery, window.__secondRecovery])")
|
||||
expect(action).to_be_enabled()
|
||||
assert page.evaluate("window.__deliveryAttempts") == 1
|
||||
|
||||
page.evaluate(
|
||||
"""
|
||||
() => {
|
||||
const destination = document.createElement('button');
|
||||
destination.type = 'button';
|
||||
destination.className = 'draft-resume';
|
||||
destination.dataset.draftIndex = '0';
|
||||
destination.textContent = 'Open current review';
|
||||
window.__attentionAttempts = 0;
|
||||
window.__attentionSettled = new Promise(resolve => { window.__finishAttention = resolve; });
|
||||
destination.onclick = async () => {
|
||||
window.__attentionAttempts += 1;
|
||||
await window.__attentionSettled;
|
||||
return true;
|
||||
};
|
||||
document.querySelector('#my-work-list').append(destination);
|
||||
window.__attentionRecovery = createMobileDeliveryRecovery({
|
||||
getItems: () => [{outbox_id:'review-attention', status:'attention', title:'Review feedback'}],
|
||||
getIndex: () => 0,
|
||||
});
|
||||
window.__attentionRecovery.open();
|
||||
window.__firstAttention = window.__attentionRecovery.activate();
|
||||
window.__secondAttention = window.__attentionRecovery.activate();
|
||||
}
|
||||
"""
|
||||
)
|
||||
expect(page.locator("#mobile-delivery-recovery")).to_be_hidden()
|
||||
expect(page.locator("#my-work-list .draft-resume")).to_be_focused()
|
||||
assert page.evaluate("window.__attentionAttempts") == 1
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
page.evaluate("window.__finishAttention()")
|
||||
page.evaluate("Promise.all([window.__firstAttention, window.__secondAttention])")
|
||||
assert page.evaluate("window.__attentionAttempts") == 1
|
||||
assert browser_errors == []
|
||||
browser.close()
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
|||
assert b"function createQueueToday" in first.feature_bundles["today-timer"].runtime_bytes
|
||||
assert b"function createConversationPager" in first.feature_bundles["today-timer"].runtime_bytes
|
||||
assert b"function createConversationPager" not in first.feature_bundles["comment-actions"].runtime_bytes
|
||||
assert b"function createMobileDeliveryRecovery" not in first.runtime_bytes
|
||||
assert b"function createMobileDeliveryRecovery" in first.feature_bundles["today-timer"].runtime_bytes
|
||||
assert b"gitea_time_logged" not in first.runtime_bytes
|
||||
assert b"gitea_time_logged" in security_center.runtime_bytes
|
||||
# Core mobile workflows stay below 99 KiB, including synced Search views and Update decisions.
|
||||
|
|
|
|||
|
|
@ -135,6 +135,109 @@ const createRecovery = require({json.dumps(str(RECOVERY))});
|
|||
}
|
||||
|
||||
|
||||
def test_attention_handoff_closes_modal_focuses_destination_and_awaits_work():
|
||||
script = f"""
|
||||
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||
(async () => {{
|
||||
let finish;
|
||||
let handoffs = 0;
|
||||
const settled = new Promise(resolve => {{finish = resolve}});
|
||||
const destination = {{
|
||||
focused:false, scrolled:false,
|
||||
focus(){{this.focused=true}}, scrollIntoView(){{this.scrolled=true}},
|
||||
async onclick(){{handoffs += 1; await settled; return true}},
|
||||
}};
|
||||
global.document = {{
|
||||
getElementById: () => null,
|
||||
querySelector: selector => selector.includes('draft-resume') ? destination : null,
|
||||
}};
|
||||
const element = () => ({{textContent:'', disabled:false}});
|
||||
const elements = {{
|
||||
dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
|
||||
title:element(), destination:element(), reason:element(), progress:element(),
|
||||
action:element(), status:element(),
|
||||
}};
|
||||
const recovery = createRecovery({{
|
||||
getItems: () => [{{outbox_id:'review', status:'attention', title:'Review feedback'}}],
|
||||
getIndex: () => 3,
|
||||
elements,
|
||||
}});
|
||||
recovery.open();
|
||||
const first = recovery.activate();
|
||||
const second = recovery.activate();
|
||||
await Promise.resolve();
|
||||
const pending = {{
|
||||
dialogOpen:elements.dialog.open, disabled:elements.action.disabled,
|
||||
focused:destination.focused, scrolled:destination.scrolled, handoffs,
|
||||
}};
|
||||
finish();
|
||||
await Promise.all([first, second]);
|
||||
process.stdout.write(JSON.stringify({{pending, handoffs, dialogOpen:elements.dialog.open}}));
|
||||
}})();
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"pending": {
|
||||
"dialogOpen": False,
|
||||
"disabled": True,
|
||||
"focused": True,
|
||||
"scrolled": True,
|
||||
"handoffs": 1,
|
||||
},
|
||||
"handoffs": 1,
|
||||
"dialogOpen": False,
|
||||
}
|
||||
|
||||
|
||||
def test_failed_attention_handoff_reopens_the_same_retryable_item():
|
||||
script = f"""
|
||||
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||
(async () => {{
|
||||
const destination = {{
|
||||
focus(){{}}, scrollIntoView(){{}},
|
||||
async onclick(){{throw new Error('Latest checklist unavailable')}},
|
||||
}};
|
||||
global.document = {{
|
||||
getElementById: () => null,
|
||||
querySelector: () => destination,
|
||||
}};
|
||||
const element = () => ({{textContent:'', disabled:false}});
|
||||
const elements = {{
|
||||
dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
|
||||
title:element(), destination:element(), reason:element(), progress:element(),
|
||||
action:element(), status:element(),
|
||||
}};
|
||||
const recovery = createRecovery({{
|
||||
getItems: () => [{{outbox_id:'conflict', checklist_conflict:true, title:'Checklist conflict'}}],
|
||||
getIndex: () => 0,
|
||||
elements,
|
||||
}});
|
||||
recovery.open();
|
||||
const result = await recovery.activate();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
result, dialogOpen:elements.dialog.open, status:elements.status.textContent,
|
||||
action:elements.action.textContent,
|
||||
}}));
|
||||
}})();
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"result": {
|
||||
"count": 1,
|
||||
"current": {
|
||||
"id": "conflict",
|
||||
"state": "attention",
|
||||
"primary": "Review changes",
|
||||
"position": 1,
|
||||
"total": 1,
|
||||
},
|
||||
},
|
||||
"dialogOpen": True,
|
||||
"status": "Latest checklist unavailable Try again.",
|
||||
"action": "Review changes",
|
||||
}
|
||||
|
||||
|
||||
def test_recovery_retains_failed_delivery_with_actionable_status():
|
||||
script = f"""
|
||||
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||
|
|
@ -170,7 +273,7 @@ const createRecovery = require({json.dumps(str(RECOVERY))});
|
|||
},
|
||||
},
|
||||
"disabled": False,
|
||||
"status": "Network unavailable Try again when the connection is ready.",
|
||||
"status": "Network unavailable Try again.",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -235,6 +338,7 @@ async def test_dashboard_packages_phone_safe_guided_delivery_recovery():
|
|||
assert '<script src="static/mobile-delivery-recovery.js"></script>' in html
|
||||
assert "const mobileDeliveryRecovery = createMobileDeliveryRecovery({" in html
|
||||
assert "activate: recoverMobileDelivery," in html
|
||||
assert "button.onclick=async()=>" in html
|
||||
assert "await (authored ? authoredOutbox : issueOutbox).retry(item.outbox_id, activeFlushLogin)" in html
|
||||
assert "(authored ? applyAuthoredOutboxResult : applyOutboxResult)(result)" in html
|
||||
assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user