mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
feat(websocket): show full message name in a tooltip on hover (#8299)
This commit is contained in:
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user