Skip to content

Commit 50b2c87

Browse files
committed
fix(chat): measure the observer's first delivery like any other
The width can change between the mount-time measure and observe(), so treating the first notification as confirmation of the mount width dropped that change and left the stale height in place.
1 parent 1557324 commit 50b2c87

2 files changed

Lines changed: 40 additions & 18 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.test.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ let editorWidth = 700
3333
let autosizeCalls = 0
3434

3535
/**
36-
* Mirrors the real observer's contract closely enough to test the width guard:
37-
* `observe` delivers an initial notification for the current size (browsers do),
38-
* and {@link resizeTo} delivers subsequent ones.
36+
* Mirrors the real observer's contract closely enough to test the width guard.
37+
* `observe` only registers the target — real deliveries, including the initial
38+
* one browsers send, are asynchronous, so every test drives them explicitly via
39+
* {@link resizeTo} / {@link FakeResizeObserver.deliverAll}. Delivering inside
40+
* `observe` would hide the window between the mount-time measure and the first
41+
* notification, which is exactly where a width change can be missed.
3942
*/
4043
class FakeResizeObserver implements ResizeObserver {
4144
private static instances: FakeResizeObserver[] = []
@@ -49,7 +52,6 @@ class FakeResizeObserver implements ResizeObserver {
4952

5053
observe(target: Element) {
5154
this.targets.push(target)
52-
this.deliver()
5355
}
5456

5557
unobserve(target: Element) {
@@ -114,6 +116,14 @@ function resizeTo(width: number, wrappedHeight: number) {
114116
act(() => FakeResizeObserver.deliverAll())
115117
}
116118

119+
/**
120+
* Delivers the observer's initial notification at the mounted width, putting the
121+
* editor in the steady state a test can then resize away from.
122+
*/
123+
function settle() {
124+
act(() => FakeResizeObserver.deliverAll())
125+
}
126+
117127
describe('PromptEditor autosize', () => {
118128
let originalScrollHeight: PropertyDescriptor | undefined
119129

@@ -157,6 +167,7 @@ describe('PromptEditor autosize', () => {
157167
*/
158168
it('re-measures when the editor width changes so no text falls outside the textarea', () => {
159169
const { textarea, unmount } = mountEditor()
170+
settle()
160171
expect(textarea.style.height).toBe('240px')
161172

162173
resizeTo(340, 500)
@@ -167,6 +178,7 @@ describe('PromptEditor autosize', () => {
167178

168179
it('re-measures again when the editor widens back', () => {
169180
const { textarea, unmount } = mountEditor()
181+
settle()
170182

171183
resizeTo(340, 500)
172184
resizeTo(700, 240)
@@ -175,28 +187,37 @@ describe('PromptEditor autosize', () => {
175187
unmount()
176188
})
177189

190+
/**
191+
* `observe` registers the target, but the first notification arrives a frame
192+
* later. A sidebar or side-panel transition can change the width inside that
193+
* window, so the first delivery must be measured like any other rather than
194+
* trusted to confirm the width the mount-time measure used.
195+
*/
196+
it('re-measures on the first delivery when the width changed before it arrived', () => {
197+
const { textarea, unmount } = mountEditor()
198+
expect(textarea.style.height).toBe('240px')
199+
200+
resizeTo(340, 500)
201+
202+
expect(textarea.style.height).toBe('500px')
203+
unmount()
204+
})
205+
178206
/**
179207
* `autosize` writes the textarea's height, which grows the scroller and
180208
* re-notifies this observer. Re-measuring on an unchanged width would make
181209
* that a feedback loop.
182210
*/
183211
it('ignores resize notifications that do not change the width', () => {
184212
const { textarea, unmount } = mountEditor()
185-
const callsAfterMount = autosizeCalls
213+
settle()
214+
const callsAfterSettle = autosizeCalls
186215

187216
contentHeight = 500
188217
act(() => FakeResizeObserver.deliverAll())
189218

190219
expect(textarea.style.height).toBe('240px')
191-
expect(autosizeCalls).toBe(callsAfterMount)
192-
unmount()
193-
})
194-
195-
/** The observer's initial delivery reports the width the mount measure used. */
196-
it('does not re-measure on the observer’s first delivery', () => {
197-
const { unmount } = mountEditor()
198-
199-
expect(autosizeCalls).toBe(1)
220+
expect(autosizeCalls).toBe(callsAfterSettle)
200221
unmount()
201222
})
202223

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,19 @@ export function PromptEditor({
112112
*
113113
* Only width is compared: `autosize` writes the textarea's height, which grows
114114
* the scroller until its cap and re-notifies this observer, so reacting to
115-
* height would feed itself. The first delivery reports the width the
116-
* mount-time measure already used, so it is recorded without re-measuring.
115+
* height would feed itself. The first delivery is measured like any other —
116+
* the width can change between the mount-time measure and `observe()`, and
117+
* re-measuring an unchanged width only writes the same height back.
117118
*/
118119
useEffect(() => {
119120
const scroller = scrollerRef.current
120121
if (!scroller) return
121122
let lastWidth: number | null = null
122123
const observer = new ResizeObserver(([entry]) => {
123124
const width = entry.contentRect.width
124-
const previousWidth = lastWidth
125+
if (width === lastWidth) return
125126
lastWidth = width
126-
if (previousWidth !== null && previousWidth !== width) autosize()
127+
autosize()
127128
})
128129
observer.observe(scroller)
129130
return () => observer.disconnect()

0 commit comments

Comments
 (0)