feat: dictate active Today progress updates (Closes #1058)
This commit is contained in:
1 parent
daf34d7cdd
commit
688eadf17a
8 files changed
+218
-31
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user