feat(websocket): show full message name in a tooltip on hover (#8299)

This commit is contained in:
Pooja
2026-06-18 19:41:58 +05:30
committed by GitHub
parent 5345cb7b5f
commit a7efed674e
8 changed files with 173 additions and 23 deletions

View File

@@ -25,7 +25,7 @@ const EnvironmentListContent = ({
<span>No Environment</span>
</div>
<ToolHint
anchorSelect="[data-tooltip-content]"
tooltipId="environment-name-tooltip"
place="right"
positionStrategy="fixed"
tooltipStyle={{
@@ -40,6 +40,7 @@ const EnvironmentListContent = ({
key={env.uid}
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'dropdown-item-active' : ''}`}
onClick={() => onEnvironmentSelect(env)}
data-tooltip-id="environment-name-tooltip"
data-tooltip-content={env.name}
data-tooltip-hidden={env.name?.length < 90}
>

View File

@@ -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;

View File

@@ -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()}
/>
) : (
<span
className="message-label"
data-testid={`ws-message-label-${index}`}
onClick={(e) => {
e.preventDefault();
onToggle();
}}
onDoubleClick={handleNameClick}
<ToolHint
text={displayName}
toolhintId={labelTooltipId}
className="message-label-anchor"
place="bottom-start"
positionStrategy="fixed"
tooltipTestId="ws-message-name-tooltip"
tooltipStyle={{ maxWidth: '320px', whiteSpace: 'normal', wordBreak: 'break-word' }}
>
{displayName}
</span>
<span
className="message-label"
data-testid={`ws-message-label-${index}`}
onClick={(e) => {
e.preventDefault();
onToggle();
}}
onDoubleClick={handleNameClick}
>
{displayName}
</span>
</ToolHint>
)}
</div>
<div className="accordion-actions" onClick={(e) => e.stopPropagation()}>

View File

@@ -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 && <span id={toolhintId} className={className} data-testid={dataTestId}>{children}</span>}
{anchorSelect && children}
{!usesExternalAnchor && <span id={toolhintId} className={className} data-testid={dataTestId}>{children}</span>}
{usesExternalAnchor && children}
<ReactToolHint
{...toolhintProps_final}
content={anchorSelect ? undefined : text}
content={usesExternalAnchor ? undefined : text}
render={tooltipTestId ? ({ content }) => <span data-testid={tooltipTestId}>{content}</span> : undefined}
className="toolhint"
offset={offset}
place={place}

View File

@@ -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 };

View File

@@ -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' }),

View File

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

View File

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