mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 22:18:33 +00:00
fix(ws): prevent message list from scrolling on request pane tab switch (#8448)
This commit is contained in:
@@ -39,7 +39,8 @@ export const SingleWSMessage = ({
|
||||
isNew,
|
||||
onNewRendered,
|
||||
isSelected,
|
||||
onSelect
|
||||
onSelect,
|
||||
paneHeight
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
@@ -105,11 +106,16 @@ export const SingleWSMessage = ({
|
||||
const fontSize = get(preferences, 'font.codeFontSize', 14);
|
||||
const lineHeight = fontSize * 1.5;
|
||||
|
||||
const HEADER_ALLOWANCE = 44;
|
||||
const maxEditorHeight = paneHeight
|
||||
? Math.max(160, paneHeight - HEADER_ALLOWANCE)
|
||||
: Math.round((typeof window !== 'undefined' ? window.innerHeight : 900) * 0.6);
|
||||
const editorHeight = useMemo(() => {
|
||||
const lineCount = (content || '').split('\n').length;
|
||||
const lines = lineCount + 1;
|
||||
return `${lines * lineHeight + 10}px`;
|
||||
}, [content, lineHeight]);
|
||||
const contentHeight = lines * lineHeight + 10;
|
||||
return `${Math.min(contentHeight, maxEditorHeight)}px`;
|
||||
}, [content, lineHeight, maxEditorHeight]);
|
||||
|
||||
const onUpdateMessageType = (newMode) => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
@@ -173,8 +179,8 @@ export const SingleWSMessage = ({
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={!isSelected ? 'disabled' : ''}
|
||||
onMouseDownCapture={() => {
|
||||
if (!isSelected) setTimeout(onSelect, 0);
|
||||
onMouseUpCapture={() => {
|
||||
if (!isSelected) onSelect();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -221,10 +227,7 @@ export const SingleWSMessage = ({
|
||||
<span
|
||||
className="message-label"
|
||||
data-testid={`ws-message-label-${index}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggle();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={handleNameClick}
|
||||
>
|
||||
{displayName}
|
||||
@@ -251,7 +254,11 @@ export const SingleWSMessage = ({
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="accordion-body" data-testid={`ws-message-body-${index}`} style={{ height: editorHeight }}>
|
||||
<div
|
||||
className="accordion-body"
|
||||
data-testid={`ws-message-body-${index}`}
|
||||
style={{ height: editorHeight }}
|
||||
>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
@@ -263,6 +270,7 @@ export const SingleWSMessage = ({
|
||||
onSave={onSave}
|
||||
mode={codemirrorMode[displayMode] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
readOnly={isSelected ? false : 'nocursor'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { IconPlus } from '@tabler/icons';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { SingleWSMessage } from './SingleWSMessage/index';
|
||||
|
||||
@@ -14,6 +15,10 @@ const getSelectedIndex = (messages) => {
|
||||
const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
const dispatch = useDispatch();
|
||||
const messagesContainerRef = useRef(null);
|
||||
const [listScrollTop, setListScrollTop] = usePersistedState({
|
||||
key: `ws-list-scroll-${item.uid}`,
|
||||
default: 0
|
||||
});
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const messages = body?.ws || [];
|
||||
|
||||
@@ -27,6 +32,19 @@ const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
const [newMessageUid, setNewMessageUid] = useState(null);
|
||||
const prevMessagesLengthRef = useRef(messages.length);
|
||||
|
||||
// Track the message pane's height so an expanded editor can be capped to fit
|
||||
// inside it. A taller editor would overflow the pane and produce a second
|
||||
// scrollbar (the list) on top of the editor's own scroll.
|
||||
const [paneHeight, setPaneHeight] = useState(0);
|
||||
useEffect(() => {
|
||||
const el = messagesContainerRef.current;
|
||||
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||
const ro = new ResizeObserver(() => setPaneHeight(el.clientHeight));
|
||||
ro.observe(el);
|
||||
setPaneHeight(el.clientHeight);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
const setSelectedIndex = useCallback((index) => {
|
||||
const currentMessages = [...(body?.ws || [])];
|
||||
const updated = currentMessages.map((msg, i) => ({
|
||||
@@ -68,6 +86,11 @@ const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
setNewMessageUid(newMsg.uid);
|
||||
setSelectedIndex(messages.length - 1);
|
||||
}
|
||||
|
||||
if (messagesContainerRef.current) {
|
||||
const container = messagesContainerRef.current;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
prevMessagesLengthRef.current = messages.length;
|
||||
}, [messages.length]);
|
||||
@@ -76,13 +99,54 @@ const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
setNewMessageUid(null);
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to bottom when new message is added
|
||||
// Restore the last scroll position on mount (component remounts on tab switch,
|
||||
// so listScrollTop is read synchronously for this request).
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && messages.length > 0) {
|
||||
const container = messagesContainerRef.current;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
const container = messagesContainerRef.current;
|
||||
if (container) {
|
||||
container.scrollTop = listScrollTop;
|
||||
}
|
||||
}, [messages.length]);
|
||||
}, [item.uid]);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
if (container) {
|
||||
setListScrollTop(container.scrollTop);
|
||||
}
|
||||
}, [setListScrollTop]);
|
||||
|
||||
// Clicking or typing in an editor makes the browser scroll the list to reveal
|
||||
// CodeMirror's cursor, flinging the whole panel. Pin the list's scrollTop for a
|
||||
// few frames so focus/keystrokes can't move it (the editor still scrolls
|
||||
// internally); a real user scroll (wheel/touch) releases the pin.
|
||||
const pinScrollRef = useRef(null);
|
||||
const pinListScroll = useCallback(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
if (!container) return;
|
||||
const top = container.scrollTop;
|
||||
let frames = 0;
|
||||
const pin = () => {
|
||||
const el = messagesContainerRef.current;
|
||||
if (!el || pinScrollRef.current === null) return;
|
||||
if (el.scrollTop !== top) el.scrollTop = top;
|
||||
if (++frames < 8) {
|
||||
pinScrollRef.current = requestAnimationFrame(pin);
|
||||
} else {
|
||||
pinScrollRef.current = null;
|
||||
}
|
||||
};
|
||||
// Cancel any in-flight pin so a fresh gesture re-snapshots the current top.
|
||||
if (pinScrollRef.current !== null) cancelAnimationFrame(pinScrollRef.current);
|
||||
pinScrollRef.current = requestAnimationFrame(pin);
|
||||
}, []);
|
||||
|
||||
// A real user scroll (wheel/touch) releases the pin immediately.
|
||||
const releasePin = useCallback(() => {
|
||||
if (pinScrollRef.current !== null) {
|
||||
cancelAnimationFrame(pinScrollRef.current);
|
||||
pinScrollRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!messages.length) {
|
||||
return (
|
||||
@@ -100,11 +164,19 @@ const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div ref={messagesContainerRef} className="messages-container">
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="messages-container"
|
||||
data-testid="ws-messages-container"
|
||||
onScroll={handleScroll}
|
||||
onMouseDownCapture={pinListScroll}
|
||||
onKeyDownCapture={pinListScroll}
|
||||
onWheel={releasePin}
|
||||
onTouchMove={releasePin}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<SingleWSMessage
|
||||
key={message.uid}
|
||||
id={`ws-message-${message.uid}`}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
@@ -116,6 +188,7 @@ const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
onNewRendered={handleNewMessageRendered}
|
||||
isSelected={selectedIndex === index}
|
||||
onSelect={() => handleSelect(index)}
|
||||
paneHeight={paneHeight}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
452
tests/websockets/fixtures/collection/ws-long-msg.bru
Normal file
452
tests/websockets/fixtures/collection/ws-long-msg.bru
Normal file
@@ -0,0 +1,452 @@
|
||||
meta {
|
||||
name: ws-long-msg
|
||||
type: ws
|
||||
seq: 4
|
||||
}
|
||||
|
||||
ws {
|
||||
url: ws://localhost:8081/ws/echo
|
||||
body: ws
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: long message
|
||||
type: json
|
||||
content: '''
|
||||
{
|
||||
"type": "bulk",
|
||||
"count": 60,
|
||||
"items": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "item-0",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "item-1",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "item-2",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "item-3",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "item-4",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "item-5",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "item-6",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "item-7",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "item-8",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "item-9",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "item-10",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "item-11",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "item-12",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "item-13",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "item-14",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "item-15",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "item-16",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "item-17",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "item-18",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "item-19",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "item-20",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "item-21",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "item-22",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "item-23",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "item-24",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "item-25",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "item-26",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "item-27",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "item-28",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "item-29",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "item-30",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "item-31",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "item-32",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "item-33",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "item-34",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "item-35",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "item-36",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "item-37",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "item-38",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "item-39",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "item-40",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "item-41",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "item-42",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "item-43",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "item-44",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "item-45",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "item-46",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "item-47",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "item-48",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "item-49",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "item-50",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "item-51",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "item-52",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "item-53",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "item-54",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "item-55",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "item-56",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "item-57",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "item-58",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "item-59",
|
||||
"active": false
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: short message
|
||||
type: text
|
||||
content: '''
|
||||
hello world
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 1
|
||||
type: text
|
||||
content: '''
|
||||
filler 1
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 2
|
||||
type: text
|
||||
content: '''
|
||||
filler 2
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 3
|
||||
type: text
|
||||
content: '''
|
||||
filler 3
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 4
|
||||
type: text
|
||||
content: '''
|
||||
filler 4
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 5
|
||||
type: text
|
||||
content: '''
|
||||
filler 5
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 6
|
||||
type: text
|
||||
content: '''
|
||||
filler 6
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 7
|
||||
type: text
|
||||
content: '''
|
||||
filler 7
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 8
|
||||
type: text
|
||||
content: '''
|
||||
filler 8
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 9
|
||||
type: text
|
||||
content: '''
|
||||
filler 9
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 10
|
||||
type: text
|
||||
content: '''
|
||||
filler 10
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 11
|
||||
type: text
|
||||
content: '''
|
||||
filler 11
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 12
|
||||
type: text
|
||||
content: '''
|
||||
filler 12
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 13
|
||||
type: text
|
||||
content: '''
|
||||
filler 13
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 14
|
||||
type: text
|
||||
content: '''
|
||||
filler 14
|
||||
'''
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: filler message 15
|
||||
type: text
|
||||
content: '''
|
||||
filler 15
|
||||
'''
|
||||
}
|
||||
324
tests/websockets/fixtures/collection/ws-scroll-top.bru
Normal file
324
tests/websockets/fixtures/collection/ws-scroll-top.bru
Normal file
@@ -0,0 +1,324 @@
|
||||
meta {
|
||||
name: ws-scroll-top
|
||||
type: ws
|
||||
seq: 5
|
||||
}
|
||||
|
||||
ws {
|
||||
url: ws://localhost:8081/ws/echo
|
||||
body: ws
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:ws {
|
||||
name: message 1
|
||||
type: json
|
||||
content: '''
|
||||
{
|
||||
"type": "bulk",
|
||||
"count": 60,
|
||||
"items": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "item-0",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "item-1",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "item-2",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "item-3",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "item-4",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "item-5",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "item-6",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "item-7",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "item-8",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "item-9",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "item-10",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "item-11",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "item-12",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "item-13",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "item-14",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "item-15",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "item-16",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "item-17",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "item-18",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "item-19",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "item-20",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "item-21",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "item-22",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "item-23",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "item-24",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "item-25",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "item-26",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "item-27",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "item-28",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "item-29",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "item-30",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "item-31",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "item-32",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "item-33",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "item-34",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "item-35",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "item-36",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "item-37",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "item-38",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "item-39",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "item-40",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "item-41",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "item-42",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "item-43",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "item-44",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "item-45",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "item-46",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "item-47",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "item-48",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "item-49",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "item-50",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "item-51",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "item-52",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "item-53",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "item-54",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "item-55",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "item-56",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "item-57",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "item-58",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "item-59",
|
||||
"active": false
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
}
|
||||
42
tests/websockets/message-editor-scroll.spec.ts
Normal file
42
tests/websockets/message-editor-scroll.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect, test } from '../../playwright';
|
||||
import { openRequest, closeAllCollections } from '../utils/page/actions';
|
||||
|
||||
const COLLECTION_NAME = 'collection';
|
||||
const SCROLL_REQ = 'ws-scroll-top';
|
||||
|
||||
test.describe('websocket message editor scroll behaviour', () => {
|
||||
test.afterEach(async ({ pageWithUserData: page }) => {
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('reopening a message restores the scroll position where we left it', async ({ pageWithUserData: page }) => {
|
||||
await openRequest(page, COLLECTION_NAME, SCROLL_REQ);
|
||||
|
||||
const header = page.getByTestId('ws-message-header-0');
|
||||
const body = page.getByTestId('ws-message-body-0');
|
||||
await expect(body).toBeVisible();
|
||||
|
||||
const cmScroll = body.locator('.CodeMirror-scroll');
|
||||
await expect
|
||||
.poll(() => cmScroll.evaluate((el) => el.scrollHeight - el.clientHeight))
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// Scroll down into the body.
|
||||
const target = await cmScroll.evaluate((el) => {
|
||||
el.scrollTop = Math.floor((el.scrollHeight - el.clientHeight) * 0.6);
|
||||
return el.scrollTop;
|
||||
});
|
||||
expect(target).toBeGreaterThan(0);
|
||||
|
||||
// Collapse then reopen.
|
||||
await header.click({ position: { x: 8, y: 12 } });
|
||||
await expect(body).toBeHidden();
|
||||
await header.click({ position: { x: 8, y: 12 } });
|
||||
await expect(body).toBeVisible();
|
||||
|
||||
// Editor should return to where we left it, not to the top.
|
||||
await expect
|
||||
.poll(() => page.getByTestId('ws-message-body-0').locator('.CodeMirror-scroll').evaluate((el) => el.scrollTop))
|
||||
.toBeGreaterThan(target - 60);
|
||||
});
|
||||
});
|
||||
79
tests/websockets/message-scroll.spec.ts
Normal file
79
tests/websockets/message-scroll.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from '../../playwright';
|
||||
import { openRequest, selectRequestPaneTab, closeAllCollections } from '../utils/page/actions';
|
||||
|
||||
const COLLECTION_NAME = 'collection';
|
||||
const LONG_MSG_REQ = 'ws-long-msg';
|
||||
|
||||
test.describe('websocket message list scroll on tab switch', () => {
|
||||
test.afterEach(async ({ pageWithUserData: page }) => {
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('does not scroll the expanded message list when switching request pane tabs', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION_NAME, LONG_MSG_REQ);
|
||||
|
||||
const container = page.getByTestId('ws-messages-container');
|
||||
await expect(container).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('ws-message-body-0')).toBeVisible();
|
||||
|
||||
// the container must actually be scrollable.
|
||||
await expect
|
||||
.poll(() => container.evaluate((el) => el.scrollHeight - el.clientHeight))
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// Start from the top of the list.
|
||||
await container.evaluate((el) => {
|
||||
el.scrollTop = 0;
|
||||
});
|
||||
await expect.poll(() => container.evaluate((el) => el.scrollTop)).toBe(0);
|
||||
|
||||
// Switch away from the Message tab and back — this remounts WsBody.
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
await expect(container).toBeHidden();
|
||||
await selectRequestPaneTab(page, 'Message');
|
||||
|
||||
await expect(container).toBeVisible();
|
||||
await expect(page.getByTestId('ws-message-body-0')).toBeVisible();
|
||||
|
||||
// The scroll position must not have jumped to the bottom on remount.
|
||||
await expect.poll(() => container.evaluate((el) => el.scrollTop)).toBe(0);
|
||||
});
|
||||
|
||||
test('restores the scroll position when switching back to the Message tab', async ({
|
||||
pageWithUserData: page
|
||||
}) => {
|
||||
await openRequest(page, COLLECTION_NAME, LONG_MSG_REQ);
|
||||
|
||||
const container = page.getByTestId('ws-messages-container');
|
||||
await expect(container).toBeVisible();
|
||||
await expect(page.getByTestId('ws-message-body-0')).toBeVisible();
|
||||
|
||||
// The container must actually be scrollable.
|
||||
await expect
|
||||
.poll(() => container.evaluate((el) => el.scrollHeight - el.clientHeight))
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// Scroll to a specific position partway down the list.
|
||||
const targetScroll = await container.evaluate((el) => {
|
||||
const target = Math.floor((el.scrollHeight - el.clientHeight) / 2);
|
||||
el.scrollTop = target;
|
||||
return el.scrollTop;
|
||||
});
|
||||
expect(targetScroll).toBeGreaterThan(0);
|
||||
await expect.poll(() => container.evaluate((el) => el.scrollTop)).toBe(targetScroll);
|
||||
|
||||
// Switch away from the Message tab and back — this remounts WsBody.
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
await expect(container).toBeHidden();
|
||||
await selectRequestPaneTab(page, 'Message');
|
||||
|
||||
await expect(container).toBeVisible();
|
||||
await expect(page.getByTestId('ws-message-body-0')).toBeVisible();
|
||||
|
||||
// The scroll position must be restored to where we left off.
|
||||
await expect.poll(() => container.evaluate((el) => el.scrollTop)).toBe(targetScroll);
|
||||
});
|
||||
});
|
||||
49
tests/websockets/message-typing-scroll.spec.ts
Normal file
49
tests/websockets/message-typing-scroll.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from '../../playwright';
|
||||
import { openRequest, closeAllCollections } from '../utils/page/actions';
|
||||
|
||||
const COLLECTION_NAME = 'collection';
|
||||
const SCROLL_REQ = 'ws-scroll-top';
|
||||
const MULTI_REQ = 'ws-long-msg';
|
||||
|
||||
test.describe('websocket message editor typing scroll', () => {
|
||||
test.afterEach(async ({ pageWithUserData: page }) => {
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('typing does not jump the list to the end', async ({ pageWithUserData: page }) => {
|
||||
await openRequest(page, COLLECTION_NAME, SCROLL_REQ);
|
||||
|
||||
const container = page.getByTestId('ws-messages-container');
|
||||
const body = page.getByTestId('ws-message-body-0');
|
||||
await expect(container).toBeVisible();
|
||||
await expect(body).toBeVisible();
|
||||
|
||||
const cmScroll = body.locator('.CodeMirror-scroll');
|
||||
await expect
|
||||
.poll(() => cmScroll.evaluate((el) => el.scrollHeight - el.clientHeight))
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// Scroll into the middle of the message body and place the cursor there.
|
||||
await cmScroll.evaluate((el) => {
|
||||
el.scrollTop = Math.floor((el.scrollHeight - el.clientHeight) / 2);
|
||||
});
|
||||
await body.locator('.CodeMirror').click();
|
||||
|
||||
const containerBefore = await container.evaluate((el) => el.scrollTop);
|
||||
const cmBefore = await cmScroll.evaluate((el) => el.scrollTop);
|
||||
|
||||
await page.keyboard.type('x');
|
||||
|
||||
const containerAfter = await container.evaluate((el) => el.scrollTop);
|
||||
const cmAfter = await cmScroll.evaluate((el) => el.scrollTop);
|
||||
|
||||
const listMoved = Math.abs(containerAfter - containerBefore);
|
||||
const editorMoved = Math.abs(cmAfter - cmBefore);
|
||||
|
||||
// Typing on the same, already-visible line must not scroll anything.
|
||||
// The bug flung the list (and would fling the editor) to the very end;
|
||||
// healthy behaviour is zero movement (≤ 1px allows for fractional scrollTop).
|
||||
expect(listMoved).toBeLessThanOrEqual(1);
|
||||
expect(editorMoved).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user