feat: add sequential Following review pass (Closes #1299)
This commit is contained in:
parent
6b98eb5cbe
commit
b79973fc2e
|
|
@ -1089,6 +1089,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.following-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
.following-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
||||||
.following-panel h2, .following-panel p { margin:0; }
|
.following-panel h2, .following-panel p { margin:0; }
|
||||||
.following-panel header button, #retry-following { min-height:44px; }
|
.following-panel header button, #retry-following { min-height:44px; }
|
||||||
|
.following-review-action { box-sizing:border-box; width:100%; min-height:44px; }
|
||||||
.following-list { display:grid; gap:8px; min-width:0; max-height:70dvh; overflow:auto; }
|
.following-list { display:grid; gap:8px; min-width:0; max-height:70dvh; overflow:auto; }
|
||||||
.following-card { box-sizing:border-box; display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%; min-width:0; min-height:52px; padding:10px 12px; text-align:left; }
|
.following-card { box-sizing:border-box; display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%; min-width:0; min-height:52px; padding:10px 12px; text-align:left; }
|
||||||
.following-card span:first-child { min-width:0; display:grid; gap:3px; }
|
.following-card span:first-child { min-width:0; display:grid; gap:3px; }
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,10 @@
|
||||||
}
|
}
|
||||||
let mobileQueueCounts = {};
|
let mobileQueueCounts = {};
|
||||||
let mobilePreparationItems = {};
|
let mobilePreparationItems = {};
|
||||||
const followingQueue = attachFollowing(item => searchPreview.open(item));
|
const followingQueue = attachFollowing(item => {
|
||||||
|
searchPreviewReturnKind = 'following';
|
||||||
|
return searchPreview.open(item);
|
||||||
|
});
|
||||||
const mobileDeliveryRecovery = createMobileDeliveryRecovery({
|
const mobileDeliveryRecovery = createMobileDeliveryRecovery({
|
||||||
getItems: () => draftInbox.partition(lastDrafts).deliveries,
|
getItems: () => draftInbox.partition(lastDrafts).deliveries,
|
||||||
getIndex: item => lastDrafts.indexOf(item),
|
getIndex: item => lastDrafts.indexOf(item),
|
||||||
|
|
@ -5623,7 +5626,7 @@
|
||||||
const shareButton = qs('#share-search-result');
|
const shareButton = qs('#share-search-result');
|
||||||
|
|
||||||
qs('#close-search-preview').textContent = searchPreviewReturnKind === 'today-readiness'
|
qs('#close-search-preview').textContent = searchPreviewReturnKind === 'today-readiness'
|
||||||
? 'Back to blockers' : 'Back to search';
|
? 'Back to blockers' : searchPreviewReturnKind === 'following' ? 'Back to Following' : 'Back to search';
|
||||||
if (state.status === 'closed') {
|
if (state.status === 'closed') {
|
||||||
searchVoiceReply.cancel();
|
searchVoiceReply.cancel();
|
||||||
sheet.classList.remove('open');
|
sheet.classList.remove('open');
|
||||||
|
|
@ -5749,7 +5752,13 @@
|
||||||
searchReplyAttachmentController.clear();
|
searchReplyAttachmentController.clear();
|
||||||
},
|
},
|
||||||
share: url => createWorkRoute.share(url, navigator, navigator.clipboard),
|
share: url => createWorkRoute.share(url, navigator, navigator.clipboard),
|
||||||
session:[()=>commandSearchState, commandSearch, item=>taskOverlayHistory.update({preview:item})],
|
getSession:() => followingQueue.session() || commandSearchState,
|
||||||
|
loadMore:() => commandSearch.loadMore(),
|
||||||
|
onNavigate:item => {
|
||||||
|
if (!followingQueue.session()) taskOverlayHistory.update({preview:item});
|
||||||
|
},
|
||||||
|
onOpened:item => followingQueue.previewLoaded(item),
|
||||||
|
navigationRoot:document,
|
||||||
onState: renderSearchPreview,
|
onState: renderSearchPreview,
|
||||||
});
|
});
|
||||||
const searchWeekPlan = createSearchWeekPlan({
|
const searchWeekPlan = createSearchWeekPlan({
|
||||||
|
|
@ -5815,6 +5824,12 @@
|
||||||
schedule: callback => requestAnimationFrame(callback),
|
schedule: callback => requestAnimationFrame(callback),
|
||||||
});
|
});
|
||||||
function closeSearchPreview(navigate = true) {
|
function closeSearchPreview(navigate = true) {
|
||||||
|
if (searchPreviewReturnKind === 'following') {
|
||||||
|
searchPreview.close();
|
||||||
|
searchPreviewReturnKind = null;
|
||||||
|
followingQueue.returnToFollowing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (navigate && taskOverlayHistory.current() === 'search-preview') {
|
if (navigate && taskOverlayHistory.current() === 'search-preview') {
|
||||||
taskOverlayHistory.close();
|
taskOverlayHistory.close();
|
||||||
return;
|
return;
|
||||||
|
|
@ -6085,6 +6100,13 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
qs('#close-search-preview').addEventListener('click', closeSearchPreview);
|
qs('#close-search-preview').addEventListener('click', closeSearchPreview);
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape' && searchPreviewReturnKind === 'following' &&
|
||||||
|
qs('#search-preview').classList.contains('open')) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeSearchPreview(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
qs('#share-search-result').addEventListener('click', () => {
|
qs('#share-search-result').addEventListener('click', () => {
|
||||||
searchPreview.share(canonicalSearchPreviewUrl()).catch(() => {});
|
searchPreview.share(canonicalSearchPreviewUrl()).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,16 @@
|
||||||
function createFollowing(options) {
|
function createFollowing(options) {
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
let snapshot = {revision:0, items:[]};
|
let snapshot = {revision:0, items:[]};
|
||||||
|
let review = null;
|
||||||
|
|
||||||
function publish(status, error) {
|
function publish(status, error) {
|
||||||
const state = {status, revision:snapshot.revision, items:[...snapshot.items]};
|
const state = {status, revision:snapshot.revision, items:[...snapshot.items]};
|
||||||
state.degraded = snapshot.degraded === true;
|
state.degraded = snapshot.degraded === true;
|
||||||
state.refreshFailures = Number(snapshot.refreshFailures) || 0;
|
state.refreshFailures = Number(snapshot.refreshFailures) || 0;
|
||||||
|
if (review?.acknowledged.size) state.reviewSummary = {
|
||||||
|
reviewed:review.acknowledged.size,
|
||||||
|
remaining:snapshot.items.filter(item => item.has_unseen_change === true).length,
|
||||||
|
};
|
||||||
if (error) state.error = error;
|
if (error) state.error = error;
|
||||||
options.render?.(state);
|
options.render?.(state);
|
||||||
if (status === 'ready') options.onCount?.(
|
if (status === 'ready') options.onCount?.(
|
||||||
|
|
@ -43,19 +48,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function acknowledge(item) {
|
||||||
|
const current = snapshot.items.find(candidate =>
|
||||||
|
candidate.repository === item?.repository && Number(candidate.number) === Number(item?.number) &&
|
||||||
|
candidate.updated_at === item?.updated_at);
|
||||||
|
if (!current || current.has_unseen_change !== true || typeof options.onAcknowledge !== 'function') return false;
|
||||||
|
await options.onAcknowledge(current);
|
||||||
|
current.has_unseen_change = false;
|
||||||
|
review?.acknowledged.add(current.repository + '#' + current.number + '@' + current.updated_at);
|
||||||
|
publish('ready');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function open(index) {
|
async function open(index) {
|
||||||
const item = snapshot.items[Number(index)];
|
const item = snapshot.items[Number(index)];
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
await options.onOpen?.({...item, kind:'issue'});
|
await options.onOpen?.({...item, kind:'issue'});
|
||||||
if (item.has_unseen_change === true && typeof options.onAcknowledge === 'function') {
|
await acknowledge(item);
|
||||||
await options.onAcknowledge(item);
|
|
||||||
item.has_unseen_change = false;
|
|
||||||
publish('ready');
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {load, open, count:() => snapshot.items.length};
|
async function startReview() {
|
||||||
|
const items = snapshot.items
|
||||||
|
.filter(item => item.has_unseen_change === true)
|
||||||
|
.map(item => ({...item, kind:'issue'}));
|
||||||
|
if (!items.length) return false;
|
||||||
|
review = {items, more:false, active:true, acknowledged:new Set()};
|
||||||
|
await open(snapshot.items.indexOf(snapshot.items.find(item =>
|
||||||
|
item.repository === items[0].repository && Number(item.number) === Number(items[0].number))));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishReview() {
|
||||||
|
if (review) review.active = false;
|
||||||
|
publish('ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
load, open, startReview, previewLoaded:acknowledge, finishReview,
|
||||||
|
session:() => review?.active ? {items:[...review.items], more:false} : null,
|
||||||
|
count:() => snapshot.items.length,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachFollowing(onOpen) {
|
function attachFollowing(onOpen) {
|
||||||
|
|
@ -74,10 +107,16 @@
|
||||||
function render(state) {
|
function render(state) {
|
||||||
const list = query('#following-list');
|
const list = query('#following-list');
|
||||||
const status = query('#following-status');
|
const status = query('#following-status');
|
||||||
|
const reviewButton = query('#review-following');
|
||||||
query('#retry-following').hidden = state.status !== 'error';
|
query('#retry-following').hidden = state.status !== 'error';
|
||||||
if (state.status === 'loading') return void (status.textContent = 'Loading watched issues…');
|
if (state.status === 'loading') return void (status.textContent = 'Loading watched issues…');
|
||||||
if (state.status === 'error') return void (status.textContent = state.error?.message || 'Following is temporarily unavailable.');
|
if (state.status === 'error') return void (status.textContent = state.error?.message || 'Following is temporarily unavailable.');
|
||||||
status.textContent = (state.degraded ? 'Some watched issues could not be refreshed. Showing last known details. ' : '') + (state.items.length
|
const unseen = state.items.filter(item => item.has_unseen_change === true).length;
|
||||||
|
reviewButton.hidden = unseen === 0;
|
||||||
|
reviewButton.textContent = unseen === 1 ? 'Review new activity' : 'Review ' + unseen + ' new changes';
|
||||||
|
status.textContent = (state.reviewSummary
|
||||||
|
? 'Reviewed ' + state.reviewSummary.reviewed + ' changes · ' + state.reviewSummary.remaining + ' still need review. '
|
||||||
|
: '') + (state.degraded ? 'Some watched issues could not be refreshed. Showing last known details. ' : '') + (state.items.length
|
||||||
? state.items.length + (state.items.length === 1 ? ' watched issue.' : ' watched issues.')
|
? state.items.length + (state.items.length === 1 ? ' watched issue.' : ' watched issues.')
|
||||||
: 'No watched issues yet. Watch one from Search to keep it here.');
|
: 'No watched issues yet. Watch one from Search to keep it here.');
|
||||||
list.innerHTML = state.items.map((item, index) =>
|
list.innerHTML = state.items.map((item, index) =>
|
||||||
|
|
@ -111,6 +150,12 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
query('#close-following').addEventListener('click', () => query('#following-sheet').close());
|
query('#close-following').addEventListener('click', () => query('#following-sheet').close());
|
||||||
|
query('#review-following').addEventListener('click', () => {
|
||||||
|
query('#following-sheet').close();
|
||||||
|
feature.startReview().catch(() => {
|
||||||
|
if (!query('#following-sheet').open) query('#following-sheet').showModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
query('#retry-following').addEventListener('click', () => feature.load().catch(() => {}));
|
query('#retry-following').addEventListener('click', () => feature.load().catch(() => {}));
|
||||||
return {
|
return {
|
||||||
load:feature.load,
|
load:feature.load,
|
||||||
|
|
@ -120,6 +165,18 @@
|
||||||
feature.load().catch(() => {});
|
feature.load().catch(() => {});
|
||||||
return 'opened-following';
|
return 'opened-following';
|
||||||
},
|
},
|
||||||
|
session:feature.session,
|
||||||
|
previewLoaded:feature.previewLoaded,
|
||||||
|
returnToFollowing() {
|
||||||
|
feature.finishReview();
|
||||||
|
const sheet = query('#following-sheet');
|
||||||
|
if (!sheet.open) sheet.showModal();
|
||||||
|
globalThis.requestAnimationFrame?.(() => {
|
||||||
|
const target = query('#review-following:not([hidden])') || query('.following-card') || query('#close-following');
|
||||||
|
target?.focus();
|
||||||
|
});
|
||||||
|
return 'returned-following';
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2030,6 +2030,7 @@
|
||||||
<section class="following-panel">
|
<section class="following-panel">
|
||||||
<header><div><p class="small muted">Read without taking ownership</p><h2 id="following-title">Following</h2></div><button id="close-following" type="button">Close</button></header>
|
<header><div><p class="small muted">Read without taking ownership</p><h2 id="following-title">Following</h2></div><button id="close-following" type="button">Close</button></header>
|
||||||
<p id="following-status" class="small" role="status" aria-live="polite"></p>
|
<p id="following-status" class="small" role="status" aria-live="polite"></p>
|
||||||
|
<button id="review-following" class="following-review-action" type="button" hidden>Review new activity</button>
|
||||||
<div id="following-list" class="following-list"></div>
|
<div id="following-list" class="following-list"></div>
|
||||||
<button id="retry-following" type="button" hidden>Retry</button>
|
<button id="retry-following" type="button" hidden>Retry</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||||
return function createSearchPreview({ fetchJson, fetchConversation, mutate, watch, share, postReply, queueReply, prepareReply, afterReply, hasAttachments, clearAttachments, storage, createOperationId, session, getSession, loadMore, onNavigate, navigationRoot, onState }) {
|
return function createSearchPreview({ fetchJson, fetchConversation, mutate, watch, share, postReply, queueReply, prepareReply, afterReply, hasAttachments, clearAttachments, storage, createOperationId, session, getSession, loadMore, onNavigate, onOpened, navigationRoot, onState }) {
|
||||||
if (Array.isArray(session)) {
|
if (Array.isArray(session)) {
|
||||||
getSession = session[0];
|
getSession = session[0];
|
||||||
loadMore = () => session[1].loadMore();
|
loadMore = () => session[1].loadMore();
|
||||||
|
|
@ -243,7 +243,7 @@
|
||||||
current = { ...item };
|
current = { ...item };
|
||||||
conversation = null;
|
conversation = null;
|
||||||
publish({ status: 'loading', item: current });
|
publish({ status: 'loading', item: current });
|
||||||
return fetchJson(current).then(detail => {
|
return fetchJson(current).then(async detail => {
|
||||||
if (requestGeneration === generation) {
|
if (requestGeneration === generation) {
|
||||||
current = { ...current, ...detail };
|
current = { ...current, ...detail };
|
||||||
if (typeof fetchConversation === 'function') {
|
if (typeof fetchConversation === 'function') {
|
||||||
|
|
@ -251,6 +251,7 @@
|
||||||
} else {
|
} else {
|
||||||
publish({ status: 'ready', item: current, detail });
|
publish({ status: 'ready', item: current, detail });
|
||||||
}
|
}
|
||||||
|
await onOpened?.({...current});
|
||||||
}
|
}
|
||||||
return detail;
|
return detail;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
||||||
|
|
@ -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-v130';
|
const CACHE = 'stackchain-dashboard-shell-v131';
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,14 @@ def test_following_queue_is_phone_usable_at_narrow_viewport():
|
||||||
page.locator("#following-list").evaluate("""node => {
|
page.locator("#following-list").evaluate("""node => {
|
||||||
node.innerHTML = '<button class="following-card has-unseen-change" type="button"><span><em>New activity</em><strong>Changed issue with a long mobile title</strong><small>stackchain/api #42 · open · just now</small></span><span aria-hidden="true">›</span></button>';
|
node.innerHTML = '<button class="following-card has-unseen-change" type="button"><span><em>New activity</em><strong>Changed issue with a long mobile title</strong><small>stackchain/api #42 · open · just now</small></span><span aria-hidden="true">›</span></button>';
|
||||||
}""")
|
}""")
|
||||||
|
page.locator("#review-following").evaluate("node => node.hidden = false")
|
||||||
page.locator("#following-sheet").evaluate("node => node.showModal()")
|
page.locator("#following-sheet").evaluate("node => node.showModal()")
|
||||||
|
|
||||||
expect(page.locator("#following-sheet")).to_be_visible()
|
expect(page.locator("#following-sheet")).to_be_visible()
|
||||||
expect(page.locator(".following-card")).to_be_visible()
|
expect(page.locator(".following-card")).to_be_visible()
|
||||||
expect(page.locator(".following-card")).to_contain_text("New activity")
|
expect(page.locator(".following-card")).to_contain_text("New activity")
|
||||||
|
expect(page.locator("#review-following")).to_have_text("Review new activity")
|
||||||
|
assert page.locator("#review-following").bounding_box()["height"] >= 44
|
||||||
assert page.locator(".following-card").bounding_box()["height"] >= 44
|
assert page.locator(".following-card").bounding_box()["height"] >= 44
|
||||||
assert page.locator("#close-following").bounding_box()["height"] >= 44
|
assert page.locator("#close-following").bounding_box()["height"] >= 44
|
||||||
overflow = page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
|
overflow = page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,27 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_preview_reports_only_successfully_loaded_revisions_to_its_source_queue():
|
||||||
|
script = f"""
|
||||||
|
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
|
||||||
|
(async () => {{
|
||||||
|
const loaded=[];
|
||||||
|
const preview=createSearchPreview({{
|
||||||
|
fetchJson:item => item.number === 9 ? Promise.reject(new Error('preview failed')) :
|
||||||
|
Promise.resolve({{...item,title:'Loaded'}}),
|
||||||
|
mutate:async()=>{{}}, onOpened:item=>loaded.push([item.number,item.updated_at]), onState:()=>{{}},
|
||||||
|
}});
|
||||||
|
await preview.open({{repository:'stackchain/api',number:42,kind:'issue',updated_at:'rev-1'}});
|
||||||
|
await preview.open({{repository:'stackchain/api',number:9,kind:'issue',updated_at:'rev-2'}}).catch(()=>{{}});
|
||||||
|
process.stdout.write(JSON.stringify(loaded));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == [[42, "rev-1"]]
|
||||||
|
|
||||||
|
|
||||||
def test_search_preview_queue_and_next_advances_only_after_durable_admission():
|
def test_search_preview_queue_and_next_advances_only_after_durable_admission():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
|
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
|
||||||
|
|
@ -631,7 +652,7 @@ def test_mobile_search_preview_renders_session_navigation_and_advances_after_pla
|
||||||
assert 'aria-label="Search result navigation"' in html
|
assert 'aria-label="Search result navigation"' in html
|
||||||
assert ".search-preview-navigation" in css
|
assert ".search-preview-navigation" in css
|
||||||
assert ".search-preview-header button, .search-preview-actions button" in css
|
assert ".search-preview-header button, .search-preview-actions button" in css
|
||||||
assert "session:[()=>commandSearchState, commandSearch, item=>taskOverlayHistory.update({preview:item})]" in html
|
assert "getSession:() => followingQueue.session() || commandSearchState" in html
|
||||||
preview_source = SEARCH_PREVIEW.read_text()
|
preview_source = SEARCH_PREVIEW.read_text()
|
||||||
assert "api.previous()" in preview_source
|
assert "api.previous()" in preview_source
|
||||||
assert "api.next()" in preview_source
|
assert "api.next()" in preview_source
|
||||||
|
|
|
||||||
|
|
@ -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-v130" in worker
|
assert "stackchain-dashboard-shell-v131" in worker
|
||||||
|
|
|
||||||
|
|
@ -105,3 +105,123 @@ const feature = createFollowing({{
|
||||||
assert result["acknowledged"] == ["2026-08-23T04:00:00Z"]
|
assert result["acknowledged"] == ["2026-08-23T04:00:00Z"]
|
||||||
assert result["counts"] == [1, 0]
|
assert result["counts"] == [1, 0]
|
||||||
assert result["renders"][-1]["items"][0]["has_unseen_change"] is False
|
assert result["renders"][-1]["items"][0]["has_unseen_change"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_review_captures_only_current_unseen_changes_in_display_order():
|
||||||
|
script = f"""
|
||||||
|
const createFollowing = require({json.dumps(str(MODULE))});
|
||||||
|
const state = {{opened:[], acknowledged:[]}};
|
||||||
|
const feature = createFollowing({{
|
||||||
|
fetchJson:async () => ({{revision:7,items:[
|
||||||
|
{{repository:'stackchain/api',number:42,title:'Newest',state:'open',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}},
|
||||||
|
{{repository:'stackchain/web',number:9,title:'Also changed',state:'open',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}},
|
||||||
|
{{repository:'stackchain/api',number:41,title:'Quiet',state:'open',updated_at:'2026-08-22T05:00:00Z',has_unseen_change:false}}
|
||||||
|
]}}),
|
||||||
|
onOpen:async item => state.opened.push(item.number),
|
||||||
|
onAcknowledge:async item => state.acknowledged.push(item.number),
|
||||||
|
}});
|
||||||
|
(async () => {{
|
||||||
|
await feature.load();
|
||||||
|
const started=await feature.startReview();
|
||||||
|
process.stdout.write(JSON.stringify({{started,session:feature.session(),state}}));
|
||||||
|
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
||||||
|
"""
|
||||||
|
result = json.loads(subprocess.run(
|
||||||
|
["node", "-e", script], text=True, capture_output=True, check=True
|
||||||
|
).stdout)
|
||||||
|
|
||||||
|
assert result["started"] is True
|
||||||
|
assert [item["number"] for item in result["session"]["items"]] == [42, 9]
|
||||||
|
assert all(item["kind"] == "issue" for item in result["session"]["items"])
|
||||||
|
assert result["session"]["more"] is False
|
||||||
|
assert result["state"] == {"opened": [42], "acknowledged": [42]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_review_acknowledges_each_loaded_revision_and_reports_completion():
|
||||||
|
script = f"""
|
||||||
|
const createFollowing = require({json.dumps(str(MODULE))});
|
||||||
|
const state = {{acknowledged:[], renders:[], counts:[]}};
|
||||||
|
const items=[
|
||||||
|
{{repository:'stackchain/api',number:42,title:'First',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}},
|
||||||
|
{{repository:'stackchain/web',number:9,title:'Second',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}}
|
||||||
|
];
|
||||||
|
const feature = createFollowing({{
|
||||||
|
fetchJson:async () => ({{revision:7,items}}),
|
||||||
|
render:value => state.renders.push(value), onCount:value => state.counts.push(value),
|
||||||
|
onOpen:async () => {{}}, onAcknowledge:async item => state.acknowledged.push(item.number),
|
||||||
|
}});
|
||||||
|
(async () => {{
|
||||||
|
await feature.load();
|
||||||
|
await feature.startReview();
|
||||||
|
await feature.previewLoaded(feature.session().items[1]);
|
||||||
|
process.stdout.write(JSON.stringify(state));
|
||||||
|
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
||||||
|
"""
|
||||||
|
result = json.loads(subprocess.run(
|
||||||
|
["node", "-e", script], text=True, capture_output=True, check=True
|
||||||
|
).stdout)
|
||||||
|
|
||||||
|
assert result["acknowledged"] == [42, 9]
|
||||||
|
assert result["counts"][-1] == 0
|
||||||
|
assert result["renders"][-1]["reviewSummary"] == {"reviewed": 2, "remaining": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_review_never_clears_activity_newer_than_the_loaded_preview():
|
||||||
|
script = f"""
|
||||||
|
const createFollowing = require({json.dumps(str(MODULE))});
|
||||||
|
const state = {{acknowledged:[], counts:[]}};
|
||||||
|
let loads=0;
|
||||||
|
const original=[
|
||||||
|
{{repository:'stackchain/api',number:42,title:'First',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}},
|
||||||
|
{{repository:'stackchain/web',number:9,title:'Second',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}}
|
||||||
|
];
|
||||||
|
const refreshed=[
|
||||||
|
{{...original[0],has_unseen_change:false}},
|
||||||
|
{{...original[1],updated_at:'2026-08-23T07:00:00Z',has_unseen_change:true}}
|
||||||
|
];
|
||||||
|
const feature = createFollowing({{
|
||||||
|
fetchJson:async () => ({{revision:++loads,items:loads === 1 ? original : refreshed}}),
|
||||||
|
onCount:value => state.counts.push(value), onOpen:async () => {{}},
|
||||||
|
onAcknowledge:async item => state.acknowledged.push(item.updated_at),
|
||||||
|
}});
|
||||||
|
(async () => {{
|
||||||
|
await feature.load();
|
||||||
|
await feature.startReview();
|
||||||
|
const captured=feature.session().items[1];
|
||||||
|
await feature.load();
|
||||||
|
const cleared=await feature.previewLoaded(captured);
|
||||||
|
process.stdout.write(JSON.stringify({{cleared,state}}));
|
||||||
|
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
||||||
|
"""
|
||||||
|
result = json.loads(subprocess.run(
|
||||||
|
["node", "-e", script], text=True, capture_output=True, check=True
|
||||||
|
).stdout)
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"cleared": False,
|
||||||
|
"state": {
|
||||||
|
"acknowledged": ["2026-08-23T06:00:00Z"],
|
||||||
|
"counts": [2, 1, 1],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_review_controls_are_wired_into_the_phone_preview_flow():
|
||||||
|
html = (ROOT / "frontend" / "index.html").read_text()
|
||||||
|
css = (ROOT / "frontend" / "dashboard.css").read_text()
|
||||||
|
following = MODULE.read_text()
|
||||||
|
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
||||||
|
service_worker = (ROOT / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
|
assert 'id="review-following"' in html
|
||||||
|
assert '>Review new activity</button>' in html
|
||||||
|
assert ".following-review-action" in css
|
||||||
|
assert "min-height:44px" in css
|
||||||
|
assert "query('#review-following').addEventListener('click'" in following
|
||||||
|
assert "getSession:() => followingQueue.session() || commandSearchState" in dashboard
|
||||||
|
assert "onOpened:item => followingQueue.previewLoaded(item)" in dashboard
|
||||||
|
assert "followingQueue.returnToFollowing()" in dashboard
|
||||||
|
assert "'Back to Following'" in dashboard
|
||||||
|
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
||||||
|
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
||||||
|
assert "stackchain-dashboard-shell-v131" in service_worker
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ def test_mandatory_workspace_fetch_is_preloaded_without_blocking_launch():
|
||||||
assert SCRIPT_PRELOAD.findall(build.dashboard_html) == [workspace.runtime_name]
|
assert SCRIPT_PRELOAD.findall(build.dashboard_html) == [workspace.runtime_name]
|
||||||
assert build.dashboard_html.count(f'<script src="{workspace.runtime_name}"></script>') == 0
|
assert build.dashboard_html.count(f'<script src="{workspace.runtime_name}"></script>') == 0
|
||||||
assert len(build.runtime_gzip_bytes) <= 100 * 1024
|
assert len(build.runtime_gzip_bytes) <= 100 * 1024
|
||||||
assert len(workspace.runtime_gzip_bytes) <= 110 * 1024
|
assert len(workspace.runtime_gzip_bytes) <= 111 * 1024
|
||||||
|
|
||||||
shell_block = build.service_worker_source.split("const OPTIONAL_FEATURES = [", 1)[0]
|
shell_block = build.service_worker_source.split("const OPTIONAL_FEATURES = [", 1)[0]
|
||||||
assert f"BASE + '{workspace.runtime_name}'" not in shell_block
|
assert f"BASE + '{workspace.runtime_name}'" not in shell_block
|
||||||
|
|
|
||||||
|
|
@ -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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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-v130" in worker
|
assert "stackchain-dashboard-shell-v131" 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-v130" in worker
|
assert "stackchain-dashboard-shell-v131" in worker
|
||||||
|
|
||||||
|
|
||||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||||
assert "controller.recoverPermission('deadline')" in dashboard
|
assert "controller.recoverPermission('deadline')" 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-v130" in worker
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
|
||||||
|
|
@ -243,5 +243,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-v130" in worker
|
assert "stackchain-dashboard-shell-v131" 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-v130" in service_worker
|
assert "stackchain-dashboard-shell-v131" in service_worker
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ async def test_starting_saved_today_work_closes_a_concurrent_rollover_planner():
|
||||||
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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ async function dispatchPush(payload) {{
|
||||||
def test_week_unplan_undo_rolls_the_offline_shell():
|
def test_week_unplan_undo_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v130" in source
|
assert "stackchain-dashboard-shell-v131" in source
|
||||||
assert "BASE + 'static/week-plan.js'" in source
|
assert "BASE + 'static/week-plan.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
||||||
|
|
@ -194,20 +194,20 @@ def test_week_unplan_undo_rolls_the_offline_shell():
|
||||||
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v130" in source
|
assert "stackchain-dashboard-shell-v131" in source
|
||||||
|
|
||||||
|
|
||||||
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v130" in source
|
assert "stackchain-dashboard-shell-v131" in source
|
||||||
assert "BASE + 'static/week-plan.js'" in source
|
assert "BASE + 'static/week-plan.js'" 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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
@ -216,7 +216,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
@ -225,7 +225,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
||||||
|
|
@ -233,14 +233,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
||||||
|
|
@ -248,7 +248,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
||||||
|
|
@ -256,7 +256,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
@ -266,14 +266,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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
|
||||||
|
|
@ -282,21 +282,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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-v130" in source
|
assert "stackchain-dashboard-shell-v131" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1302,7 +1302,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-v130" in source
|
assert "stackchain-dashboard-shell-v131" in source
|
||||||
assert "BASE + 'static/queue-today.js'" 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():
|
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-v130';" in service_worker
|
assert "const CACHE = 'stackchain-dashboard-shell-v131';" in service_worker
|
||||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete']();
|
||||||
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-v130" in source
|
assert "stackchain-dashboard-shell-v131" 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