mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 13:45:52 +00:00
feat(request-pane): restore body tab scroll position on tab switch (#7250)
* feat(request-pane): restore body tab scroll position on tab switch When editing large request bodies (JSON/XML/text/sparql), switching to another tab (params, headers, auth, etc.) and back would reset the CodeMirror editor scroll position to the top. Fix by persisting the scroll position to Redux on editor unmount (via CodeEditor's onScroll prop) and restoring it on mount (via initialScroll), mirroring the existing scroll restoration pattern in QueryResultPreview. * test: add playwright tests for body scroll restoration --------- Co-authored-by: naman-bruno <naman@usebruno.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import find from 'lodash/find';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
|
||||
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
|
||||
@@ -7,6 +8,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateRequestBodyScrollPosition } from 'providers/ReduxStore/slices/tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FileBody from '../FileBody/index';
|
||||
|
||||
@@ -16,6 +18,9 @@ const RequestBody = ({ item, collection }) => {
|
||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -30,6 +35,15 @@ const RequestBody = ({ item, collection }) => {
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onScroll = (editor) => {
|
||||
dispatch(
|
||||
updateRequestBodyScrollPosition({
|
||||
uid: focusedTab.uid,
|
||||
scrollY: editor.doc.scrollTop
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (['json', 'xml', 'text', 'sparql'].includes(bodyMode)) {
|
||||
let codeMirrorMode = {
|
||||
json: 'application/ld+json',
|
||||
@@ -57,6 +71,8 @@ const RequestBody = ({ item, collection }) => {
|
||||
onEdit={onEdit}
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
onScroll={onScroll}
|
||||
initialScroll={focusedTab?.requestBodyScrollPosition || 0}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
enableVariableHighlighting={true}
|
||||
showHintsFor={['variables']}
|
||||
|
||||
@@ -161,6 +161,13 @@ export const tabsSlice = createSlice({
|
||||
tab.responsePaneScrollPosition = action.payload.scrollY;
|
||||
}
|
||||
},
|
||||
updateRequestBodyScrollPosition: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
if (tab) {
|
||||
tab.requestBodyScrollPosition = action.payload.scrollY;
|
||||
}
|
||||
},
|
||||
updateResponseFormat: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
@@ -273,6 +280,7 @@ export const {
|
||||
updateRequestPaneTab,
|
||||
updateResponsePaneTab,
|
||||
updateResponsePaneScrollPosition,
|
||||
updateRequestBodyScrollPosition,
|
||||
updateResponseFormat,
|
||||
updateResponseViewTab,
|
||||
updateScriptPaneTab,
|
||||
|
||||
223
tests/request/body-scroll/body-scroll-restoration.spec.ts
Normal file
223
tests/request/body-scroll/body-scroll-restoration.spec.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { test, expect, Page } from '../../../playwright';
|
||||
import {
|
||||
closeAllCollections,
|
||||
createCollection,
|
||||
createRequest,
|
||||
selectRequestPaneTab,
|
||||
openRequest
|
||||
} from '../../utils/page';
|
||||
|
||||
// Generate a large JSON body that requires scrolling
|
||||
const generateLargeJsonBody = () => JSON.stringify(
|
||||
{
|
||||
users: Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `User ${i + 1}`,
|
||||
email: `user${i + 1}@example.com`,
|
||||
address: {
|
||||
street: `${i + 1} Main Street`,
|
||||
city: 'Test City',
|
||||
zipCode: `${10000 + i}`
|
||||
},
|
||||
metadata: {
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
tags: ['tag1', 'tag2', 'tag3']
|
||||
}
|
||||
}))
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
// Helper to set body content using CodeMirror API
|
||||
const setBodyContent = async (page: Page, content: string) => {
|
||||
const bodyEditor = page.locator('.request-pane .CodeMirror').first();
|
||||
await bodyEditor.evaluate((el, value) => {
|
||||
const cm = (el as any).CodeMirror;
|
||||
if (cm) {
|
||||
cm.setValue(value);
|
||||
}
|
||||
}, content);
|
||||
};
|
||||
|
||||
// Helper to get scroll position
|
||||
const getScrollPosition = async (page: Page): Promise<number> => {
|
||||
const bodyEditor = page.locator('.request-pane .CodeMirror').first();
|
||||
return await bodyEditor.evaluate((el) => {
|
||||
const cm = (el as any).CodeMirror;
|
||||
if (cm && cm.doc) {
|
||||
return cm.doc.scrollTop || 0;
|
||||
}
|
||||
const scrollElement = el.querySelector('.CodeMirror-scroll');
|
||||
return scrollElement ? scrollElement.scrollTop : 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to set scroll position
|
||||
const setScrollPosition = async (page: Page, scrollTop: number) => {
|
||||
const bodyEditor = page.locator('.request-pane .CodeMirror').first();
|
||||
await bodyEditor.evaluate((el, top) => {
|
||||
const cm = (el as any).CodeMirror;
|
||||
if (cm) {
|
||||
cm.scrollTo(null, top);
|
||||
}
|
||||
}, scrollTop);
|
||||
};
|
||||
|
||||
// Helper to select body mode
|
||||
const selectBodyMode = async (page: Page, mode: string) => {
|
||||
await page.locator('.body-mode-selector').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: mode }).click();
|
||||
await page.waitForTimeout(100);
|
||||
};
|
||||
|
||||
test.describe('Request Body Scroll Position Restoration', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('should restore scroll position when switching tabs and back', async ({ page, createTmpDir }) => {
|
||||
const collectionName = 'body-scroll-test';
|
||||
const largeJsonBody = generateLargeJsonBody();
|
||||
|
||||
await test.step('Create collection and request with JSON body', async () => {
|
||||
await createCollection(page, collectionName, await createTmpDir(collectionName));
|
||||
await createRequest(page, 'scroll-test', collectionName, {
|
||||
url: 'https://testbench-sanity.usebruno.com/api/echo/json'
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Navigate to Body tab and set JSON body', async () => {
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await selectBodyMode(page, 'JSON');
|
||||
await setBodyContent(page, largeJsonBody);
|
||||
});
|
||||
|
||||
let initialScrollTop: number;
|
||||
|
||||
await test.step('Scroll down in the body editor', async () => {
|
||||
await setScrollPosition(page, 500);
|
||||
await page.waitForTimeout(200);
|
||||
initialScrollTop = await getScrollPosition(page);
|
||||
expect(initialScrollTop).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('Switch to Headers tab', async () => {
|
||||
await selectRequestPaneTab(page, 'Headers');
|
||||
await page.waitForTimeout(200);
|
||||
});
|
||||
|
||||
await test.step('Switch back to Body tab and verify scroll position', async () => {
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const restoredScrollTop = await getScrollPosition(page);
|
||||
|
||||
// The restored scroll position should be approximately the same
|
||||
// Allow some tolerance for rendering differences
|
||||
expect(restoredScrollTop).toBeGreaterThan(0);
|
||||
expect(Math.abs(restoredScrollTop - initialScrollTop)).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
test('should restore scroll position when switching between requests', async ({ page, createTmpDir }) => {
|
||||
const collectionName = 'body-scroll-multi-request';
|
||||
const largeJsonBody = generateLargeJsonBody();
|
||||
|
||||
await test.step('Create collection with two requests', async () => {
|
||||
await createCollection(page, collectionName, await createTmpDir(collectionName));
|
||||
await createRequest(page, 'request-1', collectionName, {
|
||||
url: 'https://testbench-sanity.usebruno.com/api/echo/json'
|
||||
});
|
||||
await createRequest(page, 'request-2', collectionName, {
|
||||
url: 'https://testbench-sanity.usebruno.com/ping'
|
||||
});
|
||||
});
|
||||
|
||||
let scrollPosition: number;
|
||||
|
||||
await test.step('Open first request, add body, and scroll', async () => {
|
||||
await openRequest(page, collectionName, 'request-1');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await selectBodyMode(page, 'JSON');
|
||||
await setBodyContent(page, largeJsonBody);
|
||||
|
||||
await setScrollPosition(page, 400);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
scrollPosition = await getScrollPosition(page);
|
||||
expect(scrollPosition).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('Switch to second request', async () => {
|
||||
await openRequest(page, collectionName, 'request-2');
|
||||
await page.waitForTimeout(200);
|
||||
});
|
||||
|
||||
await test.step('Switch back to first request and verify scroll position', async () => {
|
||||
await openRequest(page, collectionName, 'request-1');
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const restoredScrollTop = await getScrollPosition(page);
|
||||
|
||||
// Verify scroll position is restored
|
||||
expect(restoredScrollTop).toBeGreaterThan(0);
|
||||
expect(Math.abs(restoredScrollTop - scrollPosition)).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
test('should preserve scroll position for XML body mode', async ({ page, createTmpDir }) => {
|
||||
const collectionName = 'body-scroll-xml';
|
||||
|
||||
// Generate large XML body
|
||||
const largeXmlBody = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
${Array.from({ length: 50 }, (_, i) => ` <item id="${i + 1}">
|
||||
<name>Item ${i + 1}</name>
|
||||
<description>This is a description for item ${i + 1}</description>
|
||||
<metadata>
|
||||
<created>2024-01-01T00:00:00Z</created>
|
||||
<updated>2024-01-01T00:00:00Z</updated>
|
||||
</metadata>
|
||||
</item>`).join('\n')}
|
||||
</root>`;
|
||||
|
||||
await test.step('Create collection and request', async () => {
|
||||
await createCollection(page, collectionName, await createTmpDir(collectionName));
|
||||
await createRequest(page, 'xml-scroll-test', collectionName, {
|
||||
url: 'https://testbench-sanity.usebruno.com/api/echo/xml'
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Set XML body mode and add content', async () => {
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await selectBodyMode(page, 'XML');
|
||||
await setBodyContent(page, largeXmlBody);
|
||||
});
|
||||
|
||||
let xmlScrollPosition: number;
|
||||
|
||||
await test.step('Scroll in XML body and verify restoration', async () => {
|
||||
await setScrollPosition(page, 350);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
xmlScrollPosition = await getScrollPosition(page);
|
||||
expect(xmlScrollPosition).toBeGreaterThan(0);
|
||||
|
||||
// Switch tabs
|
||||
await selectRequestPaneTab(page, 'Params');
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Switch back
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const restoredScrollTop = await getScrollPosition(page);
|
||||
|
||||
expect(restoredScrollTop).toBeGreaterThan(0);
|
||||
expect(Math.abs(restoredScrollTop - xmlScrollPosition)).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user