fix: restrict composer resize fast path to appends
This commit is contained in:
parent
b5b21c180c
commit
a383525afe
2 changed files with 67 additions and 28 deletions
|
|
@ -6733,21 +6733,21 @@ function transcript(){
|
|||
}
|
||||
|
||||
let _composerAutoResizeRaf=0;
|
||||
let _composerLastResizeValueLength=0;
|
||||
let _composerLastResizeValue='';
|
||||
function autoResize(){
|
||||
if(_composerAutoResizeRaf && typeof cancelAnimationFrame==='function'){
|
||||
cancelAnimationFrame(_composerAutoResizeRaf);
|
||||
_composerAutoResizeRaf=0;
|
||||
}
|
||||
const el=$('msg');
|
||||
const _nextValueLength=String(el.value||'').length;
|
||||
const _isGrowing=_nextValueLength>_composerLastResizeValueLength;
|
||||
const _nextValue=String(el.value||'');
|
||||
const _isAppendOnly=_nextValue.length>_composerLastResizeValue.length&&_nextValue.startsWith(_composerLastResizeValue);
|
||||
const _fitsCurrentHeight=el.scrollHeight<=el.offsetHeight;
|
||||
// A one-line append does not need a height round trip. Avoiding the
|
||||
// `auto` → measured-height mutation keeps textarea caret geometry stable for
|
||||
// assistive technology while deletes and genuine multi-line growth still resize.
|
||||
if(_isGrowing&&_fitsCurrentHeight){
|
||||
_composerLastResizeValueLength=_nextValueLength;
|
||||
// Only a direct append that already fits can skip the height round trip.
|
||||
// Replacements (including session and draft restoration) must remeasure so a
|
||||
// previously tall composer can shrink back to its natural height.
|
||||
if(_isAppendOnly&&_fitsCurrentHeight){
|
||||
_composerLastResizeValue=_nextValue;
|
||||
updateSendBtn();
|
||||
return;
|
||||
}
|
||||
|
|
@ -6775,7 +6775,7 @@ function autoResize(){
|
|||
const _prevScrollTop=_msgs?_msgs.scrollTop:0;
|
||||
el.style.height='auto';
|
||||
el.style.height=Math.min(el.scrollHeight,200)+'px';
|
||||
_composerLastResizeValueLength=_nextValueLength;
|
||||
_composerLastResizeValue=_nextValue;
|
||||
if(_msgs&&_msgs.scrollTop!==_prevScrollTop) _msgs.scrollTop=_prevScrollTop;
|
||||
updateSendBtn();
|
||||
// Genuine NET growth (a new row that keeps the composer taller than before)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ def test_single_line_growth_does_not_write_height_in_node_fixture():
|
|||
harness = textwrap.dedent(
|
||||
"""
|
||||
let _composerAutoResizeRaf = 0;
|
||||
let _composerLastResizeValueLength = 0;
|
||||
let _composerLastResizeValue = '';
|
||||
let writes = 0;
|
||||
const msg = {
|
||||
value: 'a',
|
||||
|
|
@ -82,12 +82,51 @@ def test_single_line_growth_does_not_write_height_in_node_fixture():
|
|||
function _repinMessagesAfterComposerResize() { throw new Error('one-line append must not repin transcript'); }
|
||||
%(autoresize)s
|
||||
autoResize();
|
||||
console.log(JSON.stringify({ writes, lastLength: _composerLastResizeValueLength, sendUpdates }));
|
||||
console.log(JSON.stringify({ writes, lastValue: _composerLastResizeValue, sendUpdates }));
|
||||
"""
|
||||
) % {"autoresize": body}
|
||||
proc = subprocess.run([node, "-e", harness], capture_output=True, text=True, timeout=30)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert json.loads(proc.stdout) == {"writes": 0, "lastLength": 1, "sendUpdates": 1}
|
||||
assert json.loads(proc.stdout) == {"writes": 0, "lastValue": "a", "sendUpdates": 1}
|
||||
|
||||
|
||||
def test_session_restore_replaces_tall_composer_with_natural_one_line_height():
|
||||
node = shutil.which("node")
|
||||
if not node: # pragma: no cover
|
||||
pytest.skip("node not available")
|
||||
body = _autoresize_body()
|
||||
harness = textwrap.dedent(
|
||||
"""
|
||||
let _composerAutoResizeRaf = 0;
|
||||
let _composerLastResizeValue = 'a\\nb';
|
||||
let height = 150, writes = 0;
|
||||
const msg = {
|
||||
// Mirrors sessions.js draft restore: a prior multi-line value is
|
||||
// replaced directly, then autoResize() runs without an input event.
|
||||
value: 'a longer one-line restored draft',
|
||||
get offsetHeight() { return height; },
|
||||
get scrollHeight() { return height > 44 ? height : 44; },
|
||||
style: {
|
||||
set height(value) { writes += 1; height = value === 'auto' ? 44 : parseInt(value, 10); },
|
||||
get height() { return height + 'px'; },
|
||||
},
|
||||
};
|
||||
const messages = { scrollTop: 0 };
|
||||
const $ = (id) => id === 'msg' ? msg : id === 'messages' ? messages : null;
|
||||
function updateSendBtn() {}
|
||||
function _repinMessagesAfterComposerResize() {}
|
||||
%(autoresize)s
|
||||
autoResize();
|
||||
console.log(JSON.stringify({ writes, height, lastValue: _composerLastResizeValue }));
|
||||
"""
|
||||
) % {"autoresize": body}
|
||||
proc = subprocess.run([node, "-e", harness], capture_output=True, text=True, timeout=30)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert json.loads(proc.stdout) == {
|
||||
"writes": 2,
|
||||
"height": 44,
|
||||
"lastValue": "a longer one-line restored draft",
|
||||
}
|
||||
|
||||
|
||||
def test_programmatic_one_line_fill_refreshes_primary_button_without_height_writes():
|
||||
|
|
@ -102,7 +141,7 @@ def test_programmatic_one_line_fill_refreshes_primary_button_without_height_writ
|
|||
harness = textwrap.dedent(
|
||||
"""
|
||||
let _composerAutoResizeRaf = 0;
|
||||
let _composerLastResizeValueLength = 0;
|
||||
let _composerLastResizeValue = '';
|
||||
let heightWrites = 0;
|
||||
const classes = new Set();
|
||||
const btn = {
|
||||
|
|
@ -169,7 +208,7 @@ def test_single_line_delete_still_runs_the_height_round_trip_in_node_fixture():
|
|||
harness = textwrap.dedent(
|
||||
"""
|
||||
let _composerAutoResizeRaf = 0;
|
||||
let _composerLastResizeValueLength = 5;
|
||||
let _composerLastResizeValue = 'hello';
|
||||
let writes = 0, height = 100;
|
||||
const msg = {
|
||||
value: 'a',
|
||||
|
|
@ -187,15 +226,15 @@ def test_single_line_delete_still_runs_the_height_round_trip_in_node_fixture():
|
|||
function _repinMessagesAfterComposerResize() {}
|
||||
%(autoresize)s
|
||||
autoResize();
|
||||
console.log(JSON.stringify({ writes, height, lastLength: _composerLastResizeValueLength, sendUpdates }));
|
||||
console.log(JSON.stringify({ writes, height, lastValue: _composerLastResizeValue, sendUpdates }));
|
||||
"""
|
||||
) % {"autoresize": body}
|
||||
proc = subprocess.run([node, "-e", harness], capture_output=True, text=True, timeout=30)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert json.loads(proc.stdout) == {"writes": 2, "height": 44, "lastLength": 1, "sendUpdates": 1}
|
||||
assert json.loads(proc.stdout) == {"writes": 2, "height": 44, "lastValue": "a", "sendUpdates": 1}
|
||||
|
||||
|
||||
def _run_resize_boundary_fixture(*, value: str, last_length: int, offset_height: int, scroll_height: int):
|
||||
def _run_resize_boundary_fixture(*, value: str, previous_value: str, offset_height: int, scroll_height: int):
|
||||
node = shutil.which("node")
|
||||
if not node: # pragma: no cover
|
||||
pytest.skip("node not available")
|
||||
|
|
@ -203,7 +242,7 @@ def _run_resize_boundary_fixture(*, value: str, last_length: int, offset_height:
|
|||
harness = textwrap.dedent(
|
||||
"""
|
||||
let _composerAutoResizeRaf = 0;
|
||||
let _composerLastResizeValueLength = %(last_length)s;
|
||||
let _composerLastResizeValue = %(previous_value)r;
|
||||
let writes = 0, height = %(offset_height)s, repins = 0;
|
||||
const msg = {
|
||||
value: %(value)r,
|
||||
|
|
@ -220,10 +259,10 @@ def _run_resize_boundary_fixture(*, value: str, last_length: int, offset_height:
|
|||
function _repinMessagesAfterComposerResize() { repins += 1; }
|
||||
%(autoresize)s
|
||||
autoResize();
|
||||
console.log(JSON.stringify({ writes, height, lastLength: _composerLastResizeValueLength, repins }));
|
||||
console.log(JSON.stringify({ writes, height, lastValue: _composerLastResizeValue, repins }));
|
||||
"""
|
||||
) % {
|
||||
"last_length": last_length,
|
||||
"previous_value": previous_value,
|
||||
"offset_height": offset_height,
|
||||
"scroll_height": scroll_height,
|
||||
"value": value,
|
||||
|
|
@ -237,21 +276,21 @@ def _run_resize_boundary_fixture(*, value: str, last_length: int, offset_height:
|
|||
def test_newline_growth_keeps_full_resize_and_repin_path():
|
||||
out = _run_resize_boundary_fixture(
|
||||
value="a\nb",
|
||||
last_length=1,
|
||||
previous_value="a",
|
||||
offset_height=44,
|
||||
scroll_height=68,
|
||||
)
|
||||
assert out == {"writes": 2, "height": 68, "lastLength": 3, "repins": 1}
|
||||
assert out == {"writes": 2, "height": 68, "lastValue": "a\nb", "repins": 1}
|
||||
|
||||
|
||||
def test_equal_length_replacement_keeps_full_resize_path():
|
||||
out = _run_resize_boundary_fixture(
|
||||
value="ab",
|
||||
last_length=2,
|
||||
previous_value="cd",
|
||||
offset_height=44,
|
||||
scroll_height=44,
|
||||
)
|
||||
assert out == {"writes": 2, "height": 44, "lastLength": 2, "repins": 0}
|
||||
assert out == {"writes": 2, "height": 44, "lastValue": "ab", "repins": 0}
|
||||
|
||||
|
||||
def _helper_body() -> str:
|
||||
|
|
@ -329,10 +368,10 @@ def test_resize_observer_installed_on_composer():
|
|||
|
||||
def test_single_line_growth_skips_the_height_round_trip():
|
||||
body = _autoresize_body()
|
||||
assert "let _composerLastResizeValueLength=0;" in MESSAGES_JS
|
||||
assert "const _isGrowing=_nextValueLength>_composerLastResizeValueLength;" in body
|
||||
assert "let _composerLastResizeValue='';" in MESSAGES_JS
|
||||
assert "const _isAppendOnly=_nextValue.length>_composerLastResizeValue.length&&_nextValue.startsWith(_composerLastResizeValue);" in body
|
||||
assert "const _fitsCurrentHeight=el.scrollHeight<=el.offsetHeight;" in body
|
||||
assert "if(_isGrowing&&_fitsCurrentHeight){" in body
|
||||
assert "if(_isAppendOnly&&_fitsCurrentHeight){" in body
|
||||
assert "el.style.height='auto'" in body
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -446,7 +485,7 @@ def _run_autoresize(scenario):
|
|||
const $ = (id) => (id === 'msg' ? msgEl : id === 'messages' ? msgsEl : null);
|
||||
let _messageUserUnpinned = %(unpinned)s, _scrollPinned = %(pinned)s;
|
||||
let _composerAutoResizeRaf = 0;
|
||||
let _composerLastResizeValueLength = 0;
|
||||
let _composerLastResizeValue = '';
|
||||
let _repinCalls = 0;
|
||||
function updateSendBtn(){}
|
||||
function _messageBottomDistance(){ return msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue