fix(#5493): honor reduced motion for transparent fade

This commit is contained in:
Rod Boev 2026-07-04 10:07:34 -04:00
parent 45a203b36b
commit 0c1bdce023
2 changed files with 87 additions and 6 deletions

View file

@ -4105,7 +4105,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
return typeof isTransparentStream==='function'&&isTransparentStream(); return typeof isTransparentStream==='function'&&isTransparentStream();
} }
function _shouldUseLiveProseFade(){ function _shouldUseLiveProseFade(){
return _shouldUseStreamFade() || _shouldUseTransparentStreamFade(); return !_streamFadeReduceMotionEnabled() && (_shouldUseStreamFade() || _shouldUseTransparentStreamFade());
} }
function _streamFadeSkipNode(node){ function _streamFadeSkipNode(node){
if(!node||node.nodeType!==1) return false; if(!node||node.nodeType!==1) return false;
@ -4382,7 +4382,6 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
_streamFadeDomText=String(next.text||''); _streamFadeDomText=String(next.text||'');
return next.caughtUp; return next.caughtUp;
} }
_streamFadeBindCleanup(assistantBody);
if(_smdParser){ if(_smdParser){
_smdEndParser(); _smdEndParser();
assistantBody.textContent=''; assistantBody.textContent='';
@ -4397,7 +4396,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
_streamFadeDomText=''; _streamFadeDomText='';
} }
const delta=String(next.text||'').slice(_streamFadeDomText.length); const delta=String(next.text||'').slice(_streamFadeDomText.length);
_streamFadeAppendText(assistantBody,delta); if(delta) assistantBody.appendChild(document.createTextNode(delta));
_streamFadeDomText=String(next.text||''); _streamFadeDomText=String(next.text||'');
return next.caughtUp; return next.caughtUp;
} }

View file

@ -169,15 +169,16 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
"_smdNewParser(assistantBody,true)", "_smdNewParser(assistantBody,true)",
"_smdWrite(next.text,true)", "_smdWrite(next.text,true)",
"_sanitizeSmdLinks(assistantBody)", "_sanitizeSmdLinks(assistantBody)",
"_streamFadeBindCleanup(assistantBody)", "assistantBody.appendChild(document.createTextNode(delta))",
"_streamFadeAppendText(assistantBody,delta)",
"_streamFadeDomText=String(next.text||'')", "_streamFadeDomText=String(next.text||'')",
"stream-fade-active", "stream-fade-active",
], ],
) )
assert render_block.index("_smdWrite(next.text,true)") < render_block.index( assert render_block.index("_smdWrite(next.text,true)") < render_block.index(
"_streamFadeAppendText(assistantBody,delta)" "assistantBody.appendChild(document.createTextNode(delta))"
) )
assert "_streamFadeAppendText(assistantBody,delta)" not in render_block
assert "_streamFadeBindCleanup(assistantBody)" not in render_block
append_block = function_block(MESSAGES_JS, "_streamFadeAppendText") append_block = function_block(MESSAGES_JS, "_streamFadeAppendText")
assert_contains_all( assert_contains_all(
append_block, append_block,
@ -276,6 +277,7 @@ def test_transparent_anchor_prose_uses_fade_renderer_when_enabled():
assert_contains_all( assert_contains_all(
predicate_block, predicate_block,
[ [
"!_streamFadeReduceMotionEnabled()",
"_shouldUseStreamFade()", "_shouldUseStreamFade()",
"_shouldUseTransparentStreamFade()", "_shouldUseTransparentStreamFade()",
], ],
@ -284,6 +286,86 @@ def test_transparent_anchor_prose_uses_fade_renderer_when_enabled():
assert "typeof isTransparentStream==='function'&&isTransparentStream()" in MESSAGES_JS assert "typeof isTransparentStream==='function'&&isTransparentStream()" in MESSAGES_JS
def test_reduced_motion_disables_live_prose_fade_predicate():
script = (
"\n".join(
function_block(MESSAGES_JS, name)
for name in [
"_shouldUseStreamFade",
"_shouldUseTransparentStreamFade",
"_streamFadeReduceMotionEnabled",
"_shouldUseLiveProseFade",
]
)
+ r"""
let _streamFadeReduceMotionMql=null;
let _streamFadeReduceMotion=false;
let _streamFadeReduceMotionOnChange=null;
let transparent=true;
let reduceMotion=true;
global.window={
_fadeTextEffect:true,
matchMedia(){
return {
get matches(){ return reduceMotion; },
addEventListener(){},
removeEventListener(){},
};
},
};
function isTransparentStream(){ return transparent; }
if(_shouldUseLiveProseFade()) throw new Error('reduced motion allowed live prose fade');
_streamFadeReduceMotionMql=null;
reduceMotion=false;
window._fadeTextEffect=false;
if(!_shouldUseLiveProseFade()) throw new Error('transparent stream fade should work when motion is allowed');
_streamFadeReduceMotionMql=null;
transparent=false;
window._fadeTextEffect=true;
if(!_shouldUseLiveProseFade()) throw new Error('regular fade preference should work when motion is allowed');
"""
)
run_node(script)
def test_transparent_stream_hidden_body_appends_plain_text_only():
script = (
function_block(MESSAGES_JS, "_renderStreamingFadeMarkdown")
+ r"""
let _streamFadeDomText='';
let _smdParser=null;
let _smdReconnect=false;
let parserEnded=false;
function _streamFadeNextText(){ return {changed:true,caughtUp:false,text:'alpha beta'}; }
function _shouldUseTransparentStreamFade(){ return true; }
function _smdEndParser(){ parserEnded=true; }
const assistantBody={
textContent:'',
innerHTML:'',
children:[],
classList:{added:[],add(name){ this.added.push(name); }},
appendChild(node){
this.children.push(node);
this.textContent += String(node.textContent || '');
return node;
},
};
global.document={
createTextNode(text){ return {type:'text',textContent:String(text)}; },
};
const caughtUp=_renderStreamingFadeMarkdown('alpha beta');
if(caughtUp) throw new Error('expected fade playout to remain catching up');
if(assistantBody.textContent!=='alpha beta') throw new Error(`wrong hidden text: ${assistantBody.textContent}`);
if(_streamFadeDomText!=='alpha beta') throw new Error(`wrong dom text: ${_streamFadeDomText}`);
if(assistantBody.children.some(node=>node.className==='stream-fade-word is-new')){
throw new Error('hidden body received fade span');
}
if(!assistantBody.classList.added.includes('stream-fade-active')) throw new Error('missing stream fade active marker');
"""
)
run_node(script)
def test_transparent_anchor_prose_receives_revealed_fade_text(): def test_transparent_anchor_prose_receives_revealed_fade_text():
render_section = slice_between( render_section = slice_between(
MESSAGES_JS, MESSAGES_JS,