fix(ws): prevent message list from scrolling on request pane tab switch (#8448)

This commit is contained in:
Pooja
2026-07-07 20:42:34 +05:30
committed by GitHub
parent 707489ef77
commit 958ca49156
7 changed files with 1044 additions and 17 deletions

View 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
'''
}

View 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
}
]
}
'''
}

View 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);
});
});

View 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);
});
});

View 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);
});
});