Recover mobile notification setup after permission is blocked #1133
|
|
@ -7990,9 +7990,15 @@
|
|||
enableOffline:() => setOfflineWorkEnabled(true),
|
||||
storageProtectionReadiness:() => deviceStorage.persistenceReadiness(),
|
||||
protectStorage:() => deviceStorage.requestPersistence(),
|
||||
notificationReadiness:() => pushController?.notificationReadiness()
|
||||
|| {state:'unavailable', detail:'Update notifications are unavailable.'},
|
||||
enablePush:async () => {
|
||||
const controller = await pushControllerReady;
|
||||
if (!controller) return;
|
||||
if (controller.notificationReadiness().state === 'blocked') {
|
||||
await controller.recoverPermission('updates');
|
||||
return;
|
||||
}
|
||||
qs('#push-updates').checked = true;
|
||||
await controller.change();
|
||||
},
|
||||
|
|
@ -8003,6 +8009,10 @@
|
|||
if (!controller) return;
|
||||
qs('#push-deadline-hour').value = qs('#device-setup-deadline-hour').value;
|
||||
qs('#push-deadline-days').value = qs('#device-setup-deadline-days').value;
|
||||
if (controller.deadlineReadiness().state === 'blocked') {
|
||||
await controller.recoverPermission('deadline');
|
||||
return;
|
||||
}
|
||||
await controller.enableDeadline();
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
const promptDismissKey = 'stackchain.device-setup-prompt-dismissed-until';
|
||||
const promptDismissMs = 7 * 24 * 60 * 60 * 1000;
|
||||
const steps = [
|
||||
['install', options.installButton, options.installStatus, options.install],
|
||||
['offline', options.offlineButton, options.offlineStatus, options.enableOffline],
|
||||
['protection', options.protectionButton, options.protectionStatus, options.protectStorage],
|
||||
['push', options.pushButton, options.pushStatus, options.enablePush],
|
||||
['deadline', options.deadlineButton, options.deadlineStatus, options.enableDeadline],
|
||||
['install', options.installButton, options.installStatus, options.install, 'Install'],
|
||||
['offline', options.offlineButton, options.offlineStatus, options.enableOffline, 'Enable'],
|
||||
['protection', options.protectionButton, options.protectionStatus, options.protectStorage, 'Protect'],
|
||||
['push', options.pushButton, options.pushStatus, options.enablePush, 'Enable'],
|
||||
['deadline', options.deadlineButton, options.deadlineStatus, options.enableDeadline, 'Enable'],
|
||||
];
|
||||
let trigger = options.launcher;
|
||||
let ownsDetour = false;
|
||||
|
|
@ -35,9 +35,10 @@
|
|||
const readiness = await options.getReadiness();
|
||||
let available = 0;
|
||||
let complete = 0;
|
||||
steps.forEach(([name, button, status]) => {
|
||||
steps.forEach(([name, button, status, _action, defaultActionLabel]) => {
|
||||
const step = readiness[name];
|
||||
status.textContent = step.detail;
|
||||
button.textContent = step.actionLabel || defaultActionLabel;
|
||||
button.hidden = step.state === 'complete' || step.state === 'unavailable';
|
||||
button.disabled = step.state === 'pending';
|
||||
if (step.state !== 'unavailable') available += 1;
|
||||
|
|
@ -130,11 +131,7 @@ function mountMobileDeviceSetup(options) {
|
|||
? {state:'complete', detail:qs('#offline-work-status').textContent || 'Offline work is saved.'}
|
||||
: {state:'incomplete', detail:'Private My Work and Today data are not saved offline.'},
|
||||
protection:options.storageProtectionReadiness(),
|
||||
push:qs('#push-updates').disabled
|
||||
? {state:'unavailable', detail:qs('#push-update-status').textContent || 'Update notifications are unavailable.'}
|
||||
: qs('#push-updates').checked
|
||||
? {state:'complete', detail:'New update notifications are enabled.'}
|
||||
: {state:'incomplete', detail:qs('#push-update-status').textContent || 'New update notifications are off.'},
|
||||
push:options.notificationReadiness(),
|
||||
deadline:options.deadlineReadiness(),
|
||||
}),
|
||||
install:() => options.installApp.install(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,34 @@
|
|||
notification, serviceWorker, fetchJson,
|
||||
}) {
|
||||
let configuration = null;
|
||||
let pendingIntent = null;
|
||||
let recoveryPromise = null;
|
||||
|
||||
function blockedReadiness() {
|
||||
return {
|
||||
state:'blocked',
|
||||
detail:'Notifications are blocked. Allow them in browser settings, then check again.',
|
||||
actionLabel:'Check again',
|
||||
};
|
||||
}
|
||||
|
||||
function pendingRecoveryReadiness() {
|
||||
return notification.permission === 'granted'
|
||||
? {state:'blocked', detail:'Notification permission changed. Check again to finish setup.', actionLabel:'Check again'}
|
||||
: blockedReadiness();
|
||||
}
|
||||
|
||||
function notificationReadiness() {
|
||||
if (!configuration?.available || control?.disabled) {
|
||||
return {state:'unavailable', detail:status?.textContent || 'Update notifications are unavailable.'};
|
||||
}
|
||||
if (configuration.subscribed) {
|
||||
return {state:'complete', detail:'New update notifications are enabled.'};
|
||||
}
|
||||
if (pendingIntent === 'updates') return pendingRecoveryReadiness();
|
||||
if (notification.permission === 'denied') return blockedReadiness();
|
||||
return {state:'incomplete', detail:status?.textContent || 'New update notifications are off.'};
|
||||
}
|
||||
|
||||
function renderDeliveryHealth() {
|
||||
const health = Object.values(configuration?.delivery_health || {});
|
||||
|
|
@ -77,6 +105,7 @@
|
|||
if (!configuration?.available || deadlineControl?.disabled) {
|
||||
return {state:'unavailable', detail:deadlineStatus?.textContent || 'Deadline reminders are unavailable.'};
|
||||
}
|
||||
if (pendingIntent === 'deadline') return pendingRecoveryReadiness();
|
||||
return configuration.deadline_enabled
|
||||
? {state:'complete', detail:enabledDeadlineText(configuration.reminder_hour, configuration.reminder_days)}
|
||||
: {state:'incomplete', detail:deadlineStatus?.textContent || 'Choose when to receive deadline reminders.'};
|
||||
|
|
@ -98,15 +127,20 @@
|
|||
control.checked = false;
|
||||
if (testControl) testControl.hidden = true;
|
||||
if (deadlineControl) deadlineControl.checked = false;
|
||||
configuration.subscribed = false;
|
||||
configuration.deadline_enabled = false;
|
||||
pendingIntent = null;
|
||||
status.textContent = 'New update notifications are off for this device.';
|
||||
if (deadlineStatus) deadlineStatus.textContent = 'Deadline reminders are off for this device.';
|
||||
}
|
||||
|
||||
async function ensureSubscription() {
|
||||
const permission = await notification.requestPermission();
|
||||
const permission = notification.permission === 'granted'
|
||||
? 'granted'
|
||||
: await notification.requestPermission();
|
||||
if (permission !== 'granted') {
|
||||
control.checked = false;
|
||||
status.textContent = 'Notifications are blocked. Allow them in your browser settings to enable updates.';
|
||||
status.textContent = blockedReadiness().detail;
|
||||
return null;
|
||||
}
|
||||
const registration = await serviceWorker.ready;
|
||||
|
|
@ -130,7 +164,10 @@
|
|||
}
|
||||
|
||||
async function enable() {
|
||||
await ensureSubscription();
|
||||
pendingIntent = 'updates';
|
||||
const subscription = await ensureSubscription();
|
||||
if (subscription) pendingIntent = null;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
async function change() {
|
||||
|
|
@ -153,6 +190,7 @@
|
|||
try {
|
||||
const registration = await serviceWorker.ready;
|
||||
let subscription = await registration.pushManager.getSubscription();
|
||||
if (deadlineControl.checked) pendingIntent = 'deadline';
|
||||
if (deadlineControl.checked && !subscription) subscription = await ensureSubscription();
|
||||
if (deadlineControl.checked && !subscription) {
|
||||
deadlineControl.checked = false;
|
||||
|
|
@ -171,6 +209,7 @@
|
|||
configuration.reminder_hour = reminderHour;
|
||||
configuration.reminder_days = reminderDays;
|
||||
configuration.timezone = timezone;
|
||||
pendingIntent = null;
|
||||
deadlineStatus.textContent = deadlineControl.checked
|
||||
? enabledDeadlineText(reminderHour, reminderDays)
|
||||
: 'Deadline reminders are off for this device.';
|
||||
|
|
@ -191,6 +230,24 @@
|
|||
return changeDeadline();
|
||||
}
|
||||
|
||||
async function recoverPermission(intent = null) {
|
||||
if (!pendingIntent && (intent === 'updates' || intent === 'deadline')) pendingIntent = intent;
|
||||
if (!pendingIntent || notification.permission !== 'granted') return false;
|
||||
if (recoveryPromise) return recoveryPromise;
|
||||
recoveryPromise = (async () => {
|
||||
if (pendingIntent === 'deadline') {
|
||||
deadlineControl.checked = true;
|
||||
return changeDeadline();
|
||||
}
|
||||
return Boolean(await enable());
|
||||
})();
|
||||
try {
|
||||
return await recoveryPromise;
|
||||
} finally {
|
||||
recoveryPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (!control || !notification || !serviceWorker) return;
|
||||
control.addEventListener('change', change);
|
||||
|
|
@ -215,5 +272,5 @@
|
|||
renderDeadlineSnooze();
|
||||
}
|
||||
|
||||
return {init, change, changeDeadline, enableDeadline, deadlineReadiness};
|
||||
return {init, change, changeDeadline, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v122';
|
||||
const CACHE = 'stackchain-dashboard-shell-v123';
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
|
|||
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||
assert '.update-reply-actions button { min-height:44px;' in html
|
||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
assert "stackchain-dashboard-shell-v122" in worker
|
||||
assert "stackchain-dashboard-shell-v123" in worker
|
||||
|
|
|
|||
|
|
@ -435,5 +435,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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
|
|
|
|||
|
|
@ -256,4 +256,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-v122" in worker
|
||||
assert "stackchain-dashboard-shell-v123" in worker
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ 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-v122" in worker
|
||||
assert "stackchain-dashboard-shell-v123" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
|
|
|||
|
|
@ -258,6 +258,26 @@ process.stdout.write(JSON.stringify({
|
|||
}
|
||||
|
||||
|
||||
def test_blocked_notification_step_becomes_a_check_again_action():
|
||||
result = run_scenario("""
|
||||
readiness.push = {
|
||||
state:'blocked',
|
||||
detail:'Notifications are blocked. Allow them in browser settings, then check again.',
|
||||
actionLabel:'Check again',
|
||||
};
|
||||
await launcher.dispatch('click', {currentTarget:launcher});
|
||||
await pushButton.dispatch('click');
|
||||
process.stdout.write(JSON.stringify({label:pushButton.textContent,detail:pushStatus.textContent,calls:state.pushCalls,hidden:pushButton.hidden}));
|
||||
""")
|
||||
|
||||
assert result == {
|
||||
"label": "Check again",
|
||||
"detail": "Notifications are blocked. Allow them in browser settings, then check again.",
|
||||
"calls": 1,
|
||||
"hidden": False,
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||
root = MODULE.parents[1]
|
||||
html = (root / "frontend" / "index.html").read_text()
|
||||
|
|
@ -293,9 +313,12 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
|||
assert "protectStorage:() => deviceStorage.requestPersistence()" in dashboard
|
||||
assert "clearPrivateDeviceData:root.stackchainPrivateDeviceData" in storage_module
|
||||
assert "promptStorage:localStorage" in dashboard
|
||||
assert "notificationReadiness:() => pushController?.notificationReadiness()" in dashboard
|
||||
assert "controller.recoverPermission('updates')" in dashboard
|
||||
assert "controller.recoverPermission('deadline')" in dashboard
|
||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||
assert "stackchain-dashboard-shell-v122" in worker
|
||||
assert "stackchain-dashboard-shell-v123" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
|
|
|||
|
|
@ -243,5 +243,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
|
|||
def test_mobile_insights_rolls_into_the_offline_shell():
|
||||
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in worker
|
||||
assert "stackchain-dashboard-shell-v123" in worker
|
||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
|
|||
assert ".mobile-start-day-finish { min-height:44px;" in html
|
||||
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||
assert "stackchain-dashboard-shell-v122" in service_worker
|
||||
assert "stackchain-dashboard-shell-v123" in service_worker
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ 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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const feature = createPushNotifications({
|
|||
control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
|
||||
deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview,
|
||||
onReviewDeadlines:() => { state.reviewed = true; },
|
||||
notification: {permission:'default', requestPermission:async () => { state.prompts += 1; return state.permission || 'granted'; }},
|
||||
notification: state.notification = {permission:'default', requestPermission:async () => { state.prompts += 1; state.notification.permission = state.permission || 'granted'; return state.notification.permission; }},
|
||||
serviceWorker: {ready:Promise.resolve(registration)},
|
||||
fetchJson: async (url, options={}) => { state.requests.push([url,options.method || 'GET',options.body || '']); if (state.failResume && options.method === 'DELETE' && url.endsWith('/deadlines/snooze')) throw new Error('offline'); return state.server || {available:true,subscribed:false,public_key:'AQID'}; },
|
||||
});
|
||||
|
|
@ -180,11 +180,121 @@ await feature.enableDeadline();
|
|||
process.stdout.write(JSON.stringify({checked:deadlineControl.checked, readiness:feature.deadlineReadiness(), requests:state.requests}));
|
||||
""")
|
||||
assert denied["checked"] is False
|
||||
assert denied["readiness"]["state"] == "incomplete"
|
||||
assert denied["readiness"]["state"] == "blocked"
|
||||
assert denied["readiness"]["actionLabel"] == "Check again"
|
||||
assert "blocked" in denied["readiness"]["detail"].lower()
|
||||
assert [request[0] for request in denied["requests"]] == ["api/v1/push-subscription"]
|
||||
|
||||
|
||||
def test_blocked_update_setup_can_recheck_without_reprompting_or_mutating():
|
||||
result = run_scenario("""
|
||||
state.permission = 'denied';
|
||||
await feature.init();
|
||||
control.checked = true;
|
||||
await state.change();
|
||||
const blocked = feature.notificationReadiness();
|
||||
await feature.recoverPermission();
|
||||
process.stdout.write(JSON.stringify({blocked,after:feature.notificationReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
|
||||
""")
|
||||
|
||||
assert result["blocked"] == {
|
||||
"state": "blocked",
|
||||
"detail": "Notifications are blocked. Allow them in browser settings, then check again.",
|
||||
"actionLabel": "Check again",
|
||||
}
|
||||
assert result["after"] == result["blocked"]
|
||||
assert result["prompts"] == 1
|
||||
assert result["subscriptions"] == 0
|
||||
assert result["requests"] == [["api/v1/push-subscription", "GET", ""]]
|
||||
|
||||
|
||||
def test_previously_blocked_permission_starts_a_resumable_update_intent():
|
||||
result = run_scenario("""
|
||||
state.permission = 'denied';
|
||||
state.notification.permission = 'denied';
|
||||
await feature.init();
|
||||
await feature.recoverPermission('updates');
|
||||
state.notification.permission = 'granted';
|
||||
const readyToRecover = feature.notificationReadiness();
|
||||
await feature.recoverPermission('updates');
|
||||
process.stdout.write(JSON.stringify({readyToRecover,readiness:feature.notificationReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
|
||||
""")
|
||||
|
||||
assert result["readyToRecover"]["actionLabel"] == "Check again"
|
||||
assert result["readiness"]["state"] == "complete"
|
||||
assert result["prompts"] == 0
|
||||
assert result["subscriptions"] == 1
|
||||
assert [request[0:2] for request in result["requests"]] == [
|
||||
["api/v1/push-subscription", "GET"],
|
||||
["api/v1/push-subscription", "PUT"],
|
||||
]
|
||||
|
||||
|
||||
def test_permission_recovery_resumes_pending_update_subscription_once():
|
||||
result = run_scenario("""
|
||||
state.permission = 'denied';
|
||||
await feature.init();
|
||||
control.checked = true;
|
||||
await state.change();
|
||||
state.notification.permission = 'granted';
|
||||
const readyToRecover = feature.notificationReadiness();
|
||||
await feature.recoverPermission();
|
||||
await feature.recoverPermission();
|
||||
process.stdout.write(JSON.stringify({readyToRecover,readiness:feature.notificationReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
|
||||
""")
|
||||
|
||||
assert result["readyToRecover"] == {
|
||||
"state": "blocked",
|
||||
"detail": "Notification permission changed. Check again to finish setup.",
|
||||
"actionLabel": "Check again",
|
||||
}
|
||||
assert result["readiness"]["state"] == "complete"
|
||||
assert result["prompts"] == 1
|
||||
assert result["subscriptions"] == 1
|
||||
assert [request[0:2] for request in result["requests"]] == [
|
||||
["api/v1/push-subscription", "GET"],
|
||||
["api/v1/push-subscription", "PUT"],
|
||||
]
|
||||
|
||||
|
||||
def test_permission_recovery_resumes_pending_deadline_choices_once():
|
||||
result = run_scenario("""
|
||||
state.server = {available:true,subscribed:false,deadline_enabled:false,reminder_hour:9,reminder_days:2,public_key:'AQID'};
|
||||
state.permission = 'denied';
|
||||
await feature.init();
|
||||
deadlineHour.value = '17';
|
||||
deadlineDays.value = '7';
|
||||
await feature.enableDeadline();
|
||||
const blocked = feature.deadlineReadiness();
|
||||
state.notification.permission = 'granted';
|
||||
const readyToRecover = feature.deadlineReadiness();
|
||||
await feature.recoverPermission();
|
||||
await feature.recoverPermission();
|
||||
process.stdout.write(JSON.stringify({blocked,readyToRecover,readiness:feature.deadlineReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
|
||||
""")
|
||||
|
||||
assert result["blocked"]["state"] == "blocked"
|
||||
assert result["blocked"]["actionLabel"] == "Check again"
|
||||
assert result["readyToRecover"] == {
|
||||
"state": "blocked",
|
||||
"detail": "Notification permission changed. Check again to finish setup.",
|
||||
"actionLabel": "Check again",
|
||||
}
|
||||
assert result["readiness"] == {
|
||||
"state": "complete",
|
||||
"detail": "Deadline reminders enabled for 17:00 local time, next 7 days.",
|
||||
}
|
||||
assert result["prompts"] == 1
|
||||
assert result["subscriptions"] == 1
|
||||
assert [request[0:2] for request in result["requests"]] == [
|
||||
["api/v1/push-subscription", "GET"],
|
||||
["api/v1/push-subscription", "PUT"],
|
||||
["api/v1/push-subscription/deadlines", "PUT"],
|
||||
]
|
||||
assert json.loads(result["requests"][-1][2])["reminder_hour"] == 17
|
||||
assert json.loads(result["requests"][-1][2])["reminder_days"] == 7
|
||||
|
||||
|
||||
def test_active_deadline_snooze_is_visible_in_agenda_with_a_local_resume_time():
|
||||
result = run_scenario("""
|
||||
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:9,reminder_days:2,snoozed_until:1765003600,public_key:'AQID'};
|
||||
|
|
|
|||
|
|
@ -168,13 +168,13 @@ async function dispatchPush(payload) {{
|
|||
def test_offline_activation_migration_rolls_the_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
|
||||
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
|
@ -183,7 +183,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
|||
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/authored-outbox.js'" in source
|
||||
assert "BASE + 'static/background-issue-sync.js'" in source
|
||||
|
|
@ -192,7 +192,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
|||
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||
assert "BASE + 'static/issue-attachment.js'" in source
|
||||
|
||||
|
|
@ -200,14 +200,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
|||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
||||
def test_offline_review_next_ships_today_completion_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -215,7 +215,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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
|||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/issue-sheet.js'" in source
|
||||
assert "BASE + 'static/checklist-conflict.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
|
@ -233,14 +233,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
|||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" 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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
|
@ -249,21 +249,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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" 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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" 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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
|
@ -1137,7 +1137,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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
|
|||
def test_readiness_runtime_is_available_in_offline_shell():
|
||||
service_worker = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v122';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v123';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
|||
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-v122" in source
|
||||
assert "stackchain-dashboard-shell-v123" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user