From a7efed674eca87b3e811c65af956a723aed13850 Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 18 Jun 2026 19:41:58 +0530 Subject: [PATCH] feat(websocket): show full message name in a tooltip on hover (#8299) --- .../EnvironmentListContent/index.js | 3 +- .../WsBody/SingleWSMessage/StyledWrapper.js | 20 +++++++- .../WsBody/SingleWSMessage/index.js | 33 ++++++++----- .../src/components/ToolHint/index.js | 20 +++++--- tests/utils/page/actions.ts | 23 +++++++++- tests/utils/page/locators.ts | 5 ++ .../message-name-tooltip.spec.ts | 46 +++++++++++++++++++ .../message-name-tooltip.spec.ts | 46 +++++++++++++++++++ 8 files changed, 173 insertions(+), 23 deletions(-) create mode 100644 tests/websockets/multi-message-bru/message-name-tooltip.spec.ts create mode 100644 tests/websockets/multi-message-yml/message-name-tooltip.spec.ts diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js index be9b469a5..7f3cce3a9 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js @@ -25,7 +25,7 @@ const EnvironmentListContent = ({ No Environment onEnvironmentSelect(env)} + data-tooltip-id="environment-name-tooltip" data-tooltip-content={env.name} data-tooltip-hidden={env.name?.length < 90} > 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 e2ce8e423..3c27a00f4 100644 --- a/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js @@ -2,10 +2,20 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` border-bottom: 1px solid ${(props) => props.theme.border.border0}; - transition: opacity 0.15s ease; + + /* Dim the row content when disabled, but not the tooltip */ + .accordion-left > :not(.toolhint), + .accordion-actions, + .accordion-body { + transition: opacity 0.15s ease; + } &.disabled { - opacity: 0.45; + .accordion-left > :not(.toolhint), + .accordion-actions, + .accordion-body { + opacity: 0.45; + } } .accordion-header { @@ -24,6 +34,12 @@ const StyledWrapper = styled.div` min-width: 0; color: ${(props) => props.theme.text}; + .message-label-anchor { + display: flex; + min-width: 0; + overflow: hidden; + } + .message-label { font-size: ${(props) => props.theme.font.size.sm}; cursor: text; 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..eb1ebba83 100644 --- a/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js +++ b/packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js @@ -5,7 +5,7 @@ import { get } from 'lodash'; 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, { useMemo, useState, useEffect, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { queueWsMessage, isWsConnectionActive, connectWS } from 'utils/network/index'; import { findCollectionByUid, findEnvironmentInCollection } from 'utils/collections/index'; @@ -53,6 +53,7 @@ export const SingleWSMessage = ({ const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(displayName); + const labelTooltipId = `ws-msg-label-${message.uid ?? index}`; // Auto-focus the name input when this is a newly created message useEffect(() => { @@ -204,17 +205,27 @@ export const SingleWSMessage = ({ onClick={(e) => e.stopPropagation()} /> ) : ( - { - e.preventDefault(); - onToggle(); - }} - onDoubleClick={handleNameClick} + - {displayName} - + { + e.preventDefault(); + onToggle(); + }} + onDoubleClick={handleNameClick} + > + {displayName} + + )}
e.stopPropagation()}> diff --git a/packages/bruno-app/src/components/ToolHint/index.js b/packages/bruno-app/src/components/ToolHint/index.js index 70d1baba7..79c3fb6a4 100644 --- a/packages/bruno-app/src/components/ToolHint/index.js +++ b/packages/bruno-app/src/components/ToolHint/index.js @@ -5,6 +5,7 @@ import { useTheme } from 'providers/Theme'; const ToolHint = ({ text, toolhintId, + tooltipId, anchorSelect, children, tooltipStyle = {}, @@ -15,7 +16,8 @@ const ToolHint = ({ theme = null, className = '', delayShow = 200, - dataTestId + dataTestId, + tooltipTestId }) => { const { theme: contextTheme } = useTheme(); const appliedTheme = theme || contextTheme; @@ -32,17 +34,21 @@ const ToolHint = ({ color: toolhintTextColor }; - const toolhintProps_final = anchorSelect - ? { anchorSelect } - : { anchorId: toolhintId }; + const usesExternalAnchor = Boolean(tooltipId || anchorSelect); + const toolhintProps_final = tooltipId + ? { id: tooltipId } + : anchorSelect + ? { anchorSelect } + : { anchorId: toolhintId }; return ( <> - {!anchorSelect && {children}} - {anchorSelect && children} + {!usesExternalAnchor && {children}} + {usesExternalAnchor && children} {content} : undefined} className="toolhint" offset={offset} place={place} diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index 27dc2e383..3a78a36aa 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -1,7 +1,7 @@ import { test, expect, Page, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright'; import process from 'node:process'; import * as path from 'path'; -import { buildCommonLocators, buildScriptErrorLocators, buildGrpcCommonLocators } from './locators'; +import { buildCommonLocators, buildScriptErrorLocators, buildGrpcCommonLocators, buildWebsocketCommonLocators } from './locators'; import { waitForCollectionMount } from './mounting'; type SandboxMode = 'safe' | 'developer'; @@ -1937,6 +1937,24 @@ const generateCollectionDocs = async ( }); }; +/** + * Rename a websocket message by double-clicking its label and typing a new name. + * @param page - The page object + * @param index - The zero-based index of the message in the list + * @param name - The new message name + */ +const renameWsMessage = async (page: Page, index: number, name: string) => { + await test.step(`Rename websocket message ${index} to "${name}"`, async () => { + const ws = buildWebsocketCommonLocators(page); + await ws.message.label(index).dblclick(); + const nameInput = ws.message.nameInput(index); + await expect(nameInput).toBeVisible(); + await nameInput.selectText(); + await page.keyboard.type(name); + await nameInput.press('Enter'); + }); +}; + export { waitForReadyPage, dismissImportIssuesToasts, @@ -2013,7 +2031,8 @@ export { closeGenerateCodeDialog, openRequestInFolder, setUrlEncoding, - generateCollectionDocs + generateCollectionDocs, + renameWsMessage }; export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, CreateTransientRequestOptions, AssertionInput }; diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index 70e007cf0..57b958b5e 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -238,6 +238,11 @@ export const buildWebsocketCommonLocators = (page: Page) => ({ .filter({ hasText: /^Close Connection$/ }) }, messages: () => page.locator('.ws-message'), + message: { + label: (index: number) => page.getByTestId(`ws-message-label-${index}`), + nameInput: (index: number) => page.getByTestId(`ws-message-name-input-${index}`), + nameTooltip: () => page.getByTestId('ws-message-name-tooltip') + }, toolbar: { latestFirst: () => page.getByRole('button', { name: 'Latest First' }), latestLast: () => page.getByRole('button', { name: 'Latest Last' }), diff --git a/tests/websockets/multi-message-bru/message-name-tooltip.spec.ts b/tests/websockets/multi-message-bru/message-name-tooltip.spec.ts new file mode 100644 index 000000000..f46a589a1 --- /dev/null +++ b/tests/websockets/multi-message-bru/message-name-tooltip.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from '../../../playwright'; +import { openRequest, closeAllCollections, renameWsMessage } from '../../utils/page/actions'; +import { buildWebsocketCommonLocators } from '../../utils/page/locators'; + +const COLLECTION_NAME = 'ws-multi-message'; +const SINGLE_MSG_REQ = 'ws-single-msg'; + +const LONG_NAME + = 'this is a very long websocket message name that should be truncated with an ellipsis'; + +test.describe('websocket message name tooltip', () => { + test.afterAll(async ({ pageWithUserData: page }) => { + await closeAllCollections(page); + }); + + test('shows the full name in a tooltip on hover', async ({ + pageWithUserData: page + }) => { + const ws = buildWebsocketCommonLocators(page); + await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); + + const messageLabel = ws.message.label(0); + const tooltip = ws.message.nameTooltip(); + + await test.step('short name → tooltip shows the name on hover', async () => { + await renameWsMessage(page, 0, 'hi'); + await expect(messageLabel).toHaveText('hi'); + + await messageLabel.hover(); + await expect(tooltip).toBeVisible({ timeout: 15000 }); + await expect(tooltip).toContainText('hi'); + }); + + await test.step('long name → label is truncated and tooltip reveals the full name', async () => { + await renameWsMessage(page, 0, LONG_NAME); + await expect(messageLabel).toHaveText(LONG_NAME); + // the label itself is clipped with an ellipsis... + await expect(messageLabel).toHaveCSS('text-overflow', 'ellipsis'); + + // ...and hovering surfaces the full name in the tooltip + await messageLabel.hover(); + await expect(tooltip).toBeVisible({ timeout: 15000 }); + await expect(tooltip).toContainText(LONG_NAME); + }); + }); +}); diff --git a/tests/websockets/multi-message-yml/message-name-tooltip.spec.ts b/tests/websockets/multi-message-yml/message-name-tooltip.spec.ts new file mode 100644 index 000000000..49d85911a --- /dev/null +++ b/tests/websockets/multi-message-yml/message-name-tooltip.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from '../../../playwright'; +import { openRequest, closeAllCollections, renameWsMessage } from '../../utils/page/actions'; +import { buildWebsocketCommonLocators } from '../../utils/page/locators'; + +const COLLECTION_NAME = 'ws-multi-message-yml'; +const SINGLE_MSG_REQ = 'ws-single-msg'; + +const LONG_NAME + = 'this is a very long websocket message name that should be truncated with an ellipsis'; + +test.describe('websocket message name tooltip (yml format)', () => { + test.afterAll(async ({ pageWithUserData: page }) => { + await closeAllCollections(page); + }); + + test('shows the full name in a tooltip on hover', async ({ + pageWithUserData: page + }) => { + const ws = buildWebsocketCommonLocators(page); + await openRequest(page, COLLECTION_NAME, SINGLE_MSG_REQ); + + const messageLabel = ws.message.label(0); + const tooltip = ws.message.nameTooltip(); + + await test.step('short name → tooltip shows the name on hover', async () => { + await renameWsMessage(page, 0, 'hi'); + await expect(messageLabel).toHaveText('hi'); + + await messageLabel.hover(); + await expect(tooltip).toBeVisible({ timeout: 15000 }); + await expect(tooltip).toContainText('hi'); + }); + + await test.step('long name → label is truncated and tooltip reveals the full name', async () => { + await renameWsMessage(page, 0, LONG_NAME); + await expect(messageLabel).toHaveText(LONG_NAME); + // the label itself is clipped with an ellipsis... + await expect(messageLabel).toHaveCSS('text-overflow', 'ellipsis'); + + // ...and hovering surfaces the full name in the tooltip + await messageLabel.hover(); + await expect(tooltip).toBeVisible({ timeout: 15000 }); + await expect(tooltip).toContainText(LONG_NAME); + }); + }); +});