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
+2 -3
View File
@@ -4105,7 +4105,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
return typeof isTransparentStream==='function'&&isTransparentStream();
}
function _shouldUseLiveProseFade(){
return _shouldUseStreamFade() || _shouldUseTransparentStreamFade();
return !_streamFadeReduceMotionEnabled() && (_shouldUseStreamFade() || _shouldUseTransparentStreamFade());
}
function _streamFadeSkipNode(node){
if(!node||node.nodeType!==1) return false;
@@ -4382,7 +4382,6 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
_streamFadeDomText=String(next.text||'');
return next.caughtUp;
}
_streamFadeBindCleanup(assistantBody);
if(_smdParser){
_smdEndParser();
assistantBody.textContent='';
@@ -4397,7 +4396,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
_streamFadeDomText='';
}
const delta=String(next.text||'').slice(_streamFadeDomText.length);
_streamFadeAppendText(assistantBody,delta);
if(delta) assistantBody.appendChild(document.createTextNode(delta));
_streamFadeDomText=String(next.text||'');
return next.caughtUp;
}
+85 -3
View File
@@ -169,15 +169,16 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
"_smdNewParser(assistantBody,true)",
"_smdWrite(next.text,true)",
"_sanitizeSmdLinks(assistantBody)",
"_streamFadeBindCleanup(assistantBody)",
"_streamFadeAppendText(assistantBody,delta)",
"assistantBody.appendChild(document.createTextNode(delta))",
"_streamFadeDomText=String(next.text||'')",
"stream-fade-active",
],
)
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")
assert_contains_all(
append_block,
@@ -276,6 +277,7 @@ def test_transparent_anchor_prose_uses_fade_renderer_when_enabled():
assert_contains_all(
predicate_block,
[
"!_streamFadeReduceMotionEnabled()",
"_shouldUseStreamFade()",
"_shouldUseTransparentStreamFade()",
],
@@ -284,6 +286,86 @@ def test_transparent_anchor_prose_uses_fade_renderer_when_enabled():
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():
render_section = slice_between(
MESSAGES_JS,