Add mobile Today progress updates without advancing work #1053
|
|
@ -269,6 +269,16 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.today-break-status:has(> span[hidden]) { display:none; }
|
||||
.today-break-status button { min-height:44px; min-width:112px; }
|
||||
@media(max-width:359px) { .today-break-actions { grid-template-columns:1fr; } }
|
||||
.today-progress-sheet { position:fixed; inset:0; width:100%; max-width:none; height:100%; max-height:none; margin:0; padding:0; border:0; background:rgba(5,12,21,.82); color:#e5e7eb; }
|
||||
.today-progress-sheet::backdrop { background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
|
||||
.today-progress-panel { position:absolute; left:0; right:0; bottom:0; box-sizing:border-box; width:min(620px,100%); max-height:100dvh; margin:auto; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #31577f; border-radius:18px 18px 0 0; background:#0b1526; }
|
||||
.today-progress-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
||||
.today-progress-panel h2, .today-progress-panel p { margin-top:0; }
|
||||
.today-progress-panel header button, .today-progress-actions button { min-height:44px; }
|
||||
.today-progress-panel textarea { box-sizing:border-box; width:100%; min-height:120px; resize:vertical; }
|
||||
.today-progress-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; }
|
||||
.today-progress-actions button { width:100%; }
|
||||
@media(max-width:359px) { .today-progress-actions { grid-template-columns:1fr; } }
|
||||
.today-recap-sheet { position:fixed; inset:0; z-index:88; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
|
||||
.today-recap-sheet[hidden] { display:none; }
|
||||
.today-recap-panel { box-sizing:border-box; width:min(620px,100%); max-height:100%; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #2a496e; border-radius:18px 18px 0 0; background:#0b1526; }
|
||||
|
|
@ -1087,10 +1097,11 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.mobile-today-hud [data-mobile-today-complete] { grid-area:complete; }
|
||||
.mobile-today-hud [data-mobile-today-toggle] { grid-area:toggle; }
|
||||
.mobile-today-hud button { min-height:44px; max-width:100%; }
|
||||
.mobile-today-hud [data-mobile-today-update] { grid-column:1 / -1; width:100%; }
|
||||
.mobile-today-hud [data-today-break-open] { grid-column:1 / -1; }
|
||||
|
||||
.mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; }
|
||||
.today-completion-undo { bottom:calc(224px + env(safe-area-inset-bottom)); }
|
||||
.today-completion-undo { bottom:calc(272px + env(safe-area-inset-bottom)); }
|
||||
}
|
||||
@media (min-width:701px) { .update-gesture-status { display:none; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
|
|
|||
|
|
@ -976,6 +976,11 @@
|
|||
getOwnerLogin: () => confirmedOwnerLogin,
|
||||
mergeChecklistConflict: mergeChecklistConflict,
|
||||
});
|
||||
const todayProgress = createTodayProgress({
|
||||
storage:localStorage,
|
||||
getLogin:() => confirmedOwnerLogin,
|
||||
admit:message => authoredOutbox.enqueueDurably(message),
|
||||
});
|
||||
const queueOfflineIssueBlocker = createOfflineIssueBlocker({
|
||||
enqueueDurably: message => authoredOutbox.enqueueDurably(message),
|
||||
});
|
||||
|
|
@ -1909,6 +1914,7 @@
|
|||
qs('#resume-today-session').hidden = active || selectedWorkFilter !== 'today' ||
|
||||
!todayMyWork.length || !workSession.resumable();
|
||||
qs('#end-today-session').hidden = !active;
|
||||
todayProgressView.update();
|
||||
updateDetailDeferLabels(active);
|
||||
mobileTaskDock.updateWork(mobileWorkEntry.mode());
|
||||
mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention);
|
||||
|
|
@ -1938,6 +1944,7 @@
|
|||
document.querySelectorAll('[data-work-session-complete]').forEach(button => {
|
||||
button.hidden = !workSession.checkpointed();
|
||||
});
|
||||
todayProgressView.update();
|
||||
},
|
||||
onFinish: () => {
|
||||
updateWorkSessionActions();
|
||||
|
|
@ -1951,6 +1958,22 @@
|
|||
todayRecapView.finish(selectedWorkFilter);
|
||||
},
|
||||
});
|
||||
function currentTodayProgressTarget() {
|
||||
if (!workSession.checkpointed()) return null;
|
||||
const item = workSession.target('continue');
|
||||
if (!item || item.is_review || !['issue', 'pull'].includes(item.kind) ||
|
||||
!item.repository || !Number.isInteger(item.number)) return null;
|
||||
return {
|
||||
identity:todayWork.identity(item), kind:item.kind, repository:item.repository,
|
||||
number:item.number, label:item.key || item.repository + '#' + item.number,
|
||||
title:item.title || '',
|
||||
};
|
||||
}
|
||||
const todayProgressView = createTodayProgressView({
|
||||
progress:todayProgress, currentTarget:currentTodayProgressTarget, qs,
|
||||
announce:message => { qs('#my-work-action-status').textContent = message; },
|
||||
onAdmitted:() => refreshMyWorkView(),
|
||||
});
|
||||
function selectTodayWork() {
|
||||
qs('[data-work-filter="today"]').click();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1536,9 +1536,24 @@
|
|||
<button data-mobile-today-complete type="button" hidden>Done & next</button>
|
||||
<div class="small" data-work-session-progress aria-live="polite"></div>
|
||||
<button data-mobile-today-toggle data-work-session-timer-toggle type="button">Pause timer</button>
|
||||
<button data-mobile-today-update type="button" hidden>Add update</button>
|
||||
<button data-today-break-open type="button">Take break</button>
|
||||
<button data-work-session-adjust-plan type="button" hidden>Adjust remaining plan</button>
|
||||
</section>
|
||||
<dialog class="today-progress-sheet" id="today-progress-sheet" aria-labelledby="today-progress-title">
|
||||
<section class="today-progress-panel">
|
||||
<header><div><p class="small muted">Active Today item</p><h2 id="today-progress-title">Add progress update</h2></div><button id="cancel-today-progress" type="button">Cancel</button></header>
|
||||
<p id="today-progress-target" class="small"></p>
|
||||
<label for="today-progress-body">Update</label>
|
||||
<textarea id="today-progress-body" rows="5" maxlength="2000" placeholder="What changed, what you learned, or what comes next"></textarea>
|
||||
<p class="small muted">Save privately on this device, or post to the exact Gitea item. Today keeps running either way.</p>
|
||||
<p id="today-progress-status" class="small" role="status" aria-live="polite"></p>
|
||||
<div class="today-progress-actions">
|
||||
<button id="save-today-progress" type="button">Save to this item</button>
|
||||
<button id="post-today-progress" type="button">Post now</button>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
<div class="today-break-status" role="status" aria-live="polite">
|
||||
<span id="today-break-status" hidden></span>
|
||||
<button id="resume-today-break" type="button" hidden>Resume Today</button>
|
||||
|
|
@ -1655,6 +1670,7 @@
|
|||
<script src="static/today-work.js"></script>
|
||||
<script src="static/today-timer.js"></script>
|
||||
<script src="static/today-break.js"></script>
|
||||
<script src="static/today-progress.js"></script>
|
||||
<script src="static/today-lock-screen.js"></script>
|
||||
<script src="static/today-session-sync.js"></script>
|
||||
<script src="static/today-recap.js"></script>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const SHELL = [
|
|||
BASE + 'static/today-work.js',
|
||||
BASE + 'static/today-timer.js',
|
||||
BASE + 'static/today-break.js',
|
||||
BASE + 'static/today-progress.js',
|
||||
BASE + 'static/today-lock-screen.js',
|
||||
BASE + 'static/today-session-sync.js',
|
||||
BASE + 'static/today-recap.js',
|
||||
|
|
|
|||
161
frontend/today-progress.js
Normal file
161
frontend/today-progress.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
function createTodayProgress({ storage, getLogin = () => '', admit, makeId = () =>
|
||||
globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random().toString(16).slice(2),
|
||||
maxLength = 2000, maxItems = 20 }) {
|
||||
const prefix = 'stackchain.today-progress.v1.';
|
||||
const login = () => String(getLogin() || '').trim().toLowerCase();
|
||||
const storageKey = () => login() ? prefix + encodeURIComponent(login()) : '';
|
||||
const validIdentity = identity => typeof identity === 'string' && identity.length > 0 && identity.length <= 500;
|
||||
const validRecord = record => record && typeof record.body === 'string' && record.body.length > 0 &&
|
||||
record.body.length <= maxLength && typeof record.operation_id === 'string' && record.operation_id.length > 0 &&
|
||||
record.operation_id.length <= 128;
|
||||
|
||||
function read() {
|
||||
const key = storageKey();
|
||||
if (!storage || !key) return {};
|
||||
try {
|
||||
const saved = JSON.parse(storage.getItem(key) || 'null');
|
||||
if (!saved || saved.version !== 1 || !saved.drafts || typeof saved.drafts !== 'object' ||
|
||||
Array.isArray(saved.drafts)) return {};
|
||||
const entries = Object.entries(saved.drafts).filter(([identity, record]) =>
|
||||
validIdentity(identity) && validRecord(record)
|
||||
).slice(0, maxItems);
|
||||
return Object.fromEntries(entries);
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function write(drafts) {
|
||||
const key = storageKey();
|
||||
if (!storage || !key) return false;
|
||||
try {
|
||||
if (Object.keys(drafts).length) storage.setItem(key, JSON.stringify({ version:1, drafts }));
|
||||
else storage.removeItem(key);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function load(identity) {
|
||||
if (!validIdentity(identity)) return '';
|
||||
return read()[identity]?.body || '';
|
||||
}
|
||||
|
||||
function save(identity, value) {
|
||||
if (!validIdentity(identity) || !storageKey()) return false;
|
||||
const body = String(value || '').trim();
|
||||
if (body.length > maxLength) return false;
|
||||
const drafts = read();
|
||||
if (!body) {
|
||||
delete drafts[identity];
|
||||
return write(drafts);
|
||||
}
|
||||
if (!drafts[identity] && Object.keys(drafts).length >= maxItems) return false;
|
||||
const previous = drafts[identity];
|
||||
drafts[identity] = {
|
||||
body,
|
||||
operation_id: previous?.body === body ? previous.operation_id : String(makeId()).slice(0, 128),
|
||||
};
|
||||
return write(drafts);
|
||||
}
|
||||
|
||||
function validTarget(target) {
|
||||
return target && ['issue', 'pull'].includes(target.kind) && validIdentity(target.identity) &&
|
||||
typeof target.repository === 'string' && target.repository.length > 0 && target.repository.length <= 200 &&
|
||||
Number.isInteger(target.number) && target.number > 0;
|
||||
}
|
||||
|
||||
async function post(target, value) {
|
||||
if (!validTarget(target)) throw new Error('An active Today issue or pull request is required.');
|
||||
if (value !== undefined && !save(target.identity, value)) throw new Error('Progress update could not be saved on this device.');
|
||||
const record = read()[target.identity];
|
||||
if (!record) throw new Error('Write a progress update before posting.');
|
||||
if (typeof admit !== 'function') throw new Error('Progress update delivery is unavailable.');
|
||||
const admission = await admit({
|
||||
kind:target.kind + '-comment', repository:target.repository, number:target.number,
|
||||
body:record.body, operationId:record.operation_id,
|
||||
});
|
||||
const drafts = read();
|
||||
if (drafts[target.identity]?.operation_id === record.operation_id) {
|
||||
delete drafts[target.identity];
|
||||
write(drafts);
|
||||
}
|
||||
return admission;
|
||||
}
|
||||
|
||||
return { load, save, discard:identity => save(identity, ''), post };
|
||||
}
|
||||
|
||||
function createTodayProgressView({ progress, currentTarget, qs, announce = () => {}, onAdmitted = () => {} }) {
|
||||
const sheet = qs('#today-progress-sheet');
|
||||
const body = qs('#today-progress-body');
|
||||
const status = qs('#today-progress-status');
|
||||
const launcher = qs('[data-mobile-today-update]');
|
||||
let openedTarget = null;
|
||||
|
||||
const update = () => { launcher.hidden = !currentTarget(); };
|
||||
const checkpoint = () => {
|
||||
if (!openedTarget) return false;
|
||||
if (progress.save(openedTarget.identity, body.value)) return true;
|
||||
status.textContent = 'Update must be 2,000 characters or fewer and device storage must be available.';
|
||||
return false;
|
||||
};
|
||||
const close = () => {
|
||||
if (sheet.open) sheet.close();
|
||||
openedTarget = null;
|
||||
};
|
||||
|
||||
launcher.addEventListener('click', () => {
|
||||
const target = currentTarget();
|
||||
if (!target) return;
|
||||
openedTarget = target;
|
||||
qs('#today-progress-target').textContent = target.label + (target.title ? ' · ' + target.title : '');
|
||||
body.value = progress.load(target.identity);
|
||||
status.textContent = '';
|
||||
sheet.showModal();
|
||||
body.focus();
|
||||
});
|
||||
qs('#cancel-today-progress').addEventListener('click', () => {
|
||||
if (checkpoint()) close();
|
||||
});
|
||||
sheet.addEventListener('cancel', event => {
|
||||
event.preventDefault();
|
||||
if (checkpoint()) close();
|
||||
});
|
||||
qs('#save-today-progress').addEventListener('click', () => {
|
||||
if (!checkpoint()) return;
|
||||
announce('Progress update saved privately to this Today item.');
|
||||
close();
|
||||
});
|
||||
qs('#post-today-progress').addEventListener('click', async () => {
|
||||
const target = currentTarget();
|
||||
if (!target || target.identity !== openedTarget?.identity) {
|
||||
status.textContent = 'The active Today item changed. Close and open its update again.';
|
||||
return;
|
||||
}
|
||||
const button = qs('#post-today-progress');
|
||||
button.disabled = true;
|
||||
status.textContent = 'Saving for delivery…';
|
||||
try {
|
||||
const admission = await progress.post(target, body.value);
|
||||
onAdmitted(admission);
|
||||
announce(admission.background ?
|
||||
'Progress update queued for delivery. Today is still on the same item.' :
|
||||
'Progress update saved for next launch. Today is still on the same item.');
|
||||
close();
|
||||
} catch (error) {
|
||||
status.textContent = error.message + ' Your update remains on this item; retry.';
|
||||
body.focus();
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
update();
|
||||
return { update };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = createTodayProgress;
|
||||
module.exports.createView = createTodayProgressView;
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ FEATURE_SOURCES = {
|
|||
"security-center": ("static/security-center.js",),
|
||||
"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/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-break.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-break.js", "static/today-progress.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-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/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||
|
|
|
|||
|
|
@ -1153,6 +1153,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
|||
"/dashboard/static/today-work.js",
|
||||
"/dashboard/static/today-timer.js",
|
||||
"/dashboard/static/today-break.js",
|
||||
"/dashboard/static/today-progress.js",
|
||||
"/dashboard/static/today-lock-screen.js",
|
||||
"/dashboard/static/today-session-sync.js",
|
||||
"/dashboard/static/today-recap.js",
|
||||
|
|
|
|||
128
tests/test_today_progress.py
Normal file
128
tests/test_today_progress.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
TODAY_PROGRESS = ROOT / "frontend" / "today-progress.js"
|
||||
|
||||
|
||||
def run_node(script: str) -> dict:
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_progress_drafts_are_account_and_item_scoped_bounded_and_durable():
|
||||
script = f"""
|
||||
const createProgress = require({json.dumps(str(TODAY_PROGRESS))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
let login = 'Timmy';
|
||||
const progress = createProgress({{storage, getLogin:() => login, admit:async () => ({{}}), maxLength:20}});
|
||||
const issue = {{identity:'issue:stackchain/dashboard:12:', kind:'issue', repository:'stackchain/dashboard', number:12}};
|
||||
const pull = {{identity:'pull:stackchain/dashboard:13:', kind:'pull', repository:'stackchain/dashboard', number:13}};
|
||||
const saved = progress.save(issue.identity, ' Investigated latency ');
|
||||
progress.save(pull.identity, 'Reviewing tests');
|
||||
const restored = createProgress({{storage, getLogin:() => login, admit:async () => ({{}}), maxLength:20}});
|
||||
login = 'alexander';
|
||||
const otherAccount = restored.load(issue.identity);
|
||||
login = 'timmy';
|
||||
process.stdout.write(JSON.stringify({{
|
||||
saved,
|
||||
issue:restored.load(issue.identity),
|
||||
pull:restored.load(pull.identity),
|
||||
otherAccount,
|
||||
tooLong:restored.save(issue.identity, 'x'.repeat(21)),
|
||||
keys:[...values.keys()],
|
||||
}}));
|
||||
"""
|
||||
output = run_node(script)
|
||||
assert output["saved"] is True
|
||||
assert output["issue"] == "Investigated latency"
|
||||
assert output["pull"] == "Reviewing tests"
|
||||
assert output["otherAccount"] == ""
|
||||
assert output["tooLong"] is False
|
||||
assert output["keys"] == ["stackchain.today-progress.v1.timmy"]
|
||||
|
||||
|
||||
def test_post_admits_exact_comment_once_and_clears_only_after_durable_admission():
|
||||
script = f"""
|
||||
const createProgress = require({json.dumps(str(TODAY_PROGRESS))});
|
||||
const values = new Map();
|
||||
const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}};
|
||||
const calls=[]; let fail=true;
|
||||
const progress=createProgress({{
|
||||
storage, getLogin:()=> 'timmy', makeId:()=> 'progress-op-1',
|
||||
admit:async message => {{ calls.push(message); if (fail) throw new Error('offline store unavailable'); return {{background:true,item:{{id:message.operationId}}}}; }},
|
||||
}});
|
||||
const target={{identity:'issue:stackchain/dashboard:12:',kind:'issue',repository:'stackchain/dashboard',number:12}};
|
||||
progress.save(target.identity, 'Shipped the first slice');
|
||||
let error='';
|
||||
try {{ await progress.post(target); }} catch (caught) {{ error=caught.message; }}
|
||||
const retained=progress.load(target.identity);
|
||||
fail=false;
|
||||
const admitted=await progress.post(target);
|
||||
process.stdout.write(JSON.stringify({{error,retained,after:progress.load(target.identity),calls,admitted}}));
|
||||
"""
|
||||
output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})")
|
||||
assert output["error"] == "offline store unavailable"
|
||||
assert output["retained"] == "Shipped the first slice"
|
||||
assert output["after"] == ""
|
||||
assert len(output["calls"]) == 2
|
||||
assert output["calls"][0] == output["calls"][1] == {
|
||||
"kind": "issue-comment",
|
||||
"repository": "stackchain/dashboard",
|
||||
"number": 12,
|
||||
"body": "Shipped the first slice",
|
||||
"operationId": "progress-op-1",
|
||||
}
|
||||
assert output["admitted"]["background"] is True
|
||||
|
||||
|
||||
def test_progress_rejects_inactive_or_unsupported_targets_without_mutation():
|
||||
script = f"""
|
||||
const createProgress = require({json.dumps(str(TODAY_PROGRESS))});
|
||||
const values=new Map(); let admitted=0;
|
||||
const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}};
|
||||
const progress=createProgress({{storage,getLogin:()=> 'timmy',admit:async()=>{{admitted++;}}}});
|
||||
const results=[];
|
||||
for (const target of [null, {{identity:'review:r:1:',kind:'review',repository:'r',number:1}}, {{identity:'issue:r:0:',kind:'issue',repository:'r',number:0}}]) {{
|
||||
try {{ await progress.post(target, 'note'); results.push('accepted'); }} catch (error) {{ results.push(error.message); }}
|
||||
}}
|
||||
process.stdout.write(JSON.stringify({{results,admitted,keys:[...values.keys()]}}));
|
||||
"""
|
||||
output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})")
|
||||
assert output["admitted"] == 0
|
||||
assert output["keys"] == []
|
||||
assert output["results"] == [
|
||||
"An active Today issue or pull request is required.",
|
||||
"An active Today issue or pull request is required.",
|
||||
"An active Today issue or pull request is required.",
|
||||
]
|
||||
|
||||
|
||||
def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware():
|
||||
html = (ROOT / "frontend" / "index.html").read_text()
|
||||
css = (ROOT / "frontend" / "dashboard.css").read_text()
|
||||
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
||||
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
|
||||
|
||||
assert 'data-mobile-today-update' in html
|
||||
assert 'id="today-progress-sheet"' in html
|
||||
assert 'aria-labelledby="today-progress-title"' in html
|
||||
assert 'id="today-progress-body"' in html
|
||||
assert 'maxlength="2000"' in html
|
||||
assert 'id="save-today-progress"' in html
|
||||
assert 'id="post-today-progress"' in html
|
||||
assert '<script src="static/today-progress.js"></script>' in html
|
||||
assert '"static/today-progress.js"' in bundle
|
||||
assert "workSession.target('continue')" in dashboard
|
||||
assert "authoredOutbox.enqueueDurably" in dashboard
|
||||
assert ".today-progress-panel" in css
|
||||
assert "env(safe-area-inset-bottom)" in css
|
||||
assert ".today-progress-actions button" in css and "min-height:44px" in css
|
||||
Loading…
Reference in New Issue
Block a user