feat: keep background Updates badge current (Closes #1281)
This commit is contained in:
1 parent
b18a50fec5
commit
05405bf2e8
11 files changed
+172
-12
No files matched your search
@@ -364,7 +364,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-today-action-mailbox-v1", "stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["remaining"] == ["gitea.preference"]
|
||||
assert result["replaced"] == [
|
||||
@@ -392,7 +392,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-today-action-mailbox-v1", "stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||
assert result["replaced"] == ["/dashboard/login?reason=session-expired"]
|
||||
@@ -525,7 +525,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-today-action-mailbox-v1", "stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||
@@ -550,7 +550,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-today-action-mailbox-v1", "stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||
@@ -648,7 +648,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-today-action-mailbox-v1", "stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||
assert result["assigned"] == "/dashboard/login"
|
||||
|
||||
@@ -186,3 +186,27 @@ console.log(JSON.stringify({changed, saved:saved.has('stackchain.app-badge.enabl
|
||||
"saved": False,
|
||||
"status": "App icon badge is off, but the browser could not clear the old count.",
|
||||
}
|
||||
|
||||
|
||||
def test_app_badge_synchronizes_opt_in_with_the_active_service_worker():
|
||||
result = run_badge("""
|
||||
const listeners = {};
|
||||
const messages = [];
|
||||
const control = {checked:false, disabled:false, addEventListener(name, callback) { listeners[name] = callback; }};
|
||||
const controller = createMobileAppBadge({
|
||||
control, status:{textContent:''}, container:{hidden:false},
|
||||
navigator:{async setAppBadge() {}, async clearAppBadge() {}},
|
||||
storage:{getItem() { return null; }, setItem() {}, removeItem() {}},
|
||||
serviceWorker:{ready:Promise.resolve({active:{postMessage(message) { messages.push(message); }}})},
|
||||
});
|
||||
controller.start();
|
||||
control.checked = true;
|
||||
await listeners.change();
|
||||
control.checked = false;
|
||||
await listeners.change();
|
||||
console.log(JSON.stringify(messages));
|
||||
""")
|
||||
assert result == [
|
||||
{"type": "stackchain-app-badge-preference", "enabled": True},
|
||||
{"type": "stackchain-app-badge-preference", "enabled": False},
|
||||
]
|
||||
@@ -34,6 +34,7 @@ process.stdout.write(JSON.stringify(databases));
|
||||
"stackchain-search-reply-drafts-v1",
|
||||
"stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-app-badge-preference-v1",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ const clear = createPrivateDeviceDataPurger({{
|
||||
"stackchain-search-reply-drafts-v1",
|
||||
"stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert state["caches"] == ["stackchain-dashboard-shell-v37"]
|
||||
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||
|
||||
@@ -613,6 +613,7 @@ async def test_dispatch_sends_one_privacy_safe_deep_link_per_new_thread(tmp_path
|
||||
"route": "#/my-work/update/42",
|
||||
"tag": "stackchain-update-42",
|
||||
"notification_id": 42,
|
||||
"unread_count": 1,
|
||||
}
|
||||
assert "private/repo" not in json.dumps(sent)
|
||||
assert "Secret title" not in json.dumps(sent)
|
||||
@@ -692,7 +693,9 @@ async def test_update_burst_sends_bounded_individual_pushes_and_one_private_dige
|
||||
"route": "#/my-work/updates",
|
||||
"tag": "stackchain-update-digest",
|
||||
"update_count": 3,
|
||||
"unread_count": 5,
|
||||
}
|
||||
assert [payload["unread_count"] for payload in sent] == [5, 5, 5]
|
||||
assert "private/repo" not in json.dumps(sent)
|
||||
assert "Secret update" not in json.dumps(sent)
|
||||
assert await dispatch_unread_updates(
|
||||
@@ -792,6 +795,7 @@ async def test_production_notification_page_dispatches_one_unread_push(tmp_path)
|
||||
"route": "#/my-work/update/42",
|
||||
"tag": "stackchain-update-42",
|
||||
"notification_id": 42,
|
||||
"unread_count": 1,
|
||||
}]
|
||||
assert "private/repo" not in json.dumps(sent)
|
||||
assert "Secret title" not in json.dumps(sent)
|
||||
|
||||
@@ -14,7 +14,7 @@ def run_worker_scenario(scenario: str) -> dict:
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
const listeners = {{}};
|
||||
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, todayCommands: {{}}, failTodayCommandPut: false, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], closedNotifications: 0, focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
|
||||
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, todayCommands: {{}}, failTodayCommandPut: false, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], closedNotifications: 0, focused: [], opened: [], appBadges: [], clearedAppBadges: 0, badgeEnabled: false, failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
|
||||
const storedResponses = new Map();
|
||||
storedResponses.set(
|
||||
'https://forge.example/dashboard/__offline-session-lease',
|
||||
@@ -78,6 +78,10 @@ const context = {{
|
||||
}},
|
||||
purge: async () => {{ state.todayCommands = {{}}; }},
|
||||
}},
|
||||
__STACKCHAIN_APP_BADGE_PREFERENCE: {{
|
||||
get: async () => state.badgeEnabled,
|
||||
set: async enabled => {{ state.badgeEnabled = enabled; }},
|
||||
}},
|
||||
addEventListener: (name, handler) => {{ listeners[name] = handler; }},
|
||||
skipWaiting: async () => {{ state.skipped = true; }},
|
||||
clients: {{
|
||||
@@ -88,6 +92,8 @@ const context = {{
|
||||
registration: {{
|
||||
showNotification: async (title, options) => state.notifications.push({{title,options}}),
|
||||
getNotifications: async () => [{{close:()=>{{state.closedNotifications += 1;}}}}],
|
||||
setAppBadge: async value => state.appBadges.push(value),
|
||||
clearAppBadge: async () => {{ state.clearedAppBadges += 1; }},
|
||||
}},
|
||||
__issueSync: {{
|
||||
flush: async () => {{ state.backgroundFlushes += 1; state.outboxLifecycle.push('flush'); return state.flushResult; }},
|
||||
@@ -479,6 +485,7 @@ def test_revoked_background_session_purges_worker_data_and_notifies_dashboard_cl
|
||||
"stackchain-search-reply-drafts-v1",
|
||||
"stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
||||
assert result["state"]["clientMessages"] == [
|
||||
@@ -800,6 +807,7 @@ def test_device_purge_message_stops_worker_outbox_and_acknowledges_completion():
|
||||
"stackchain-search-reply-drafts-v1",
|
||||
"stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["replies"] == [{"ok": True}]
|
||||
|
||||
@@ -973,6 +981,40 @@ def test_update_digest_push_opens_unread_inbox_without_item_actions_or_private_c
|
||||
assert "must-not-render" not in json.dumps(result["notifications"])
|
||||
|
||||
|
||||
def test_opted_in_background_updates_reconcile_authoritative_badge_without_churn():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
await dispatchMessage({type:'stackchain-app-badge-preference', enabled:true}, [], {url:'https://forge.example/dashboard/'});
|
||||
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:7});
|
||||
await dispatchPush({tag:'stackchain-update-digest', route:'#/my-work/updates', update_count:2, unread_count:7});
|
||||
await dispatchPush({tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda', protect_route:'#/my-work/agenda/protect-today', deadline_count:1, unread_count:99});
|
||||
await dispatchPush({tag:'stackchain-update-43', route:'#/my-work/update/43', notification_id:43, unread_count:'invalid'});
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["badgeEnabled"] is True
|
||||
assert result["appBadges"] == [7]
|
||||
assert result["clearedAppBadges"] == 0
|
||||
assert len(result["notifications"]) == 4
|
||||
|
||||
|
||||
def test_disabling_background_badge_clears_it_and_future_updates_leave_it_off():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
state.badgeEnabled = true;
|
||||
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:3});
|
||||
await dispatchMessage({type:'stackchain-app-badge-preference', enabled:false}, [], {url:'https://forge.example/dashboard/'});
|
||||
await dispatchPush({tag:'stackchain-update-43', route:'#/my-work/update/43', notification_id:43, unread_count:4});
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["badgeEnabled"] is False
|
||||
assert result["appBadges"] == [3]
|
||||
assert result["clearedAppBadges"] == 1
|
||||
|
||||
|
||||
def test_deadline_digest_push_offers_protect_today_and_snooze_without_rendering_private_copy():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
@@ -1565,6 +1607,7 @@ def test_expired_offline_lease_purges_private_worker_data_and_refuses_cached_she
|
||||
"stackchain-search-reply-drafts-v1",
|
||||
"stackchain-conversation-reply-drafts-v1",
|
||||
"stackchain-today-action-mailbox-v1",
|
||||
"stackchain-app-badge-preference-v1",
|
||||
]
|
||||
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
||||
assert "private cached dashboard" not in result["body"]
|
||||
|
||||
Reference in new issue
Block a user