From 973ca18e00e1fa09c69b6f402e0b6bbbdae4e0e4 Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 6 May 2026 13:59:29 +0530 Subject: [PATCH] Revert "feat: ws multi message (#7719)" (#7921) This reverts commit a305b41c937084949ba8cc45b64546cad430efb6. --- .../RequestPane/WSRequestPane/index.js | 107 +------ .../WsBody/SingleWSMessage/StyledWrapper.js | 118 +++---- .../WsBody/SingleWSMessage/index.js | 286 +++++++---------- .../RequestPane/WsBody/StyledWrapper.js | 34 +- .../components/RequestPane/WsBody/index.js | 143 ++++----- .../ReduxStore/slices/collections/actions.js | 5 +- .../ReduxStore/slices/collections/index.js | 3 +- .../bruno-app/src/utils/collections/index.js | 6 +- packages/bruno-app/src/utils/network/index.js | 12 +- .../src/opencollection/items/websocket.ts | 4 +- .../src/ipc/network/ws-event-handlers.js | 32 +- .../bruno-electron/src/utils/collection.js | 2 - .../yml/items/parseWebsocketRequest.ts | 30 +- .../yml/items/stringifyWebsocketRequest.ts | 28 +- packages/bruno-lang/v2/src/bruToJson.js | 5 +- packages/bruno-lang/v2/src/jsonToBru.js | 5 +- .../bruno-lang/v2/tests/bruToJson.spec.js | 152 +-------- .../src/requests/websocket.ts | 1 - .../fixtures/collection/bruno.json | 5 - .../fixtures/collection/collection.bru | 3 - .../fixtures/collection/ws-multi-msg.bru | 29 -- .../fixtures/collection/ws-single-msg.bru | 21 -- .../init-user-data/preferences.json | 12 - .../multi-message-bru/multi-message.spec.ts | 256 --------------- .../fixtures/collection/opencollection.yml | 6 - .../fixtures/collection/ws-multi-msg.yml | 24 -- .../fixtures/collection/ws-single-msg.yml | 18 -- .../init-user-data/preferences.json | 12 - .../multi-message-yml/multi-message.spec.ts | 294 ------------------ 29 files changed, 302 insertions(+), 1351 deletions(-) delete mode 100644 tests/websockets/multi-message-bru/fixtures/collection/bruno.json delete mode 100644 tests/websockets/multi-message-bru/fixtures/collection/collection.bru delete mode 100644 tests/websockets/multi-message-bru/fixtures/collection/ws-multi-msg.bru delete mode 100644 tests/websockets/multi-message-bru/fixtures/collection/ws-single-msg.bru delete mode 100644 tests/websockets/multi-message-bru/init-user-data/preferences.json delete mode 100644 tests/websockets/multi-message-bru/multi-message.spec.ts delete mode 100644 tests/websockets/multi-message-yml/fixtures/collection/opencollection.yml delete mode 100644 tests/websockets/multi-message-yml/fixtures/collection/ws-multi-msg.yml delete mode 100644 tests/websockets/multi-message-yml/fixtures/collection/ws-single-msg.yml delete mode 100644 tests/websockets/multi-message-yml/init-user-data/preferences.json delete mode 100644 tests/websockets/multi-message-yml/multi-message.spec.ts diff --git a/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js index 432d07c95..66825181a 100644 --- a/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js @@ -2,19 +2,12 @@ import React, { useMemo, useCallback, useRef } from 'react'; import Documentation from 'components/Documentation/index'; import RequestHeaders from 'components/RequestPane/RequestHeaders'; import StatusDot from 'components/StatusDot/index'; -import ActionIcon from 'ui/ActionIcon'; -import ToolHint from 'components/ToolHint/index'; -import { IconPlus, IconWand } from '@tabler/icons'; -import { find, get } from 'lodash'; +import { find } from 'lodash'; import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs'; -import { updateRequestBody } from 'providers/ReduxStore/slices/collections'; import { useDispatch, useSelector } from 'react-redux'; import HeightBoundContainer from 'ui/HeightBoundContainer'; import ResponsiveTabs from 'ui/ResponsiveTabs'; import { getPropertyFromDraftOrRequest } from 'utils/collections/index'; -import { prettifyJsonString, uuid } from 'utils/common/index'; -import xmlFormat from 'xml-formatter'; -import toast from 'react-hot-toast'; import WsBody from '../WsBody/index'; import StyledWrapper from './StyledWrapper'; import WSAuth from './WSAuth'; @@ -31,8 +24,6 @@ const WSRequestPane = ({ item, collection, handleRun }) => { const focusedTab = find(tabs, (t) => t.uid === activeTabUid); const requestPaneTab = focusedTab?.requestPaneTab; - const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body'); - const selectTab = useCallback( (tab) => { dispatch(updateRequestPaneTab({ @@ -43,63 +34,6 @@ const WSRequestPane = ({ item, collection, handleRun }) => { [dispatch, item.uid] ); - const addNewMessage = useCallback(() => { - const currentMessages = Array.isArray(body?.ws) - ? body.ws.map((msg) => ({ ...msg, selected: false })) - : []; - currentMessages.push({ - uid: uuid(), - name: `message ${currentMessages.length + 1}`, - content: '{}', - type: 'json', - selected: true - }); - dispatch(updateRequestBody({ - content: currentMessages, - itemUid: item.uid, - collectionUid: collection.uid - })); - }, [body, dispatch, item.uid, collection.uid]); - - const onPrettifyAll = useCallback(() => { - const currentMessages = [...(body?.ws || [])]; - let changed = false; - - currentMessages.forEach((msg, i) => { - if (msg.type === 'json') { - try { - const pretty = prettifyJsonString(msg.content); - if (pretty !== msg.content) { - currentMessages[i] = { ...msg, content: pretty }; - changed = true; - } - } catch (e) { - // skip invalid json - } - } else if (msg.type === 'xml') { - try { - const pretty = xmlFormat(msg.content, { collapseContent: true }); - if (pretty !== msg.content) { - currentMessages[i] = { ...msg, content: pretty }; - changed = true; - } - } catch (e) { - // skip invalid xml - } - } - }); - - if (changed) { - dispatch(updateRequestBody({ - content: currentMessages, - itemUid: item.uid, - collectionUid: collection.uid - })); - } else { - toast.error('Nothing to prettify'); - } - }, [body, dispatch, item.uid, collection.uid]); - const headers = getPropertyFromDraftOrRequest(item, 'request.headers'); const docs = getPropertyFromDraftOrRequest(item, 'request.docs'); const auth = getPropertyFromDraftOrRequest(item, 'request.auth'); @@ -143,8 +77,9 @@ const WSRequestPane = ({ item, collection, handleRun }) => { ); } @@ -164,41 +99,17 @@ const WSRequestPane = ({ item, collection, handleRun }) => { return
404 | Not found
; } } - }, [requestPaneTab, item, collection, handleRun, addNewMessage]); + }, [requestPaneTab, item, collection, handleRun]); if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) { return
An error occurred!
; } - let rightContent = null; - if (requestPaneTab === 'auth') { - rightContent = ( -
- -
- ); - } else if (requestPaneTab === 'body') { - rightContent = ( -
- - - - - - - - - - -
- ); - } + const rightContent = requestPaneTab === 'auth' ? ( +
+ +
+ ) : null; return ( diff --git a/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js index a8ae692e8..a2922b8a3 100644 --- a/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js @@ -1,88 +1,72 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - border-bottom: 1px solid ${(props) => props.theme.border.border0}; - transition: opacity 0.15s ease; + display: flex; + flex-direction: column; - &.disabled { - opacity: 0.45; + &.single { + height: 100%; + + .editor-container { + height: calc(100% - 32px); + } } - .accordion-header { + &:not(.single) { + min-height: 240px; + margin-bottom: 8px; + + &.last { + margin-bottom: 0; + } + } + + .message-toolbar { display: flex; align-items: center; - justify-content: space-between; - padding: 0.5rem 0; - cursor: pointer; - user-select: none; + justify-content: flex-end; + gap: 4px; + padding: 4px 0px; + padding-top: 0px; + height: 32px; + flex-shrink: 0; - .accordion-left { - display: flex; - align-items: center; - gap: 0.375rem; - flex: 1; - min-width: 0; - color: ${(props) => props.theme.text}; - - .message-label { - font-size: ${(props) => props.theme.font.size.sm}; - cursor: default; - } - - .name-input { - font-size: ${(props) => props.theme.font.size.sm}; - color: inherit; - background: ${(props) => props.theme.background.surface1}; - border: none; - border-radius: 0.375rem; - padding: 0.25rem 0.5rem; - outline: none; - flex: 1; - } + .message-label { + font-size: ${(props) => props.theme.font.size.sm}; + color: ${(props) => props.theme.colors.text.subtext1}; + margin-right: auto; } - .accordion-actions { + .toolbar-actions { display: flex; align-items: center; - gap: 0.125rem; - - .hover-actions { - display: flex; - align-items: center; - gap: 0.125rem; - visibility: hidden; - opacity: 0; - transition: opacity 0.15s ease; - - .hover-action-btn { - display: flex; - align-items: center; - justify-content: center; - width: 1.5rem; - height: 1.5rem; - border-radius: 0.25rem; - color: ${(props) => props.theme.text}; - transition: all 0.15s ease; - - &:hover { - background-color: ${(props) => props.theme.dropdown.hoverBg}; - } - - &.delete:hover { - color: ${(props) => props.theme.colors.text.danger}; - } - } - } + gap: 2px; } - &:hover .hover-actions { - visibility: visible; - opacity: 1; + .toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 4px; + color: ${(props) => props.theme.colors.text.muted}; + transition: all 0.15s ease; + + &:hover { + background-color: ${(props) => props.theme.dropdown.hoverBg}; + color: ${(props) => props.theme.text}; + } + + &.delete:hover { + color: ${(props) => props.theme.colors.text.danger}; + } } } - &:not(.disabled) .accordion-header .message-label { - color: ${(props) => props.theme.primary.text}; + .editor-container { + flex: 1; + min-height: 0; } `; diff --git a/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js b/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js index ebf9e1d04..275665dc0 100644 --- a/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js +++ b/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js @@ -1,117 +1,56 @@ -import { IconTrash, IconSend, IconChevronRight, IconChevronDown } from '@tabler/icons'; +import { IconTrash, IconWand } from '@tabler/icons'; import CodeEditor from 'components/CodeEditor/index'; import ToolHint from 'components/ToolHint/index'; import { get } from 'lodash'; +import invert from 'lodash/invert'; import { updateRequestBody } from 'providers/ReduxStore/slices/collections'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { useTheme } from 'providers/Theme'; -import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { queueWsMessage, isWsConnectionActive, connectWS } from 'utils/network/index'; -import { findCollectionByUid, findEnvironmentInCollection } from 'utils/collections/index'; -import toast from 'react-hot-toast'; +import { autoDetectLang } from 'utils/codemirror/lang-detect'; +import { toastError } from 'utils/common/error'; +import { prettifyJsonString } from 'utils/common/index'; +import xmlFormat from 'xml-formatter'; import WSRequestBodyMode from '../BodyMode/index'; import StyledWrapper from './StyledWrapper'; -const codemirrorMode = { - text: 'application/text', - xml: 'application/xml', - json: 'application/ld+json' +export const TYPE_BY_DECODER = { + base64: 'binary', + json: 'json', + xml: 'xml' }; -// Maps stored type to display mode -const typeToMode = (type) => { - switch (type) { - case 'json': return 'json'; - case 'xml': return 'xml'; - default: return 'text'; - } -}; +export const DECODER_BY_TYPE = invert(TYPE_BY_DECODER); export const SingleWSMessage = ({ message, item, collection, index, + methodType, handleRun, - isExpanded, - onToggle, - isNew, - onNewRendered, - isSelected, - onSelect + canClientSendMultipleMessages, + isLast }) => { const dispatch = useDispatch(); const { displayedTheme } = useTheme(); const preferences = useSelector((state) => state.app.preferences); const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body'); - const collections = useSelector((state) => state.collections.collections); const { name, content, type } = message; - const displayMode = typeToMode(type); - const displayName = name || `message ${index + 1}`; + const [messageFormat, setMessageFormat] = useState(autoDetectLang(content)); - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(displayName); + const onUpdateMessageType = (type) => { + setMessageFormat(type); - // Auto-focus the name input when this is a newly created message - useEffect(() => { - if (isNew) { - setIsEditing(true); - setEditValue(displayName); - onNewRendered(); - } - }, [isNew]); - - const saveName = (value) => { - const trimmed = value.trim() || `message ${index + 1}`; const currentMessages = [...(body.ws || [])]; + currentMessages[index] = { ...currentMessages[index], - name: trimmed + type: DECODER_BY_TYPE[type] }; - dispatch(updateRequestBody({ - content: currentMessages, - itemUid: item.uid, - collectionUid: collection.uid - })); - setIsEditing(false); - }; - const handleNameKeyDown = (e) => { - if (e.key === 'Enter') { - saveName(editValue); - } else if (e.key === 'Escape') { - setEditValue(displayName); - setIsEditing(false); - } - }; - - const handleNameBlur = () => { - saveName(editValue); - }; - - const handleNameClick = useCallback((e) => { - e.stopPropagation(); - setEditValue(displayName); - setIsEditing(true); - }, [displayName, onToggle]); - - const fontSize = get(preferences, 'font.codeFontSize', 14); - const lineHeight = fontSize * 1.5; - - const editorHeight = useMemo(() => { - const lineCount = (content || '').split('\n').length; - const lines = lineCount + 1; - return `${lines * lineHeight + 10}px`; - }, [content, lineHeight]); - - const onUpdateMessageType = (newMode) => { - const currentMessages = [...(body.ws || [])]; - currentMessages[index] = { - ...currentMessages[index], - type: typeToMode(newMode) - }; dispatch(updateRequestBody({ content: currentMessages, itemUid: item.uid, @@ -121,11 +60,13 @@ export const SingleWSMessage = ({ const onEdit = (value) => { const currentMessages = [...(body.ws || [])]; + currentMessages[index] = { - ...currentMessages[index], - name: name || `message ${index + 1}`, + name: name ? name : `message ${index + 1}`, + type: DECODER_BY_TYPE[messageFormat], content: value }; + dispatch(updateRequestBody({ content: currentMessages, itemUid: item.uid, @@ -137,7 +78,9 @@ export const SingleWSMessage = ({ const onDeleteMessage = () => { const currentMessages = [...(body.ws || [])]; + currentMessages.splice(index, 1); + dispatch(updateRequestBody({ content: currentMessages, itemUid: item.uid, @@ -145,112 +88,97 @@ export const SingleWSMessage = ({ })); }; - const onSendMessage = useCallback(async () => { - try { - const col = findCollectionByUid(collections, collection.uid); - const environment = findEnvironmentInCollection(col, col?.activeEnvironmentUid); + let codeType = messageFormat; + if (TYPE_BY_DECODER[type]) { + codeType = TYPE_BY_DECODER[type]; + } - // Auto-connect if not already connected - const connectionStatus = await isWsConnectionActive(item.uid); - if (!connectionStatus.isActive) { - await connectWS(item, col, environment, col?.runtimeVariables, { connectOnly: true }); - } + const codemirrorMode = { + text: 'application/text', + xml: 'application/xml', + json: 'application/ld+json' + }; - const result = await queueWsMessage(item, col, environment, col?.runtimeVariables, index); - if (!result.success) { - toast.error(result.error || 'Failed to send message'); + const onPrettify = () => { + if (codeType === 'json') { + try { + const prettyBodyJson = prettifyJsonString(content); + const currentMessages = [...(body.ws || [])]; + currentMessages[index] = { + ...currentMessages[index], + name: name ? name : `message ${index + 1}`, + content: prettyBodyJson + }; + dispatch(updateRequestBody({ + content: currentMessages, + itemUid: item.uid, + collectionUid: collection.uid + })); + } catch (e) { + toastError(new Error('Unable to prettify. Invalid JSON format.')); } - } catch (err) { - toast.error(err.message || 'Failed to send message'); } - }, [collections]); + + if (codeType === 'xml') { + try { + const prettyBodyXML = xmlFormat(content, { collapseContent: true }); + + const currentMessages = [...(body.ws || [])]; + currentMessages[index] = { + ...currentMessages[index], + name: name ? name : `message ${index + 1}`, + content: prettyBodyXML + }; + + dispatch(updateRequestBody({ + content: currentMessages, + itemUid: item.uid, + collectionUid: collection.uid + })); + } catch (e) { + toastError(new Error('Unable to prettify. Invalid XML format.')); + } + } + }; + + const isSingleMessage = !canClientSendMultipleMessages || body.ws.length === 1; return ( - { - if (!isSelected) setTimeout(onSelect, 0); - }} - > -
{ - if (e.target !== e.currentTarget) return; - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onToggle(); - } - }} - > -
- {isExpanded ? ( - - ) : ( - - )} - {isEditing ? ( - node?.focus()} - className="name-input" - data-testid={`ws-message-name-input-${index}`} - value={editValue} - onChange={(e) => setEditValue(e.target.value)} - onKeyDown={handleNameKeyDown} - onBlur={handleNameBlur} - onClick={(e) => e.stopPropagation()} - /> - ) : ( - { - e.preventDefault(); - onToggle(); - }} - onDoubleClick={handleNameClick} - > - {displayName} - - )} -
-
e.stopPropagation()}> -
- - + + + {index > 0 && ( + + - {(body.ws || []).length > 1 && ( - - - - )} -
- + )}
- {isExpanded && ( -
- -
- )} +
+ +
); }; diff --git a/packages/bruno-app/src/components/RequestPane/WsBody/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/WsBody/StyledWrapper.js index e08f17db7..b0ae614d9 100644 --- a/packages/bruno-app/src/components/RequestPane/WsBody/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/WsBody/StyledWrapper.js @@ -5,10 +5,21 @@ const Wrapper = styled.div` flex-direction: column; width: 100%; height: 100%; + position: relative; .messages-container { flex: 1; - overflow-y: auto; + display: flex; + flex-direction: column; + + &.single { + height: 100%; + } + + &.multi { + overflow-y: auto; + padding-bottom: 48px; + } } .empty-state { @@ -25,20 +36,13 @@ const Wrapper = styled.div` } } - .add-message-link { - display: flex; - align-items: center; - gap: 4px; - font-size: 0.875rem; - color: ${(props) => props.theme.primary.text}; - cursor: pointer; - background: none; - border: none; - padding: 4px 0; - - &:hover { - opacity: 0.8; - } + .add-message-footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 8px; + background: ${(props) => props.theme.bg}; } `; diff --git a/packages/bruno-app/src/components/RequestPane/WsBody/index.js b/packages/bruno-app/src/components/RequestPane/WsBody/index.js index 479ed05d4..67ca5abc8 100644 --- a/packages/bruno-app/src/components/RequestPane/WsBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/WsBody/index.js @@ -1,124 +1,99 @@ import { get } from 'lodash'; import { updateRequestBody } from 'providers/ReduxStore/slices/collections'; import { IconPlus } from '@tabler/icons'; -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; +import Button from 'ui/Button'; import StyledWrapper from './StyledWrapper'; import { SingleWSMessage } from './SingleWSMessage/index'; -const getSelectedIndex = (messages) => { - const idx = messages.findIndex((msg) => msg.selected); - return idx >= 0 ? idx : 0; -}; - -const WSBody = ({ item, collection, handleRun, onAddMessage }) => { +const WSBody = ({ item, collection, handleRun }) => { const dispatch = useDispatch(); const messagesContainerRef = useRef(null); const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body'); - const messages = body?.ws || []; - const selectedIndex = getSelectedIndex(messages); + const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType'); + const canClientSendMultipleMessages = false; - // Expand the selected message by default (falls back to first) - const [expandedUids, setExpandedUids] = useState(() => { - const uid = messages[selectedIndex]?.uid || messages[0]?.uid; - return new Set(uid ? [uid] : []); - }); - const [newMessageUid, setNewMessageUid] = useState(null); - const prevMessagesLengthRef = useRef(messages.length); - - const setSelectedIndex = useCallback((index) => { - const currentMessages = [...(body?.ws || [])]; - const updated = currentMessages.map((msg, i) => ({ - ...msg, - selected: i === index - })); - dispatch(updateRequestBody({ - content: updated, - itemUid: item.uid, - collectionUid: collection.uid - })); - }, [body, dispatch, item.uid, collection.uid]); - - const toggleMessage = useCallback((uid) => { - if (!uid) return; - setExpandedUids((prev) => { - const next = new Set(prev); - if (next.has(uid)) { - next.delete(uid); - } else { - next.add(uid); - } - return next; - }); - }, []); - - const handleSelect = useCallback((index) => { - if (index !== selectedIndex) { - setSelectedIndex(index); - } - }, [selectedIndex, setSelectedIndex]); - - // React to new message being added (messages.length increased) + // Auto-scroll to the latest message when messages are added useEffect(() => { - if (messages.length > prevMessagesLengthRef.current) { - const newMsg = messages[messages.length - 1]; - if (newMsg?.uid) { - setExpandedUids((prev) => new Set(prev).add(newMsg.uid)); - setNewMessageUid(newMsg.uid); - setSelectedIndex(messages.length - 1); - } - } - prevMessagesLengthRef.current = messages.length; - }, [messages.length]); - - const handleNewMessageRendered = useCallback(() => { - setNewMessageUid(null); - }, []); - - // Auto-scroll to bottom when new message is added - useEffect(() => { - if (messagesContainerRef.current && messages.length > 0) { + if (messagesContainerRef.current && body?.ws?.length > 0) { const container = messagesContainerRef.current; container.scrollTop = container.scrollHeight; } - }, [messages.length]); + }, [body?.ws?.length]); - if (!messages.length) { + const addNewMessage = () => { + const currentMessages = Array.isArray(body.ws) ? [...body.ws] : []; + + currentMessages.push({ + name: `message ${currentMessages.length + 1}`, + content: '{}' + }); + + dispatch(updateRequestBody({ + content: currentMessages, + itemUid: item.uid, + collectionUid: collection.uid + })); + }; + + if (!body?.ws || !Array.isArray(body.ws)) { return (

No WebSocket messages available

- +
); } + const messagesToShow = body.ws.filter((_, index) => canClientSendMultipleMessages || index === 0); + return ( -
- {messages.map((message, index) => ( +
1 ? 'multi' : 'single'}`} + > + {messagesToShow.map((message, index) => ( toggleMessage(message.uid)} - isNew={newMessageUid === message.uid} - onNewRendered={handleNewMessageRendered} - isSelected={selectedIndex === index} - onSelect={() => handleSelect(index)} + canClientSendMultipleMessages={canClientSendMultipleMessages} + isLast={index === messagesToShow.length - 1} /> ))}
+ + {canClientSendMultipleMessages && ( +
+ +
+ )} ); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index e33c19202..55b920749 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -577,9 +577,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => { toast.error(err.message); }); } else if (isWsRequest) { - const wsMessages = itemCopy.draft?.request?.body?.ws || itemCopy.request?.body?.ws || []; - const wsSelectedMessageIndex = Math.max(0, wsMessages.findIndex((msg) => msg.selected)); - sendWsRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables, wsSelectedMessageIndex) + sendWsRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables) .then(resolve) .catch((err) => { toast.error(err.message); @@ -1606,7 +1604,6 @@ export const newWsRequest = (params) => (dispatch, getState) => { mode: 'ws', ws: [ { - uid: uuid(), name: 'message 1', type: 'json', content: '{}' diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 23710c326..b389e826c 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -100,8 +100,7 @@ const REQUEST_UID_PATHS = [ 'assertions', 'body.formUrlEncoded', 'body.multipartForm', - 'body.file', - 'body.ws' + 'body.file' ]; const ROOT_UID_PATHS = ['request.headers', 'request.vars.req', 'request.vars.res']; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 0b551b53b..0cc1b508e 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -785,11 +785,10 @@ export const transformRequestToSaveToFilesystem = (item) => { if (itemToSave.request.body.mode === 'ws') { itemToSave.request.body = { ...itemToSave.request.body, - ws: itemToSave.request.body.ws.map(({ name, content, type, selected }, index) => ({ + ws: itemToSave.request.body.ws.map(({ name, content, type }, index) => ({ name: name ? name : `message ${index + 1}`, type, - content: replaceTabsWithSpaces(content), - selected: selected || false + content: replaceTabsWithSpaces(content) })) }; } @@ -1015,7 +1014,6 @@ export const refreshUidsInItem = (item) => { each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); each(get(item, 'request.body.file'), (param) => (param.uid = uuid())); - each(get(item, 'request.body.ws'), (msg) => (msg.uid = uuid())); each(get(item, 'request.assertions'), (assertion) => (assertion.uid = uuid())); return item; diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index 3e9c906bd..b1a265276 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -224,7 +224,7 @@ export const connectWS = async (item, collection, environment, runtimeVariables, }); }; -export const sendWsRequest = async (item, collection, environment, runtimeVariables, selectedMessageIndex = 0) => { +export const sendWsRequest = async (item, collection, environment, runtimeVariables) => { const ensureConnection = async () => { const connectionStatus = await isWsConnectionActive(item.uid); if (!connectionStatus.isActive) { @@ -234,8 +234,8 @@ export const sendWsRequest = async (item, collection, environment, runtimeVariab await ensureConnection(); - // Send only the selected message by index - const result = await queueWsMessage(item, collection, environment, runtimeVariables, selectedMessageIndex); + // Use queueWsMessage helper to queue all messages with proper variable interpolation + const result = await queueWsMessage(item, collection, environment, runtimeVariables, null); if (result.success) { return {}; @@ -250,10 +250,10 @@ export const sendWsRequest = async (item, collection, environment, runtimeVariab * @param {Object} collection - The collection object * @param {Object} environment - The environment variables * @param {Object} runtimeVariables - The runtime variables - * @param {number} selectedMessageIndex - Index of the message to queue + * @param {string} messageContent - The message content to queue (or null to queue all messages) * @returns {Promise} - The result of the queue operation */ -export const queueWsMessage = async (item, collection, environment, runtimeVariables, selectedMessageIndex) => { +export const queueWsMessage = async (item, collection, environment, runtimeVariables, messageContent) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; ipcRenderer.invoke('renderer:ws:queue-message', { @@ -261,7 +261,7 @@ export const queueWsMessage = async (item, collection, environment, runtimeVaria collection, environment, runtimeVariables, - selectedMessageIndex + messageContent }).then(resolve).catch(reject); }); }; diff --git a/packages/bruno-converters/src/opencollection/items/websocket.ts b/packages/bruno-converters/src/opencollection/items/websocket.ts index 15ddef9fe..13055e2ce 100644 --- a/packages/bruno-converters/src/opencollection/items/websocket.ts +++ b/packages/bruno-converters/src/opencollection/items/websocket.ts @@ -45,8 +45,7 @@ export const fromOpenCollectionWebsocketItem = (item: WebSocketRequest): BrunoIt wsMessages.push({ name: m.title || `message ${index + 1}`, type: m.message?.type || 'json', - content: m.message?.data || '', - selected: m.selected || false + content: m.message?.data || '' }); }); } @@ -126,7 +125,6 @@ export const toOpenCollectionWebsocketItem = (item: BrunoItem): WebSocketRequest } else { websocket.message = messages.map((msg): WebSocketMessageVariant => ({ title: msg.name || 'Untitled', - ...(msg.selected ? { selected: true } : {}), message: { type: (msg.type as WebSocketMessage['type']) || 'json', data: msg.content || '' diff --git a/packages/bruno-electron/src/ipc/network/ws-event-handlers.js b/packages/bruno-electron/src/ipc/network/ws-event-handlers.js index b93198383..b9aad047e 100644 --- a/packages/bruno-electron/src/ipc/network/ws-event-handlers.js +++ b/packages/bruno-electron/src/ipc/network/ws-event-handlers.js @@ -400,19 +400,35 @@ const registerWsEventHandlers = (window) => { ipcMain.handle( 'renderer:ws:queue-message', - async (event, { item, collection, environment, runtimeVariables, selectedMessageIndex }) => { + async (event, { item, collection, environment, runtimeVariables, messageContent }) => { try { const itemCopy = cloneDeep(item); const preparedRequest = await prepareWsRequest(itemCopy, collection, environment, runtimeVariables, {}); - const messages = preparedRequest.body?.ws; - if (!messages || !Array.isArray(messages)) { - return { success: true }; - } + // If messageContent is provided, find and queue that specific message (interpolated) + // Otherwise, queue all messages + if (messageContent !== undefined && messageContent !== null) { + // Find the message index in the original request + const originalMessages = itemCopy.draft?.request?.body?.ws || itemCopy.request?.body?.ws || []; + const messageIndex = originalMessages.findIndex((msg) => msg.content === messageContent); - const message = messages[selectedMessageIndex]; - if (message && message.content) { - wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content, message.type); + if (messageIndex >= 0 && preparedRequest.body?.ws?.[messageIndex]) { + // Queue the interpolated version of the specific message + const message = preparedRequest.body.ws[messageIndex]; + wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content, message.type); + } else { + // Message not found in request body, queue as-is (shouldn't happen in normal flow) + wsClient.queueMessage(preparedRequest.uid, collection.uid, messageContent); + } + } else { + // Queue all messages (they are already interpolated by prepareWsRequest -> interpolateVars) + if (preparedRequest.body && preparedRequest.body.ws && Array.isArray(preparedRequest.body.ws)) { + preparedRequest.body.ws + .filter((message) => message && message.content) + .forEach((message) => { + wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content, message.type); + }); + } } return { success: true }; diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 758df6a8d..9c837e01b 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -579,8 +579,6 @@ const hydrateRequestWithUuid = (request, pathname) => { bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); bodyMultipartForm.forEach((param) => (param.uid = uuid())); file.forEach((param) => (param.uid = uuid())); - const wsMessages = get(request, 'request.body.ws', []); - wsMessages.forEach((msg) => (msg.uid = uuid())); examples.forEach((example, eIndex) => { example.uid = getExampleUid(pathname, eIndex); example.itemUid = request.uid; diff --git a/packages/bruno-filestore/src/formats/yml/items/parseWebsocketRequest.ts b/packages/bruno-filestore/src/formats/yml/items/parseWebsocketRequest.ts index 79cceb416..14799ef56 100644 --- a/packages/bruno-filestore/src/formats/yml/items/parseWebsocketRequest.ts +++ b/packages/bruno-filestore/src/formats/yml/items/parseWebsocketRequest.ts @@ -1,6 +1,6 @@ import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item'; import type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket'; -import type { WebSocketRequest, WebSocketMessage, WebSocketMessageVariant } from '@opencollection/types/requests/websocket'; +import type { WebSocketRequest, WebSocketMessage } from '@opencollection/types/requests/websocket'; import { toBrunoAuth } from '../common/auth'; import { toBrunoHttpHeaders } from '../common/headers'; import { toBrunoVariables } from '../common/variables'; @@ -35,26 +35,14 @@ const parseWebsocketRequest = (ocRequest: WebSocketRequest): BrunoItem => { // message if (websocket?.message) { - if (Array.isArray(websocket.message)) { - // multiple messages: WebSocketMessageVariant[] - const variants = websocket.message as WebSocketMessageVariant[]; - brunoRequest.body.ws = variants.map((variant, index) => ({ - name: variant.title || `message ${index + 1}`, - type: variant.message?.type || 'text', - content: ensureString(variant.message?.data), - selected: variant.selected || false - })); - } else { - // single message uses flat WebSocketMessage - const message = websocket.message as WebSocketMessage; - const messageData = ensureString(message.data); - if (messageData.trim().length) { - brunoRequest.body.ws = [{ - name: '', - type: message.type || 'text', - content: messageData - }]; - } + const message = websocket.message as WebSocketMessage; + const messageData = ensureString(message.data); + if (messageData.trim().length) { + brunoRequest.body.ws = [{ + name: '', + type: message.type || 'text', + content: messageData + }]; } } diff --git a/packages/bruno-filestore/src/formats/yml/items/stringifyWebsocketRequest.ts b/packages/bruno-filestore/src/formats/yml/items/stringifyWebsocketRequest.ts index 0d3ead549..6a3d00fe7 100644 --- a/packages/bruno-filestore/src/formats/yml/items/stringifyWebsocketRequest.ts +++ b/packages/bruno-filestore/src/formats/yml/items/stringifyWebsocketRequest.ts @@ -1,6 +1,6 @@ import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item'; import type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket'; -import type { WebSocketRequest, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime, WebSocketMessage, WebSocketMessageVariant } from '@opencollection/types/requests/websocket'; +import type { WebSocketRequest, WebSocketMessage, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime } from '@opencollection/types/requests/websocket'; import type { Auth } from '@opencollection/types/common/auth'; import type { Scripts } from '@opencollection/types/common/scripts'; import type { Variable } from '@opencollection/types/common/variables'; @@ -41,31 +41,21 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => { websocket.headers = headers; } - // message: single message without a custom name uses flat WebSocketMessage (backward compatible), - // otherwise uses WebSocketMessageVariant[] to preserve names + // message if (brunoRequest.body?.mode === 'ws' && brunoRequest.body.ws?.length) { const messages = brunoRequest.body.ws; - const hasCustomName = messages.length === 1 && messages[0].name && messages[0].name.trim().length > 0; - const hasContent = messages.length === 1 && (messages[0].content || '').trim().length > 0; - - if (messages.length === 1 && !hasCustomName && hasContent) { + // todo: bruno app supports only one message for now + // update this when bruno app supports multiple messages + if (messages.length) { const msg = messages[0]; const message: WebSocketMessage = { - type: (msg.type as WebSocketMessage['type']) || 'text', + type: (msg.type as 'text' | 'json' | 'xml' | 'binary') || 'text', data: msg.content || '' }; - websocket.message = message; - } else { - const variants: WebSocketMessageVariant[] = messages.map((msg, index) => ({ - title: msg.name || `message ${index + 1}`, - selected: msg.selected || false, - message: { - type: (msg.type as WebSocketMessage['type']) || 'text', - data: msg.content || '' - } - })); - websocket.message = variants; + if (message.data.trim().length) { + websocket.message = message; + } } } diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index a07e09ca5..ecab5eee6 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -1159,12 +1159,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { const namePair = _.find(pairs, { name: 'name' }); const contentPair = _.find(pairs, { name: 'content' }); const typePair = _.find(pairs, { name: 'type' }); - const selectedPair = _.find(pairs, { name: 'selected' }); const messageName = namePair ? namePair.value : ''; const messageContent = contentPair ? contentPair.value : ''; const messageTypeContent = typePair ? typePair.value : ''; - const messageSelected = selectedPair ? selectedPair.value === 'true' : false; return { body: { @@ -1173,8 +1171,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { { name: messageName, type: messageTypeContent, - content: messageContent, - selected: messageSelected + content: messageContent } ] } diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index de35e01d1..0cce61d1f 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -634,7 +634,7 @@ ${indentString(body.sparql)} // Convert each ws message to a separate body:ws block if (Array.isArray(body.ws)) { body.ws.forEach((message) => { - const { name, content, type = '', selected } = message; + const { name, content, type = '' } = message; bru += `body:ws {\n`; @@ -642,9 +642,6 @@ ${indentString(body.sparql)} if (type.length) { bru += `${indentString(`type: ${getValueString(type)}`)}\n`; } - if (selected) { - bru += `${indentString(`selected: true`)}\n`; - } // Convert content to JSON string if it's an object let contentValue = typeof content === 'object' ? JSON.stringify(content, null, 2) : content || '{}'; diff --git a/packages/bruno-lang/v2/tests/bruToJson.spec.js b/packages/bruno-lang/v2/tests/bruToJson.spec.js index c018289da..b9b27a685 100644 --- a/packages/bruno-lang/v2/tests/bruToJson.spec.js +++ b/packages/bruno-lang/v2/tests/bruToJson.spec.js @@ -9,7 +9,7 @@ body:ws { name: message 1 content: ''' {"foo":"bar"} - ''' + ''' } settings { @@ -24,8 +24,7 @@ settings { { content: '{"foo":"bar"}', name: 'message 1', - type: 'json', - selected: false + type: 'json' } ] }, @@ -38,153 +37,6 @@ settings { const output = parser(input); expect(output).toEqual(expected); }); - - it('parses a single message flagged with selected: true', () => { - const input = ` -body:ws { - type: json - name: message 1 - selected: true - content: ''' - {"foo":"bar"} - ''' -} -`; - - const expected = { - body: { - mode: 'ws', - ws: [ - { - content: '{"foo":"bar"}', - name: 'message 1', - type: 'json', - selected: true - } - ] - } - }; - - const output = parser(input); - expect(output).toEqual(expected); - }); - - it('parses multiple messages with none marked as selected', () => { - const input = ` -body:ws { - name: message 1 - type: json - content: ''' - {"action":"subscribe"} - ''' -} - -body:ws { - name: message 2 - type: text - content: ''' - hello world - ''' -} -`; - - const expected = { - body: { - mode: 'ws', - ws: [ - { - name: 'message 1', - type: 'json', - content: '{"action":"subscribe"}', - selected: false - }, - { - name: 'message 2', - type: 'text', - content: 'hello world', - selected: false - } - ] - } - }; - - const output = parser(input); - expect(output).toEqual(expected); - }); - - it('parses multiple messages with exactly one marked as selected', () => { - const input = ` -body:ws { - name: message 1 - type: json - content: ''' - {"action":"subscribe"} - ''' -} - -body:ws { - name: message 2 - type: text - selected: true - content: ''' - hello world - ''' -} - -body:ws { - name: message 3 - type: xml - content: ''' - - ''' -} -`; - - const expected = { - body: { - mode: 'ws', - ws: [ - { - name: 'message 1', - type: 'json', - content: '{"action":"subscribe"}', - selected: false - }, - { - name: 'message 2', - type: 'text', - content: 'hello world', - selected: true - }, - { - name: 'message 3', - type: 'xml', - content: '', - selected: false - } - ] - } - }; - - const output = parser(input); - expect(output).toEqual(expected); - }); - - it('treats selected: false as not selected', () => { - const input = ` -body:ws { - name: message 1 - type: text - selected: false - content: ''' - hello - ''' -} -`; - - const output = parser(input); - expect(output.body.ws[0].selected).toBe(false); - }); }); describe('body:grpc', () => { diff --git a/packages/bruno-schema-types/src/requests/websocket.ts b/packages/bruno-schema-types/src/requests/websocket.ts index ce928f341..1781ebfc8 100644 --- a/packages/bruno-schema-types/src/requests/websocket.ts +++ b/packages/bruno-schema-types/src/requests/websocket.ts @@ -4,7 +4,6 @@ export interface WebSocketMessage { name?: string | null; type?: string | null; content?: string | null; - selected?: boolean | null; } export interface WebSocketRequestBody { diff --git a/tests/websockets/multi-message-bru/fixtures/collection/bruno.json b/tests/websockets/multi-message-bru/fixtures/collection/bruno.json deleted file mode 100644 index 95b292bea..000000000 --- a/tests/websockets/multi-message-bru/fixtures/collection/bruno.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "1", - "name": "ws-multi-message", - "type": "collection" -} \ No newline at end of file diff --git a/tests/websockets/multi-message-bru/fixtures/collection/collection.bru b/tests/websockets/multi-message-bru/fixtures/collection/collection.bru deleted file mode 100644 index 2492c42b1..000000000 --- a/tests/websockets/multi-message-bru/fixtures/collection/collection.bru +++ /dev/null @@ -1,3 +0,0 @@ -vars:pre-request { - variable: Variable Value -} diff --git a/tests/websockets/multi-message-bru/fixtures/collection/ws-multi-msg.bru b/tests/websockets/multi-message-bru/fixtures/collection/ws-multi-msg.bru deleted file mode 100644 index cada227df..000000000 --- a/tests/websockets/multi-message-bru/fixtures/collection/ws-multi-msg.bru +++ /dev/null @@ -1,29 +0,0 @@ -meta { - name: ws-multi-msg - type: ws - seq: 1 -} - -ws { - url: ws://localhost:8081/ws/echo - body: ws - auth: inherit -} - -body:ws { - name: message 1 - type: json - content: ''' - { - "action": "subscribe" - } - ''' -} - -body:ws { - name: message 2 - type: text - content: ''' - hello world - ''' -} diff --git a/tests/websockets/multi-message-bru/fixtures/collection/ws-single-msg.bru b/tests/websockets/multi-message-bru/fixtures/collection/ws-single-msg.bru deleted file mode 100644 index 297016c04..000000000 --- a/tests/websockets/multi-message-bru/fixtures/collection/ws-single-msg.bru +++ /dev/null @@ -1,21 +0,0 @@ -meta { - name: ws-single-msg - type: ws - seq: 2 -} - -ws { - url: ws://localhost:8081/ws/echo - body: ws - auth: inherit -} - -body:ws { - name: message 1 - type: json - content: ''' - { - "foo": "bar" - } - ''' -} diff --git a/tests/websockets/multi-message-bru/init-user-data/preferences.json b/tests/websockets/multi-message-bru/init-user-data/preferences.json deleted file mode 100644 index 73d96b809..000000000 --- a/tests/websockets/multi-message-bru/init-user-data/preferences.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "maximized": false, - "lastOpenedCollections": [ - "{{projectRoot}}/tests/websockets/multi-message-bru/fixtures/collection" - ], - "preferences": { - "onboarding": { - "hasLaunchedBefore": true, - "hasSeenWelcomeModal": true - } - } -} \ No newline at end of file diff --git a/tests/websockets/multi-message-bru/multi-message.spec.ts b/tests/websockets/multi-message-bru/multi-message.spec.ts deleted file mode 100644 index fbc41d983..000000000 --- a/tests/websockets/multi-message-bru/multi-message.spec.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { expect, test } from '../../../playwright'; -import { buildWebsocketCommonLocators } from '../../utils/page/locators'; -import { openRequest, saveRequest, closeAllCollections } from '../../utils/page/actions'; -import { readFile, writeFile } from 'fs/promises'; -import { join } from 'path'; - -const COLLECTION_NAME = 'ws-multi-message'; -const MULTI_MSG_REQ = 'ws-multi-msg'; -const SINGLE_MSG_REQ = 'ws-single-msg'; -const MULTI_MSG_BRU_PATH = join(__dirname, 'fixtures/collection/ws-multi-msg.bru'); -const SINGLE_MSG_BRU_PATH = join(__dirname, 'fixtures/collection/ws-single-msg.bru'); -const MAX_CONNECTION_TIME = 3000; - -test.describe('websocket multi-message (bru format)', () => { - let originalMultiMsgData = ''; - let originalSingleMsgData = ''; - - test.beforeAll(async () => { - originalMultiMsgData = await readFile(MULTI_MSG_BRU_PATH, 'utf8'); - originalSingleMsgData = await readFile(SINGLE_MSG_BRU_PATH, 'utf8'); - }); - - test.afterEach(async () => { - await writeFile(MULTI_MSG_BRU_PATH, originalMultiMsgData, 'utf8'); - await writeFile(SINGLE_MSG_BRU_PATH, originalSingleMsgData, 'utf8'); - }); - - test.afterAll(async ({ pageWithUserData: page }) => { - await closeAllCollections(page); - }); - - test('add a new message and save', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await page.getByTestId('ws-add-message').click(); - - const nameInput = page.getByTestId(/^ws-message-name-input-/); - await expect(nameInput).toBeVisible(); - - await nameInput.selectText(); - await page.keyboard.type('ping message'); - await nameInput.press('Enter'); - - await expect(page.getByTestId(/^ws-message-label-/).filter({ hasText: 'ping message' })).toBeVisible(); - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(3); - - await saveRequest(page); - - const bruContent = await readFile(MULTI_MSG_BRU_PATH, 'utf8'); - expect(bruContent).toContain('name: ping message'); - }); - - test('edit message content and verify persistence', async ({ pageWithUserData: page }) => { - const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a'; - - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - // Expand the first message if not already expanded - const editorBody = page.getByTestId('ws-message-body-0'); - if (!(await editorBody.isVisible())) { - await page.getByTestId('ws-message-header-0').click(); - } - const editor = editorBody.locator('.CodeMirror'); - await editor.click(); - const textarea = editor.locator('textarea'); - await textarea.focus(); - await page.keyboard.press(selectAllShortcut); - await page.keyboard.insertText('{"updated": "content"}'); - - await saveRequest(page); - - const bruContent = await readFile(SINGLE_MSG_BRU_PATH, 'utf8'); - expect(bruContent).toContain('{"updated": "content"}'); - }); - - test('messages with different types persist correctly', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - const firstHeader = page.getByTestId('ws-message-header-0'); - await expect(firstHeader.locator('.selected-body-mode')).toContainText('JSON'); - - const secondHeader = page.getByTestId('ws-message-header-1'); - await expect(secondHeader.locator('.selected-body-mode')).toContainText('TEXT'); - - // Change message 1 type from json to xml - await firstHeader.locator('.body-mode-selector').click(); - await page.locator('.dropdown-item').filter({ hasText: 'XML' }).click(); - - await expect(firstHeader.locator('.selected-body-mode')).toContainText('XML'); - - await saveRequest(page); - - const bruContent = await readFile(MULTI_MSG_BRU_PATH, 'utf8'); - expect(bruContent).toContain('type: xml'); - expect(bruContent).toContain('type: text'); - - // Re-open to verify persistence - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await expect(page.getByTestId('ws-message-header-0').locator('.selected-body-mode')).toContainText('XML'); - await expect(page.getByTestId('ws-message-header-1').locator('.selected-body-mode')).toContainText('TEXT'); - }); - - test('send selected message to active connection', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await locators.connectionControls.connect().click(); - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - const messageItems = locators.messages().locator('.text-ellipsis'); - const beforeCount = await messageItems.count(); - - // Click the main send button — sends the currently selected message - await page.getByTestId('run-button').click(); - - // Expect at least one new message (outgoing + echo response from server) - await expect.poll(() => messageItems.count(), { timeout: MAX_CONNECTION_TIME }).toBeGreaterThan(beforeCount); - - await locators.connectionControls.disconnect().click(); - await expect(locators.connectionControls.connect()).toBeVisible(); - }); - - test('first message is implicitly selected when no message is marked selected', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - // ws-multi-msg.bru has two messages with no `selected: true` flag. The - // main send button should therefore dispatch the first message. - await locators.connectionControls.connect().click(); - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - await page.getByTestId('run-button').click(); - - // the first message's content ("subscribe"), and none should carry the - // second message's content ("hello world"). - await expect(locators.messages().filter({ hasText: 'subscribe' }).first()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - await expect(locators.messages().filter({ hasText: 'hello world' })).toHaveCount(0); - - await locators.connectionControls.disconnect().click(); - }); - - test('selecting a different message routes run-button to that message', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - // Select the second message by clicking its header - await page.getByTestId('ws-message-header-1').click(); - - await locators.connectionControls.connect().click(); - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - await page.getByTestId('run-button').click(); - - await expect(locators.messages().filter({ hasText: 'hello world' }).first()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - await expect(locators.messages().filter({ hasText: 'subscribe' })).toHaveCount(0); - - await locators.connectionControls.disconnect().click(); - }); - - test('per-message send button sends that specific message', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - // Hover the header to reveal hover-actions, then click the second - await page.getByTestId('ws-message-header-1').hover(); - await page.getByTestId('ws-send-msg-1').click(); - - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - await expect(locators.messages().filter({ hasText: 'hello world' }).first()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - await locators.connectionControls.disconnect().click(); - }); - - test('prettify json message content', async ({ pageWithUserData: page }) => { - const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a'; - - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - // Expand the first message if not already expanded - const editorBody = page.getByTestId('ws-message-body-0'); - if (!(await editorBody.isVisible())) { - await page.getByTestId('ws-message-header-0').click(); - } - const editor = editorBody.locator('.CodeMirror'); - await editor.click(); - const textarea = editor.locator('textarea'); - await textarea.focus(); - await page.keyboard.press(selectAllShortcut); - await page.keyboard.insertText('{"name":"bruno","version":"1.0"}'); - - await page.getByTestId('ws-prettify-all').click(); - - // Verify prettification split single line into multiple lines - const lineNumbers = await editor.locator('.CodeMirror-linenumber').count(); - expect(lineNumbers).toBeGreaterThan(1); - }); - - test('delete a message', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(2); - - // Hover over the message header to reveal the delete button - await page.getByTestId('ws-message-header-1').hover(); - await page.getByTestId('ws-delete-msg-1').click(); - - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(1); - - await saveRequest(page); - - const bruContent = await readFile(MULTI_MSG_BRU_PATH, 'utf8'); - const bodyWsCount = (bruContent.match(/body:ws/g) || []).length; - expect(bodyWsCount).toBe(1); - }); - - test('rename a message via double-click', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - const messageLabel = page.getByTestId('ws-message-label-0'); - await messageLabel.dblclick(); - - const nameInput = page.getByTestId('ws-message-name-input-0'); - await expect(nameInput).toBeVisible(); - - await nameInput.selectText(); - await page.keyboard.type('subscribe request'); - await nameInput.press('Enter'); - - await expect(page.getByTestId('ws-message-label-0').filter({ hasText: 'subscribe request' })).toBeVisible(); - - await saveRequest(page); - - const bruContent = await readFile(SINGLE_MSG_BRU_PATH, 'utf8'); - expect(bruContent).toContain('name: subscribe request'); - }); -}); diff --git a/tests/websockets/multi-message-yml/fixtures/collection/opencollection.yml b/tests/websockets/multi-message-yml/fixtures/collection/opencollection.yml deleted file mode 100644 index 48e8110d4..000000000 --- a/tests/websockets/multi-message-yml/fixtures/collection/opencollection.yml +++ /dev/null @@ -1,6 +0,0 @@ -opencollection: '1.0.0' - -info: - name: ws-multi-message-yml - -bundled: false diff --git a/tests/websockets/multi-message-yml/fixtures/collection/ws-multi-msg.yml b/tests/websockets/multi-message-yml/fixtures/collection/ws-multi-msg.yml deleted file mode 100644 index e4c1ac45b..000000000 --- a/tests/websockets/multi-message-yml/fixtures/collection/ws-multi-msg.yml +++ /dev/null @@ -1,24 +0,0 @@ -info: - name: ws-multi-msg - type: websocket - seq: 1 - -websocket: - url: ws://localhost:8081/ws/echo - message: - - title: message 1 - message: - type: json - data: |- - { - "action": "subscribe" - } - - title: message 2 - message: - type: text - data: hello world - auth: inherit - -settings: - timeout: 0 - keepAliveInterval: 0 diff --git a/tests/websockets/multi-message-yml/fixtures/collection/ws-single-msg.yml b/tests/websockets/multi-message-yml/fixtures/collection/ws-single-msg.yml deleted file mode 100644 index 7afd34771..000000000 --- a/tests/websockets/multi-message-yml/fixtures/collection/ws-single-msg.yml +++ /dev/null @@ -1,18 +0,0 @@ -info: - name: ws-single-msg - type: websocket - seq: 2 - -websocket: - url: ws://localhost:8081/ws/echo - message: - type: json - data: |- - { - "foo": "bar" - } - auth: inherit - -settings: - timeout: 0 - keepAliveInterval: 0 diff --git a/tests/websockets/multi-message-yml/init-user-data/preferences.json b/tests/websockets/multi-message-yml/init-user-data/preferences.json deleted file mode 100644 index b62afc1e9..000000000 --- a/tests/websockets/multi-message-yml/init-user-data/preferences.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "maximized": false, - "lastOpenedCollections": [ - "{{projectRoot}}/tests/websockets/multi-message-yml/fixtures/collection" - ], - "preferences": { - "onboarding": { - "hasLaunchedBefore": true, - "hasSeenWelcomeModal": true - } - } -} \ No newline at end of file diff --git a/tests/websockets/multi-message-yml/multi-message.spec.ts b/tests/websockets/multi-message-yml/multi-message.spec.ts deleted file mode 100644 index 5fdaab951..000000000 --- a/tests/websockets/multi-message-yml/multi-message.spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { expect, test } from '../../../playwright'; -import { buildWebsocketCommonLocators } from '../../utils/page/locators'; -import { openRequest, saveRequest, closeAllCollections } from '../../utils/page/actions'; -import { readFile, writeFile } from 'fs/promises'; -import { join } from 'path'; - -const COLLECTION_NAME = 'ws-multi-message-yml'; -const MULTI_MSG_REQ = 'ws-multi-msg'; -const SINGLE_MSG_REQ = 'ws-single-msg'; -const MULTI_MSG_YML_PATH = join(__dirname, 'fixtures/collection/ws-multi-msg.yml'); -const SINGLE_MSG_YML_PATH = join(__dirname, 'fixtures/collection/ws-single-msg.yml'); -const MAX_CONNECTION_TIME = 3000; - -test.describe('websocket multi-message (yml format)', () => { - let originalMultiMsgData = ''; - let originalSingleMsgData = ''; - - test.beforeAll(async () => { - originalMultiMsgData = await readFile(MULTI_MSG_YML_PATH, 'utf8'); - originalSingleMsgData = await readFile(SINGLE_MSG_YML_PATH, 'utf8'); - }); - - test.afterEach(async () => { - await writeFile(MULTI_MSG_YML_PATH, originalMultiMsgData, 'utf8'); - await writeFile(SINGLE_MSG_YML_PATH, originalSingleMsgData, 'utf8'); - }); - - test.afterAll(async ({ pageWithUserData: page }) => { - await closeAllCollections(page); - }); - - test('backward compatibility: old single-message format loads correctly', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - // The old format (message: { type, data }) should load as a single accordion - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(1); - - // Expand the first message if not already expanded - if (!(await page.getByTestId('ws-message-body-0').isVisible())) { - await page.getByTestId('ws-message-header-0').click(); - } - await expect(page.getByTestId('ws-message-body-0')).toBeVisible(); - - // Verify the type is correctly read from the old format - await expect(page.getByTestId('ws-message-header-0').locator('.selected-body-mode')).toContainText('JSON'); - - // Add a second message to trigger format migration - await page.getByTestId('ws-add-message').click(); - const nameInput = page.getByTestId(/^ws-message-name-input-/); - await expect(nameInput).toBeVisible(); - await nameInput.selectText(); - await page.keyboard.type('new message'); - await nameInput.press('Enter'); - - await saveRequest(page); - - // Verify the yml file now uses the array format (WebSocketMessageVariant[]) - const ymlContent = await readFile(SINGLE_MSG_YML_PATH, 'utf8'); - expect(ymlContent).toContain('- title:'); - expect(ymlContent).toContain('new message'); - - // Re-open to verify it still loads correctly after format migration - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(2); - }); - - test('add a new message and save', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await page.getByTestId('ws-add-message').click(); - - const nameInput = page.getByTestId(/^ws-message-name-input-/); - await expect(nameInput).toBeVisible(); - - await nameInput.selectText(); - await page.keyboard.type('ping message'); - await nameInput.press('Enter'); - - await expect(page.getByTestId(/^ws-message-label-/).filter({ hasText: 'ping message' })).toBeVisible(); - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(3); - - await saveRequest(page); - - const ymlContent = await readFile(MULTI_MSG_YML_PATH, 'utf8'); - expect(ymlContent).toContain('ping message'); - }); - - test('edit message content and verify persistence', async ({ pageWithUserData: page }) => { - const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a'; - - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - // Expand the first message if not already expanded - const editorBody = page.getByTestId('ws-message-body-0'); - if (!(await editorBody.isVisible())) { - await page.getByTestId('ws-message-header-0').click(); - } - const editor = editorBody.locator('.CodeMirror'); - await editor.click(); - const textarea = editor.locator('textarea'); - await textarea.focus(); - await page.keyboard.press(selectAllShortcut); - await page.keyboard.insertText('{"updated": "content"}'); - - await saveRequest(page); - - const ymlContent = await readFile(SINGLE_MSG_YML_PATH, 'utf8'); - expect(ymlContent).toContain('{"updated": "content"}'); - }); - - test('messages with different types persist correctly', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - const firstHeader = page.getByTestId('ws-message-header-0'); - await expect(firstHeader.locator('.selected-body-mode')).toContainText('JSON'); - - const secondHeader = page.getByTestId('ws-message-header-1'); - await expect(secondHeader.locator('.selected-body-mode')).toContainText('TEXT'); - - // Change message 1 type from json to xml - await firstHeader.locator('.body-mode-selector').click(); - await page.locator('.dropdown-item').filter({ hasText: 'XML' }).click(); - - await expect(firstHeader.locator('.selected-body-mode')).toContainText('XML'); - - await saveRequest(page); - - const ymlContent = await readFile(MULTI_MSG_YML_PATH, 'utf8'); - expect(ymlContent).toContain('type: xml'); - expect(ymlContent).toContain('type: text'); - - // Re-open to verify persistence - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await expect(page.getByTestId('ws-message-header-0').locator('.selected-body-mode')).toContainText('XML'); - await expect(page.getByTestId('ws-message-header-1').locator('.selected-body-mode')).toContainText('TEXT'); - }); - - test('send selected message to active connection', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await locators.connectionControls.connect().click(); - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - const messageItems = locators.messages().locator('.text-ellipsis'); - const beforeCount = await messageItems.count(); - - // Click the main send button — sends the currently selected message - await page.getByTestId('run-button').click(); - - // Expect at least one new message (outgoing + echo response from server) - await expect.poll(() => messageItems.count(), { timeout: MAX_CONNECTION_TIME }).toBeGreaterThan(beforeCount); - - await locators.connectionControls.disconnect().click(); - await expect(locators.connectionControls.connect()).toBeVisible(); - }); - - test('first message is implicitly selected when no message is marked selected', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - // ws-multi-msg.yml has two messages with no `selected: true` flag. The - // main send button should therefore dispatch the first message. - await locators.connectionControls.connect().click(); - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - await page.getByTestId('run-button').click(); - - // the first message's content ("subscribe"), and none should carry the - // second message's content ("hello world"). - await expect(locators.messages().filter({ hasText: 'subscribe' }).first()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - await expect(locators.messages().filter({ hasText: 'hello world' })).toHaveCount(0); - - await locators.connectionControls.disconnect().click(); - }); - - test('selecting a different message routes run-button to that message', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - // Select the second message by clicking its header - await page.getByTestId('ws-message-header-1').click(); - - await locators.connectionControls.connect().click(); - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - await page.getByTestId('run-button').click(); - - await expect(locators.messages().filter({ hasText: 'hello world' }).first()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - await expect(locators.messages().filter({ hasText: 'subscribe' })).toHaveCount(0); - - await locators.connectionControls.disconnect().click(); - }); - - test('per-message send button sends that specific message', async ({ pageWithUserData: page }) => { - const locators = buildWebsocketCommonLocators(page); - - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - // Hover the header to reveal hover-actions, then click the second - // message's send button - await page.getByTestId('ws-message-header-1').hover(); - await page.getByTestId('ws-send-msg-1').click(); - - await expect(locators.connectionControls.disconnect()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - await expect(locators.messages().filter({ hasText: 'hello world' }).first()).toBeAttached({ - timeout: MAX_CONNECTION_TIME - }); - - await locators.connectionControls.disconnect().click(); - }); - - test('prettify json message content', async ({ pageWithUserData: page }) => { - const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a'; - - await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); - - // Expand the first message if not already expanded - const editorBody = page.getByTestId('ws-message-body-0'); - if (!(await editorBody.isVisible())) { - await page.getByTestId('ws-message-header-0').click(); - } - const editor = editorBody.locator('.CodeMirror'); - await editor.click(); - const textarea = editor.locator('textarea'); - await textarea.focus(); - await page.keyboard.press(selectAllShortcut); - await page.keyboard.insertText('{"name":"bruno","version":"1.0"}'); - - await page.getByTestId('ws-prettify-all').click(); - - // Verify prettification split single line into multiple lines - const lineNumbers = await editor.locator('.CodeMirror-linenumber').count(); - expect(lineNumbers).toBeGreaterThan(1); - }); - - test('delete a message', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(2); - - // Hover over the message header to reveal the delete button - await page.getByTestId('ws-message-header-1').hover(); - await page.getByTestId('ws-delete-msg-1').click(); - - await expect(page.getByTestId(/^ws-message-header-/)).toHaveCount(1); - - await saveRequest(page); - - const ymlContent = await readFile(MULTI_MSG_YML_PATH, 'utf8'); - const titleCount = (ymlContent.match(/- title:/g) || []).length; - expect(titleCount).toBeLessThanOrEqual(1); - }); - - test('rename a message via double-click', async ({ pageWithUserData: page }) => { - await openRequest(page, COLLECTION_NAME, MULTI_MSG_REQ); - - const messageLabel = page.getByTestId('ws-message-label-0'); - await messageLabel.dblclick(); - - const nameInput = page.getByTestId('ws-message-name-input-0'); - await expect(nameInput).toBeVisible(); - - await nameInput.selectText(); - await page.keyboard.type('subscribe request'); - await nameInput.press('Enter'); - - await expect(page.getByTestId('ws-message-label-0').filter({ hasText: 'subscribe request' })).toBeVisible(); - - await saveRequest(page); - - const ymlContent = await readFile(MULTI_MSG_YML_PATH, 'utf8'); - expect(ymlContent).toContain('subscribe request'); - }); -});