diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 7fd86aedc..98593967f 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -236,7 +236,8 @@ export default class CodeEditor extends React.Component { // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098 const nextValue = this.props.value ?? ''; const currentValue = this.editor.getValue(); - if (this.editor.hasFocus?.() && currentValue !== nextValue) { + // Skip updating only when focused and editable; read-only editors (e.g. response viewer) must always show new value + if (this.editor.hasFocus?.() && currentValue !== nextValue && !this.props.readOnly) { this.cachedValue = currentValue; } else { const cursor = this.editor.getCursor(); diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js index d5766f90d..b31c4aa28 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js @@ -179,6 +179,7 @@ const HttpMethodSelector = ({ method = DEFAULT_METHOD, onMethodSelect, showCaret items={menuItems} placement="bottom-start" selectedItemId={selectedItemId} + data-testid="method-selector" > diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index d5d832708..16931c9d2 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -103,7 +103,7 @@ const RequestBodyMode = ({ item, collection }) => { return ( -
+
{ }; return ( - + { + const collectionName = 'response-pane-update-test'; + const requestName = 'Echo Request'; + + test.beforeAll(async ({ page, createTmpDir }) => { + const collectionPath = await createTmpDir('response-pane-collection'); + await createCollection(page, collectionName, collectionPath); + await createRequest(page, requestName, collectionName, { url: echoUrl, method: 'POST' }); + }); + + test.afterAll(async ({ page }) => { + await closeAllCollections(page); + }); + + test('Response pane shows new response after re-send with Cmd+Enter while focused in response', async ({ + page + }) => { + const locators = buildCommonLocators(page); + + await test.step('Open request and set body to {"run": 1}', async () => { + await openRequest(page, collectionName, requestName); + + await selectRequestPaneTab(page, 'Body'); + await locators.request.bodyModeSelector().click(); + await locators.dropdown.item('JSON').click(); + + const bodyCodeMirror = locators.request.bodyEditor().locator('.CodeMirror'); + await bodyCodeMirror.click(); + await page.keyboard.press(selectAllShortcut); + await page.keyboard.type('{"run": 1}'); + await expect(locators.request.bodyEditor()).toContainText('"run": 1', { timeout: 5000 }); + }); + + await test.step('Send first request and verify response contains run: 1', async () => { + await sendRequest(page, 200, 15000); + const runEquals1 = /"run":\s*1/; // "run" with value 1, optional space after colon + await expect(locators.response.previewContainer()).toContainText(runEquals1); + }); + + await test.step('Change body to {"run": 2}', async () => { + await selectRequestPaneTab(page, 'Body'); + const bodyCodeMirror = locators.request.bodyEditor().locator('.CodeMirror'); + await bodyCodeMirror.click(); + await page.keyboard.press(selectAllShortcut); + await page.keyboard.type('{"run": 2}'); + await expect(locators.request.bodyEditor()).toContainText('"run": 2', { timeout: 5000 }); + }); + + await test.step('Click inside response pane (Raw/JSON editor) to give it focus', async () => { + const responseEditor = locators.response.previewContainerCodeMirror(); + await responseEditor.waitFor({ state: 'visible', timeout: 5000 }); + await responseEditor.click(); + }); + + await test.step('Press Cmd+Enter / Ctrl+Enter to re-send request', async () => { + await page.keyboard.press(runShortcut); + await expect(locators.response.statusCode()).toContainText('200', { timeout: 15000 }); + }); + + await test.step('Response pane must show new response (run: 2)', async () => { + const responseBody = locators.response.previewContainer(); + // Must show the new response body: single JSON object with "run": 2 + const runEquals2 = /"run":\s*2/; + await expect(responseBody).toContainText(runEquals2, { timeout: 5000 }); + }); + }); +}); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index 46104f099..141578700 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -89,8 +89,11 @@ const createCollection = async (page, collectionName: string, collectionLocation }); }; +const STANDARD_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT']; + type CreateRequestOptions = { url?: string; + method?: string; inFolder?: boolean; }; @@ -241,7 +244,7 @@ const createRequest = async ( parentName: string, options: CreateRequestOptions = {} ) => { - const { url, inFolder = false } = options; + const { url, method, inFolder = false } = options; const parentType = inFolder ? 'folder' : 'collection'; await test.step(`Create request "${requestName}" in ${parentType} "${parentName}"`, async () => { @@ -258,6 +261,19 @@ const createRequest = async ( await locators.dropdown.item('New Request').click(); await page.getByPlaceholder('Request Name').fill(requestName); + if (method) { + await page.locator('.bruno-modal .method-selector').click(); + const isStandardMethod = STANDARD_HTTP_METHODS.includes(method.toUpperCase()); + if (isStandardMethod) { + await locators.modal.newRequestMethodOption(method).click(); + } else { + await locators.modal.newRequestMethodOption('add-custom').click(); + await page.locator('.bruno-modal .method-selector input').fill(method); + await page.keyboard.press('Enter'); + } + await page.waitForTimeout(200); + } + if (url) { await page.locator('#new-request-url .CodeMirror').click(); await page.keyboard.type(url); diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index c4c827af8..239f2ff49 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -51,7 +51,8 @@ export const buildCommonLocators = (page: Page) => ({ closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button'), card: () => page.locator('.bruno-modal-card'), footer: () => page.locator('.bruno-modal-footer'), - submitButton: () => page.locator('.bruno-modal-footer .submit') + submitButton: () => page.locator('.bruno-modal-footer .submit'), + newRequestMethodOption: (id: string) => page.getByTestId(`method-selector-${id.toLowerCase()}`) }, environment: { selector: () => page.getByTestId('environment-selector-trigger'), @@ -73,7 +74,9 @@ export const buildCommonLocators = (page: Page) => ({ methodDropdown: () => page.getByTestId('request-method-selector'), newRequestUrl: () => page.locator('#new-request-url .CodeMirror'), requestNameInput: () => page.getByPlaceholder('Request Name'), - requestTestId: () => page.getByTestId('request-name') + requestTestId: () => page.getByTestId('request-name'), + bodyModeSelector: () => page.getByTestId('request-body-mode-selector'), + bodyEditor: () => page.getByTestId('request-body-editor') }, tags: { input: () => page.getByTestId('tag-input').getByRole('textbox'), @@ -88,6 +91,7 @@ export const buildCommonLocators = (page: Page) => ({ formatTab: () => page.getByTestId('format-response-tab'), formatTabDropdown: () => page.getByTestId('format-response-tab-dropdown'), previewContainer: () => page.getByTestId('response-preview-container'), + previewContainerCodeMirror: () => page.getByTestId('response-preview-container').locator('.CodeMirror').first(), codeLine: () => page.locator('.response-pane .editor-container .CodeMirror-line'), jsonTreeLine: () => page.locator('.response-pane .object-content') },