feat: dictate active Today progress updates (Closes #1058)
CI / lint (pull_request) Successful in 2m29s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m1s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 03:05:53 +00:00
1 parent daf34d7cdd
commit 688eadf17a
8 files changed
+218 -31

No files matched your search

+54
View File
@@ -201,6 +201,43 @@ process.stdout.write(JSON.stringify({{results,admitted,keys:[...values.keys()]}}
]
def test_progress_sheet_scopes_voice_to_the_open_item_and_aborts_on_close():
script = f"""
const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))});
class Element {{
constructor() {{ this.hidden=false;this.value='';this.textContent='';this.open=false;this.listeners={{}}; }}
addEventListener(name,callback) {{ this.listeners[name]=callback; }}
click() {{ return this.listeners.click?.(); }}
showModal() {{ this.open=true; }} close() {{ this.open=false; }} focus() {{}}
}}
const selectors=['#today-progress-sheet','#today-progress-body','#today-progress-status','[data-mobile-today-update]',
'#today-progress-target','#cancel-today-progress','#save-today-progress','#post-today-progress'];
const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()]));
const target={{identity:'issue:stackchain/dashboard:1058:',kind:'issue',repository:'stackchain/dashboard',number:1058,label:'#1058',title:'Voice progress'}};
const calls=[];
const voice={{open:async identity=>calls.push(['open',identity]),cancel:()=>calls.push(['cancel'])}};
const view=createView({{
progress:{{load:()=> 'Saved text',save:()=>true}},currentTarget:()=>target,qs:selector=>elements[selector],voice,
photos:{{open:async()=>calls.push(['photos'])}},
}});
(async()=>{{
await elements['[data-mobile-today-update]'].click();
const opened={{body:elements['#today-progress-body'].value,sheet:elements['#today-progress-sheet'].open,calls:[...calls]}};
await elements['#cancel-today-progress'].click();
process.stdout.write(JSON.stringify({{opened,closed:!elements['#today-progress-sheet'].open,calls}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
assert run_node(script) == {
"opened": {
"body": "Saved text",
"sheet": True,
"calls": [["open", "issue:stackchain/dashboard:1058:"], ["photos"]],
},
"closed": True,
"calls": [["open", "issue:stackchain/dashboard:1058:"], ["photos"], ["cancel"]],
}
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()
@@ -211,6 +248,20 @@ def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware():
assert 'id="today-progress-sheet"' in html
assert 'aria-labelledby="today-progress-title"' in html
assert 'id="today-progress-body"' in html
for control in (
"voice-today-progress",
"start-voice-today-progress",
"stop-voice-today-progress",
"voice-today-progress-review",
"voice-today-progress-transcript",
"append-voice-today-progress",
"replace-with-voice-today-progress",
"discard-voice-today-progress",
"voice-today-progress-status",
):
assert f'id="{control}"' in html
assert 'for="voice-today-progress-transcript"' in html
assert 'data-draft-label="progress update"' in html
assert 'id="take-today-progress-photo"' in html
assert 'id="today-progress-attachment"' in html
assert 'id="today-progress-attachment-preview"' in html
@@ -225,6 +276,8 @@ def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware():
assert "scope:'today-progress'" in TODAY_PROGRESS.read_text()
assert "lanes:{ today:" in TODAY_PROGRESS.read_text()
assert "photos:todayProgressPhotos" in dashboard
assert "['today-progress', '#today-progress-body']" in dashboard
assert "voice:todayProgressVoice" in dashboard
assert "const controller = issueAttachment.mount" in TODAY_PROGRESS.read_text()
assert "const checkpointAttachments = await photos?.serialize?.() || [];" in TODAY_PROGRESS.read_text()
assert "progress.save(openedTarget.identity, body.value, checkpointAttachments)" in TODAY_PROGRESS.read_text()
@@ -232,3 +285,4 @@ def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware():
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
assert ".today-progress-panel .voice-conversation-controls button" in css
+99 -4
View File
@@ -78,6 +78,101 @@ const store={{
}
def test_today_progress_dictation_uses_its_composer_boundary_and_copy():
script = f"""
const {{mountVoiceConversation}} = require({json.dumps(str(VOICE_CAPTURE))});
const requested=[]; const nodes=new Map();
const qs=selector=>{{requested.push(selector);if(!nodes.has(selector))nodes.set(selector,{{selector}});return nodes.get(selector);}};
const created=[];
const result=mountVoiceConversation({{
kind:'today-progress',draftSelector:'#today-progress-body',qs,Recognition:function Recognition(){{}},
transcriptStore:{{name:'store'}},getLogin:()=> 'timmy',
createCapture:options=>{{created.push(options);return {{supported:true}};}},
}});
process.stdout.write(JSON.stringify({{result,requested,elements:Object.fromEntries(Object.entries(created[0].elements).map(([key,value])=>[key,value.selector]))}}));
"""
assert run_node(script) == {
"result": {"supported": True},
"requested": [
"#voice-today-progress", "#start-voice-today-progress", "#stop-voice-today-progress",
"#voice-today-progress-review", "#voice-today-progress-transcript",
"#append-voice-today-progress", "#replace-with-voice-today-progress",
"#discard-voice-today-progress", "#voice-today-progress-status", "#today-progress-body",
],
"elements": {
"root": "#voice-today-progress", "start": "#start-voice-today-progress",
"stop": "#stop-voice-today-progress", "review": "#voice-today-progress-review",
"transcript": "#voice-today-progress-transcript", "append": "#append-voice-today-progress",
"replace": "#replace-with-voice-today-progress", "discard": "#discard-voice-today-progress",
"status": "#voice-today-progress-status", "draft": "#today-progress-body",
},
}
script = f"""
const {{createVoiceConversationCapture}} = require({json.dumps(str(VOICE_CAPTURE))});
class Element {{
constructor() {{ this.hidden=false;this.value='';this.textContent='';this.listeners={{}};this.events=[]; }}
addEventListener(name,callback) {{ this.listeners[name]=callback; }}
click() {{ this.listeners.click?.(); }} dispatchEvent(event) {{ this.events.push(event.type); }}
}}
let recognition;
class Recognition {{ constructor() {{ recognition=this; }} start() {{}} stop() {{}} abort() {{}} }}
const elements=Object.fromEntries(
['root','start','stop','review','transcript','append','replace','discard','status','draft'].map(key=>[key,new Element()])
);
elements.draft.value='Typed';
elements.draft.maxLength=2000;
elements.root.dataset={{draftLabel:'progress update'}};
const saved=[];
const store={{load:async()=>'',save:async(_login,text)=>saved.push(text),clear:async()=>{{}}}};
(async()=>{{
const controller=createVoiceConversationCapture({{
Recognition,elements,transcriptStore:store,getLogin:()=> 'timmy',createEvent:name=>({{type:name}})
}});
await controller.open('issue:stackchain/dashboard:1058:');
elements.start.click();
recognition.onresult({{results:[Object.assign([{{transcript:'x'.repeat(2500)}}],{{isFinal:true}})]}});
const reviewed={{length:elements.transcript.value.length,status:elements.status.textContent,savedLength:saved[0].length}};
elements.replace.click();
process.stdout.write(JSON.stringify({{reviewed,draftLength:elements.draft.value.length,status:elements.status.textContent}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
assert run_node(script) == {
"reviewed": {
"length": 2000,
"savedLength": 2000,
"status": "Transcript ready. Review it before using it.",
},
"draftLength": 2000,
"status": "Transcript added. Review the progress update before sending.",
}
def test_unsupported_today_dictation_keeps_progress_entry_available_with_truthful_copy():
script = f"""
const {{createVoiceConversationCapture}} = require({json.dumps(str(VOICE_CAPTURE))});
class Element {{
constructor() {{ this.hidden=false;this.value='';this.textContent='';this.listeners={{}}; }}
addEventListener(name,callback) {{ this.listeners[name]=callback; }} dispatchEvent() {{}}
}}
const elements=Object.fromEntries(
['root','start','stop','review','transcript','append','replace','discard','status','draft'].map(key=>[key,new Element()])
);
const controller=createVoiceConversationCapture({{Recognition:null,elements,draftLabel:'progress update'}});
process.stdout.write(JSON.stringify({{
supported:controller.supported,voiceHidden:elements.root.hidden,draftHidden:elements.draft.hidden,
status:elements.status.textContent
}}));
"""
assert run_node(script) == {
"supported": False,
"voiceHidden": True,
"draftHidden": False,
"status": "Voice capture is unavailable; type the progress update instead.",
}
def test_private_store_isolates_conversation_transcripts_by_account_and_target():
script = f"""
const createVoiceTranscriptStore=require({json.dumps(str(VOICE_STORE))});
@@ -173,9 +268,9 @@ def test_all_mobile_conversation_composers_ship_accessible_voice_review_controls
assert f'id="replace-with-voice-{kind}"' in html
assert f'id="discard-voice-{kind}"' in html
assert "mountConversationVoice('issue-comment', '#issue-comment')" in dashboard
assert "mountConversationVoice('pull-comment', '#pull-comment')" in dashboard
assert "mountConversationVoice('update-reply', '#update-reply')" in dashboard
assert "['issue-comment', '#issue-comment']" in dashboard
assert "['pull-comment', '#pull-comment']" in dashboard
assert "['update-reply', '#update-reply']" in dashboard
assert ".voice-conversation-controls button" in css
assert "min-height:44px" in css
assert '"static/voice-conversation-capture.js"' in bundle
@@ -206,7 +301,7 @@ def test_mobile_search_reply_ships_target_scoped_voice_review_and_cancels_on_clo
assert f'id="{control}"' in html
assert 'for="voice-search-reply-transcript"' in html
assert "mountConversationVoice('search-reply', '#search-preview-reply')" in dashboard
assert "['search-reply', '#search-preview-reply']" in dashboard
assert "searchVoiceReply.open(searchConversationVoiceTarget(state.item))" in dashboard
assert "searchVoiceReply.cancel();" in dashboard
assert ".search-preview-reply .voice-conversation-controls button" in css