Compare commits
No commits in common. "f6873890469781e0b075c1b01af192e0d85eaf75" and "7ba238fb53336eb4283fd734b3256098407642cd" have entirely different histories.
f687389046
...
7ba238fb53
|
|
@ -1807,16 +1807,10 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let todaySessionSync = null;
|
let todaySessionSync = null;
|
||||||
let todayLockScreen = null;
|
|
||||||
const timer = createTodayTimer({
|
const timer = createTodayTimer({
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
getLogin: () => confirmedOwnerLogin,
|
getLogin: () => confirmedOwnerLogin,
|
||||||
onChange: snapshot => {
|
onChange: snapshot => todaySessionSync?.publish(snapshot),
|
||||||
todaySessionSync?.publish(snapshot);
|
|
||||||
queueMicrotask(() => {
|
|
||||||
if (todayLockScreen) todayLockScreen.sync(snapshot, workSession.checkpointed());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const timerView = createTodayTimerView({
|
const timerView = createTodayTimerView({
|
||||||
timer,
|
timer,
|
||||||
|
|
@ -1945,28 +1939,6 @@
|
||||||
todayRecapView.finish(selectedWorkFilter);
|
todayRecapView.finish(selectedWorkFilter);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
todayLockScreen = createTodayLockScreen({
|
|
||||||
storage:localStorage,
|
|
||||||
getLogin:() => confirmedOwnerLogin,
|
|
||||||
serviceWorker:navigator.serviceWorker,
|
|
||||||
NotificationRef:window.Notification,
|
|
||||||
control:qs('#today-lock-screen'),
|
|
||||||
status:qs('#today-lock-screen-status'),
|
|
||||||
locationRef:window.location,
|
|
||||||
historyRef:window.history,
|
|
||||||
onAction:action => {
|
|
||||||
const state = timer.snapshot();
|
|
||||||
if (!state.identity || (action === 'pause' && !state.running) || (action === 'resume' && state.running)) return;
|
|
||||||
const changed = action === 'pause' ? timer.pause() : timer.resume();
|
|
||||||
if (changed === false) return;
|
|
||||||
selectTodayWork();
|
|
||||||
const item = todayMyWork.find(entry => todayWork.identity(entry) === state.identity);
|
|
||||||
if (item) workSession.reopen(item);
|
|
||||||
timerView.render();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
todayLockScreen.consumeLaunchAction();
|
|
||||||
todayLockScreen.sync(timer.snapshot(), workSession.checkpointed());
|
|
||||||
function selectTodayWork() {
|
function selectTodayWork() {
|
||||||
qs('[data-work-filter="today"]').click();
|
qs('[data-work-filter="today"]').click();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,8 +182,6 @@
|
||||||
<label for="push-deadline-hour">Reminder hour <select id="push-deadline-hour" aria-label="Deadline reminder local hour"></select></label>
|
<label for="push-deadline-hour">Reminder hour <select id="push-deadline-hour" aria-label="Deadline reminder local hour"></select></label>
|
||||||
<label for="push-deadline-days">Warn me <select id="push-deadline-days" aria-label="Deadline reminder horizon"><option value="0">Due today</option><option value="2" selected>Next 2 days</option><option value="7">Next 7 days</option></select></label>
|
<label for="push-deadline-days">Warn me <select id="push-deadline-days" aria-label="Deadline reminder horizon"><option value="0">Due today</option><option value="2" selected>Next 2 days</option><option value="7">Next 7 days</option></select></label>
|
||||||
<span class="small" id="push-deadline-status" role="status" aria-live="polite"></span>
|
<span class="small" id="push-deadline-status" role="status" aria-live="polite"></span>
|
||||||
<label class="push-update-control" for="today-lock-screen"><input id="today-lock-screen" type="checkbox" /> Show active Today controls on my lock screen</label>
|
|
||||||
<span class="small" id="today-lock-screen-status" role="status" aria-live="polite"></span>
|
|
||||||
<button id="clear-offline-work" type="button">Clear offline work data</button>
|
<button id="clear-offline-work" type="button">Clear offline work data</button>
|
||||||
<button id="retry-offline-storage" type="button" hidden>Retry offline saving</button>
|
<button id="retry-offline-storage" type="button" hidden>Retry offline saving</button>
|
||||||
<span class="small" id="offline-work-status" role="status" aria-live="polite"></span>
|
<span class="small" id="offline-work-status" role="status" aria-live="polite"></span>
|
||||||
|
|
@ -1626,7 +1624,6 @@
|
||||||
<script src="static/work-selection.js"></script>
|
<script src="static/work-selection.js"></script>
|
||||||
<script src="static/today-work.js"></script>
|
<script src="static/today-work.js"></script>
|
||||||
<script src="static/today-timer.js"></script>
|
<script src="static/today-timer.js"></script>
|
||||||
<script src="static/today-lock-screen.js"></script>
|
|
||||||
<script src="static/today-session-sync.js"></script>
|
<script src="static/today-session-sync.js"></script>
|
||||||
<script src="static/today-recap.js"></script>
|
<script src="static/today-recap.js"></script>
|
||||||
<script src="static/today-wrap-up.js"></script>
|
<script src="static/today-wrap-up.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const BASE = new URL('./', self.location.href).pathname;
|
const BASE = new URL('./', self.location.href).pathname;
|
||||||
importScripts(BASE + 'static/private-data-registry.js');
|
importScripts(BASE + 'static/private-data-registry.js');
|
||||||
importScripts(BASE + 'static/background-issue-sync.js');
|
importScripts(BASE + 'static/background-issue-sync.js');
|
||||||
const CACHE = 'stackchain-dashboard-shell-v119';
|
const CACHE = 'stackchain-dashboard-shell-v118';
|
||||||
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;
|
||||||
|
|
@ -51,7 +51,6 @@ const SHELL = [
|
||||||
BASE + 'static/work-selection.js',
|
BASE + 'static/work-selection.js',
|
||||||
BASE + 'static/today-work.js',
|
BASE + 'static/today-work.js',
|
||||||
BASE + 'static/today-timer.js',
|
BASE + 'static/today-timer.js',
|
||||||
BASE + 'static/today-lock-screen.js',
|
|
||||||
BASE + 'static/today-session-sync.js',
|
BASE + 'static/today-session-sync.js',
|
||||||
BASE + 'static/today-recap.js',
|
BASE + 'static/today-recap.js',
|
||||||
BASE + 'static/today-wrap-up.js',
|
BASE + 'static/today-wrap-up.js',
|
||||||
|
|
@ -420,26 +419,6 @@ self.addEventListener('sync', event => {
|
||||||
if (event.tag === 'stackchain-issue-outbox-v1') event.waitUntil(flushAndNotify());
|
if (event.tag === 'stackchain-issue-outbox-v1') event.waitUntil(flushAndNotify());
|
||||||
});
|
});
|
||||||
|
|
||||||
async function updateTodayLockScreen(active, running) {
|
|
||||||
const tag = 'stackchain-today-session';
|
|
||||||
if (!active) {
|
|
||||||
const notifications = await self.registration.getNotifications({ tag });
|
|
||||||
notifications.forEach(notification => notification.close());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await self.registration.showNotification(running ? 'Today session running' : 'Today session paused', {
|
|
||||||
body: running ? 'Your active Today timer is running.' : 'Your active Today timer is paused.',
|
|
||||||
tag,
|
|
||||||
renotify:false,
|
|
||||||
silent:true,
|
|
||||||
actions: [
|
|
||||||
{ action:running ? 'pause-today' : 'resume-today', title:running ? 'Pause' : 'Resume' },
|
|
||||||
{ action:'open-today', title:'Open Today' },
|
|
||||||
],
|
|
||||||
data: { route:'#/my-work/today' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener('message', event => {
|
self.addEventListener('message', event => {
|
||||||
if (event.data?.type === 'stackchain-resume-outbox') event.waitUntil((async () => {
|
if (event.data?.type === 'stackchain-resume-outbox') event.waitUntil((async () => {
|
||||||
await issueSync.resume();
|
await issueSync.resume();
|
||||||
|
|
@ -452,15 +431,11 @@ self.addEventListener('message', event => {
|
||||||
try {
|
try {
|
||||||
await issueSync.purge();
|
await issueSync.purge();
|
||||||
await deletePrivateDatabases();
|
await deletePrivateDatabases();
|
||||||
await updateTodayLockScreen(false, false);
|
|
||||||
event.ports?.[0]?.postMessage({ ok: true });
|
event.ports?.[0]?.postMessage({ ok: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
event.ports?.[0]?.postMessage({ ok: false, error: String(error?.message || 'Outbox purge failed.') });
|
event.ports?.[0]?.postMessage({ ok: false, error: String(error?.message || 'Outbox purge failed.') });
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
if (event.data?.type === 'stackchain-today-lock-screen') {
|
|
||||||
event.waitUntil(updateTodayLockScreen(event.data.active === true, event.data.running === true));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('push', event => {
|
self.addEventListener('push', event => {
|
||||||
|
|
@ -553,35 +528,9 @@ async function openCanonicalIssueUrl(rawUrl) {
|
||||||
return client.focus();
|
return client.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyTodayTimerAction(action) {
|
|
||||||
if (!['pause', 'resume'].includes(action)) return;
|
|
||||||
const route = '#/my-work/today';
|
|
||||||
const windows = await self.clients.matchAll({ type:'window', includeUncontrolled:true });
|
|
||||||
const client = windows.find(candidate => candidate.url.startsWith(self.location.origin + BASE));
|
|
||||||
if (client) {
|
|
||||||
client.postMessage?.({ type:'stackchain-today-timer-action', action });
|
|
||||||
return client.focus?.();
|
|
||||||
}
|
|
||||||
const target = new URL(BASE + '?today_timer_action=' + action + route, self.location.origin).href;
|
|
||||||
return self.clients.openWindow(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener('notificationclick', event => {
|
self.addEventListener('notificationclick', event => {
|
||||||
const route = String(event.notification.data?.route || '');
|
const route = String(event.notification.data?.route || '');
|
||||||
const issueUrl = String(event.notification.data?.url || '');
|
const issueUrl = String(event.notification.data?.url || '');
|
||||||
if (
|
|
||||||
event.notification.tag === 'stackchain-today-session'
|
|
||||||
&& route === '#/my-work/today'
|
|
||||||
&& ['pause-today', 'resume-today', 'open-today', ''].includes(event.action)
|
|
||||||
) {
|
|
||||||
event.notification.close();
|
|
||||||
if (event.action === 'pause-today' || event.action === 'resume-today') {
|
|
||||||
event.waitUntil(applyTodayTimerAction(event.action === 'pause-today' ? 'pause' : 'resume'));
|
|
||||||
} else {
|
|
||||||
event.waitUntil(openWorkRoute(route));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (issueUrl) {
|
if (issueUrl) {
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
event.waitUntil(openCanonicalIssueUrl(issueUrl));
|
event.waitUntil(openCanonicalIssueUrl(issueUrl));
|
||||||
|
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
function createTodayLockScreen({
|
|
||||||
storage,
|
|
||||||
getLogin,
|
|
||||||
serviceWorker,
|
|
||||||
NotificationRef,
|
|
||||||
control,
|
|
||||||
status,
|
|
||||||
locationRef,
|
|
||||||
historyRef,
|
|
||||||
onAction = () => {},
|
|
||||||
}) {
|
|
||||||
const preferenceKey = () => {
|
|
||||||
const login = String(getLogin?.() || '').trim().toLowerCase();
|
|
||||||
return login ? 'stackchain.today-lock-screen.v1.' + encodeURIComponent(login) : '';
|
|
||||||
};
|
|
||||||
const supported = Boolean(serviceWorker && NotificationRef);
|
|
||||||
const enabled = () => {
|
|
||||||
const key = preferenceKey();
|
|
||||||
return Boolean(key && storage?.getItem(key) === '1');
|
|
||||||
};
|
|
||||||
const setStatus = message => {
|
|
||||||
if (status) status.textContent = message;
|
|
||||||
};
|
|
||||||
const post = async message => {
|
|
||||||
const target = serviceWorker?.controller || (await serviceWorker?.ready)?.active;
|
|
||||||
target?.postMessage?.(message);
|
|
||||||
};
|
|
||||||
const hide = () => post({
|
|
||||||
type:'stackchain-today-lock-screen', active:false, running:false,
|
|
||||||
});
|
|
||||||
const render = () => {
|
|
||||||
if (control) {
|
|
||||||
control.checked = enabled();
|
|
||||||
control.disabled = !supported;
|
|
||||||
}
|
|
||||||
if (!supported) setStatus('Lock-screen controls are not supported on this device.');
|
|
||||||
else if (enabled()) setStatus('Lock-screen Today controls are on.');
|
|
||||||
};
|
|
||||||
const consumeAction = action => {
|
|
||||||
if (!enabled() || !['pause', 'resume'].includes(action)) return false;
|
|
||||||
onAction(action);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
const api = {
|
|
||||||
enabled,
|
|
||||||
async enable() {
|
|
||||||
if (!supported) {
|
|
||||||
render();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const permission = NotificationRef.permission === 'granted' ?
|
|
||||||
'granted' : await NotificationRef.requestPermission();
|
|
||||||
if (permission !== 'granted') {
|
|
||||||
if (control) control.checked = false;
|
|
||||||
setStatus(permission === 'denied' ?
|
|
||||||
'Lock-screen controls are blocked in browser settings.' :
|
|
||||||
'Lock-screen controls were not enabled.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const key = preferenceKey();
|
|
||||||
if (!key) return false;
|
|
||||||
storage?.setItem(key, '1');
|
|
||||||
render();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
async disable() {
|
|
||||||
const key = preferenceKey();
|
|
||||||
if (key) storage?.removeItem(key);
|
|
||||||
if (control) control.checked = false;
|
|
||||||
setStatus('Lock-screen Today controls are off.');
|
|
||||||
await hide();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
async sync(snapshot, active) {
|
|
||||||
if (!enabled() || NotificationRef?.permission !== 'granted') return false;
|
|
||||||
const visible = Boolean(active && snapshot?.identity);
|
|
||||||
await post({
|
|
||||||
type:'stackchain-today-lock-screen',
|
|
||||||
active:visible,
|
|
||||||
running:visible && Boolean(snapshot.running),
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
consumeLaunchAction() {
|
|
||||||
let url;
|
|
||||||
try { url = new URL(locationRef?.href || ''); }
|
|
||||||
catch (_error) { return false; }
|
|
||||||
const action = url.searchParams.get('today_timer_action');
|
|
||||||
if (!['pause', 'resume'].includes(action)) return false;
|
|
||||||
url.searchParams.delete('today_timer_action');
|
|
||||||
historyRef?.replaceState?.(null, '', url.pathname + url.search + url.hash);
|
|
||||||
return consumeAction(action);
|
|
||||||
},
|
|
||||||
render,
|
|
||||||
};
|
|
||||||
control?.addEventListener?.('change', () => {
|
|
||||||
if (control.checked) api.enable();
|
|
||||||
else api.disable();
|
|
||||||
});
|
|
||||||
serviceWorker?.addEventListener?.('message', event => {
|
|
||||||
if (event.data?.type === 'stackchain-today-timer-action') consumeAction(String(event.data.action || ''));
|
|
||||||
});
|
|
||||||
render();
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = createTodayLockScreen;
|
|
||||||
|
|
@ -34,7 +34,7 @@ FEATURE_SOURCES = {
|
||||||
"security-center": ("static/security-center.js",),
|
"security-center": ("static/security-center.js",),
|
||||||
"today-timer": (
|
"today-timer": (
|
||||||
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js",
|
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js",
|
||||||
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-handoff.js",
|
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-handoff.js",
|
||||||
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||||
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
||||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||||
|
|
|
||||||
|
|
@ -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 { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||||
assert '.update-reply-actions button { min-height:44px;' in html
|
assert '.update-reply-actions button { min-height:44px;' in html
|
||||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
assert "stackchain-dashboard-shell-v119" in worker
|
assert "stackchain-dashboard-shell-v118" 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():
|
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-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/later-sync.js'" 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 { 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-v119" in worker
|
assert "stackchain-dashboard-shell-v118" 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]))
|
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-v119" in worker
|
assert "stackchain-dashboard-shell-v118" in worker
|
||||||
|
|
||||||
|
|
||||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||||
assert "promptStorage:localStorage" in dashboard
|
assert "promptStorage:localStorage" in dashboard
|
||||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||||
assert "stackchain-dashboard-shell-v119" in worker
|
assert "stackchain-dashboard-shell-v118" in worker
|
||||||
assert ".device-setup-panel" in css
|
assert ".device-setup-panel" in css
|
||||||
assert ".device-readiness-card" in css
|
assert ".device-readiness-card" in css
|
||||||
assert "overflow-x:hidden" in css
|
assert "overflow-x:hidden" in css
|
||||||
|
|
|
||||||
|
|
@ -174,5 +174,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
|
||||||
def test_mobile_insights_rolls_into_the_offline_shell():
|
def test_mobile_insights_rolls_into_the_offline_shell():
|
||||||
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in worker
|
assert "stackchain-dashboard-shell-v118" in worker
|
||||||
assert "BASE + 'static/mobile-insights.js'" 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 ".mobile-start-day-finish { min-height:44px;" in html
|
||||||
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
||||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||||
assert "stackchain-dashboard-shell-v119" in service_worker
|
assert "stackchain-dashboard-shell-v118" in service_worker
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@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():
|
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-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/plan-today.js'" in source
|
assert "BASE + 'static/plan-today.js'" in source
|
||||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ def run_worker_scenario(scenario: str) -> dict:
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
const listeners = {{}};
|
const listeners = {{}};
|
||||||
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, 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: {{}}, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
|
||||||
const storedResponses = new Map();
|
const storedResponses = new Map();
|
||||||
storedResponses.set(
|
storedResponses.set(
|
||||||
'https://forge.example/dashboard/__offline-session-lease',
|
'https://forge.example/dashboard/__offline-session-lease',
|
||||||
|
|
@ -73,10 +73,7 @@ const context = {{
|
||||||
matchAll: async () => state.clientList || [],
|
matchAll: async () => state.clientList || [],
|
||||||
openWindow: async url => {{ state.opened.push(url); }},
|
openWindow: async url => {{ state.opened.push(url); }},
|
||||||
}},
|
}},
|
||||||
registration: {{
|
registration: {{showNotification: async (title, options) => state.notifications.push({{title,options}})}},
|
||||||
showNotification: async (title, options) => state.notifications.push({{title,options}}),
|
|
||||||
getNotifications: async () => [{{close:()=>{{state.closedNotifications += 1;}}}}],
|
|
||||||
}},
|
|
||||||
__issueSync: {{
|
__issueSync: {{
|
||||||
flush: async () => {{ state.backgroundFlushes += 1; state.outboxLifecycle.push('flush'); return state.flushResult; }},
|
flush: async () => {{ state.backgroundFlushes += 1; state.outboxLifecycle.push('flush'); return state.flushResult; }},
|
||||||
purge: async () => {{ state.outboxPurges += 1; state.outboxLifecycle.push('purge'); }},
|
purge: async () => {{ state.outboxPurges += 1; state.outboxLifecycle.push('purge'); }},
|
||||||
|
|
@ -168,13 +165,13 @@ async function dispatchPush(payload) {{
|
||||||
def test_offline_activation_migration_rolls_the_shell_cache():
|
def test_offline_activation_migration_rolls_the_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
|
|
||||||
|
|
||||||
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-v119" in source
|
assert "stackchain-dashboard-shell-v118" 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
|
||||||
|
|
@ -183,7 +180,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/authored-outbox.js'" in source
|
assert "BASE + 'static/authored-outbox.js'" in source
|
||||||
assert "BASE + 'static/background-issue-sync.js'" in source
|
assert "BASE + 'static/background-issue-sync.js'" in source
|
||||||
|
|
@ -192,7 +189,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||||
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||||
assert "BASE + 'static/issue-attachment.js'" in source
|
assert "BASE + 'static/issue-attachment.js'" in source
|
||||||
|
|
||||||
|
|
@ -200,14 +197,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_offline_review_next_ships_today_completion_atomically():
|
def test_offline_review_next_ships_today_completion_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/today-completion.js'" in source
|
assert "BASE + 'static/today-completion.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -215,7 +212,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||||
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-v119" in source
|
assert "stackchain-dashboard-shell-v118" 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
|
||||||
|
|
||||||
|
|
@ -223,7 +220,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/issue-sheet.js'" in source
|
assert "BASE + 'static/issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/checklist-conflict.js'" in source
|
assert "BASE + 'static/checklist-conflict.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
@ -233,14 +230,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||||
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-v119" in source
|
assert "stackchain-dashboard-shell-v118" 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-v119" in source
|
assert "stackchain-dashboard-shell-v118" 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
|
||||||
|
|
@ -249,21 +246,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-v119" in source
|
assert "stackchain-dashboard-shell-v118" 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-v119" in source
|
assert "stackchain-dashboard-shell-v118" 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-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -504,91 +501,6 @@ def test_authenticated_resume_rearms_a_previously_purged_worker_before_flushing(
|
||||||
assert result["state"]["backgroundResumes"] == 1
|
assert result["state"]["backgroundResumes"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_active_today_message_replaces_one_privacy_safe_lock_screen_notification():
|
|
||||||
result = run_worker_scenario(
|
|
||||||
"""
|
|
||||||
await dispatchMessage({type:'stackchain-today-lock-screen',active:true,running:true,identity:'secret/repo#42'});
|
|
||||||
process.stdout.write(JSON.stringify(state));
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["notifications"] == [
|
|
||||||
{
|
|
||||||
"title": "Today session running",
|
|
||||||
"options": {
|
|
||||||
"body": "Your active Today timer is running.",
|
|
||||||
"tag": "stackchain-today-session",
|
|
||||||
"renotify": False,
|
|
||||||
"silent": True,
|
|
||||||
"actions": [
|
|
||||||
{"action": "pause-today", "title": "Pause"},
|
|
||||||
{"action": "open-today", "title": "Open Today"},
|
|
||||||
],
|
|
||||||
"data": {"route": "#/my-work/today"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
assert "secret" not in json.dumps(result["notifications"])
|
|
||||||
|
|
||||||
|
|
||||||
def test_inactive_today_message_closes_the_lock_screen_notification():
|
|
||||||
result = run_worker_scenario(
|
|
||||||
"""
|
|
||||||
await dispatchMessage({type:'stackchain-today-lock-screen',active:false,running:false});
|
|
||||||
process.stdout.write(JSON.stringify(state));
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["notifications"] == []
|
|
||||||
assert result["closedNotifications"] == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_private_data_purge_also_removes_the_today_lock_screen_notification():
|
|
||||||
result = run_worker_scenario(
|
|
||||||
"""
|
|
||||||
await dispatchMessage({type:'stackchain-purge-outbox'}, [{postMessage:()=>{}}]);
|
|
||||||
process.stdout.write(JSON.stringify(state));
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["outboxPurges"] == 1
|
|
||||||
assert result["closedNotifications"] == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_today_lock_screen_action_updates_an_open_dashboard_without_navigation():
|
|
||||||
result = run_worker_scenario(
|
|
||||||
"""
|
|
||||||
state.clientMessages=[];
|
|
||||||
state.clientList=[{
|
|
||||||
url:'https://forge.example/dashboard/#/my-work/today',
|
|
||||||
postMessage:message=>state.clientMessages.push(message),
|
|
||||||
focus:async()=>state.focused.push('today'),
|
|
||||||
}];
|
|
||||||
await dispatchNotificationClick('#/my-work/today','pause-today',null,'stackchain-today-session');
|
|
||||||
process.stdout.write(JSON.stringify(state));
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["clientMessages"] == [
|
|
||||||
{"type": "stackchain-today-timer-action", "action": "pause"}
|
|
||||||
]
|
|
||||||
assert result["focused"] == ["today"]
|
|
||||||
assert result["opened"] == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_today_lock_screen_action_opens_a_validated_one_shot_route_when_closed():
|
|
||||||
result = run_worker_scenario(
|
|
||||||
"""
|
|
||||||
await dispatchNotificationClick('#/my-work/today','resume-today',null,'stackchain-today-session');
|
|
||||||
process.stdout.write(JSON.stringify(state));
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["opened"] == [
|
|
||||||
"https://forge.example/dashboard/?today_timer_action=resume#/my-work/today"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_background_mutation_abort_also_cancels_stalled_csrf_lookup():
|
def test_background_mutation_abort_also_cancels_stalled_csrf_lookup():
|
||||||
result = run_worker_scenario(
|
result = run_worker_scenario(
|
||||||
"""
|
"""
|
||||||
|
|
@ -1018,7 +930,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-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1076,7 +988,6 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/work-selection.js",
|
"/dashboard/static/work-selection.js",
|
||||||
"/dashboard/static/today-work.js",
|
"/dashboard/static/today-work.js",
|
||||||
"/dashboard/static/today-timer.js",
|
"/dashboard/static/today-timer.js",
|
||||||
"/dashboard/static/today-lock-screen.js",
|
|
||||||
"/dashboard/static/today-session-sync.js",
|
"/dashboard/static/today-session-sync.js",
|
||||||
"/dashboard/static/today-recap.js",
|
"/dashboard/static/today-recap.js",
|
||||||
"/dashboard/static/today-wrap-up.js",
|
"/dashboard/static/today-wrap-up.js",
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
SOURCE = Path(__file__).parents[1] / "frontend" / "today-lock-screen.js"
|
|
||||||
ROOT = SOURCE.parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def run_scenario(scenario: str) -> dict:
|
|
||||||
script = SOURCE.read_text() + r"""
|
|
||||||
const values = new Map();
|
|
||||||
const messages = [];
|
|
||||||
const listeners = {};
|
|
||||||
const control = {checked:false, disabled:false, addEventListener:(name, fn)=>listeners[name]=fn};
|
|
||||||
const status = {textContent:''};
|
|
||||||
const serviceWorker = {
|
|
||||||
controller:{postMessage:message=>messages.push(message)},
|
|
||||||
ready:Promise.resolve({active:{postMessage:message=>messages.push(message)}}),
|
|
||||||
addEventListener:(name, fn)=>listeners['sw-' + name]=fn,
|
|
||||||
};
|
|
||||||
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
||||||
let permission = 'default';
|
|
||||||
const NotificationRef = {get permission(){return permission;},requestPermission:async()=>{permission='granted';return permission;}};
|
|
||||||
const actions = [];
|
|
||||||
const locationRef = {href:'https://forge.example/dashboard/#/my-work/today'};
|
|
||||||
const historyRef = {replaceState:(_a,_b,url)=>{locationRef.href=new URL(url, locationRef.href).href;}};
|
|
||||||
(async()=>{
|
|
||||||
const lockScreen = createTodayLockScreen({storage,getLogin:()=> 'Timmy',serviceWorker,NotificationRef,control,status,locationRef,historyRef,onAction:action=>actions.push(action)});
|
|
||||||
%SCENARIO%
|
|
||||||
})().catch(error=>{console.error(error);process.exit(1)});
|
|
||||||
""".replace("%SCENARIO%", scenario)
|
|
||||||
completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
|
||||||
return json.loads(completed.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def test_operator_opt_in_syncs_one_privacy_safe_session_notification():
|
|
||||||
result = run_scenario(r"""
|
|
||||||
await lockScreen.enable();
|
|
||||||
await lockScreen.sync({identity:'issue:secret/repo:42:',running:true,elapsed_ms:125000}, true);
|
|
||||||
process.stdout.write(JSON.stringify({checked:control.checked,status:status.textContent,messages,stored:[...values.entries()]}));
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert result["checked"] is True
|
|
||||||
assert result["status"] == "Lock-screen Today controls are on."
|
|
||||||
assert result["stored"] == [["stackchain.today-lock-screen.v1.timmy", "1"]]
|
|
||||||
assert result["messages"][-1] == {
|
|
||||||
"type": "stackchain-today-lock-screen",
|
|
||||||
"active": True,
|
|
||||||
"running": True,
|
|
||||||
}
|
|
||||||
assert "secret" not in json.dumps(result)
|
|
||||||
|
|
||||||
|
|
||||||
def test_disabling_or_ending_session_removes_notification():
|
|
||||||
result = run_scenario(r"""
|
|
||||||
await lockScreen.enable();
|
|
||||||
await lockScreen.sync({identity:'issue:r:42:',running:false,elapsed_ms:0}, false);
|
|
||||||
await lockScreen.disable();
|
|
||||||
process.stdout.write(JSON.stringify({checked:control.checked,messages,stored:[...values.entries()]}));
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert result["checked"] is False
|
|
||||||
assert result["stored"] == []
|
|
||||||
assert result["messages"][-2:] == [
|
|
||||||
{"type": "stackchain-today-lock-screen", "active": False, "running": False},
|
|
||||||
{"type": "stackchain-today-lock-screen", "active": False, "running": False},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_valid_notification_action_is_consumed_once_and_removed_from_url():
|
|
||||||
result = run_scenario(r"""
|
|
||||||
values.set('stackchain.today-lock-screen.v1.timmy','1');
|
|
||||||
locationRef.href='https://forge.example/dashboard/?today_timer_action=pause#/my-work/today';
|
|
||||||
const consumed = lockScreen.consumeLaunchAction();
|
|
||||||
const second = lockScreen.consumeLaunchAction();
|
|
||||||
listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'resume'}});
|
|
||||||
process.stdout.write(JSON.stringify({consumed,second,actions,href:locationRef.href}));
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert result == {
|
|
||||||
"consumed": True,
|
|
||||||
"second": False,
|
|
||||||
"actions": ["pause", "resume"],
|
|
||||||
"href": "https://forge.example/dashboard/#/my-work/today",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_permission_denial_is_truthful_and_does_not_persist_opt_in():
|
|
||||||
result = run_scenario(r"""
|
|
||||||
NotificationRef.requestPermission=async()=>{permission='denied';return permission;};
|
|
||||||
const enabled = await lockScreen.enable();
|
|
||||||
process.stdout.write(JSON.stringify({enabled,checked:control.checked,status:status.textContent,stored:[...values.entries()]}));
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert result == {
|
|
||||||
"enabled": False,
|
|
||||||
"checked": False,
|
|
||||||
"status": "Lock-screen controls are blocked in browser settings.",
|
|
||||||
"stored": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_lock_screen_flow_is_wired_into_the_packaged_today_journey():
|
|
||||||
index = (ROOT / "frontend" / "index.html").read_text()
|
|
||||||
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
|
||||||
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
|
|
||||||
|
|
||||||
assert 'id="today-lock-screen"' in index
|
|
||||||
assert 'id="today-lock-screen-status"' in index
|
|
||||||
assert '<script src="static/today-lock-screen.js"></script>' in index
|
|
||||||
assert index.index('static/today-lock-screen.js') < index.index('static/dashboard.js')
|
|
||||||
assert 'createTodayLockScreen({' in dashboard
|
|
||||||
assert "action === 'pause' ? timer.pause() : timer.resume()" in dashboard
|
|
||||||
assert "todayLockScreen.sync(snapshot, workSession.checkpointed())" in dashboard
|
|
||||||
assert "todayLockScreen.consumeLaunchAction()" in dashboard
|
|
||||||
assert '"static/today-lock-screen.js"' in bundle
|
|
||||||
|
|
@ -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():
|
def test_readiness_runtime_is_available_in_offline_shell():
|
||||||
service_worker = SERVICE_WORKER.read_text()
|
service_worker = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "const CACHE = 'stackchain-dashboard-shell-v119';" in service_worker
|
assert "const CACHE = 'stackchain-dashboard-shell-v118';" in service_worker
|
||||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
||||||
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-v119" in source
|
assert "stackchain-dashboard-shell-v118" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user