diff --git a/frontend/src/container/TraceWaterfall/SpanLineActionButtons/__tests__/SpanLineActionButtons.test.tsx b/frontend/src/container/TraceWaterfall/SpanLineActionButtons/__tests__/SpanLineActionButtons.test.tsx new file mode 100644 index 0000000000..4bc8e5bc25 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/SpanLineActionButtons/__tests__/SpanLineActionButtons.test.tsx @@ -0,0 +1,90 @@ +import { fireEvent, screen } from '@testing-library/react'; +import { useCopySpanLink } from 'hooks/trace/useCopySpanLink'; +import { render } from 'tests/test-utils'; +import { Span } from 'types/api/trace/getTraceV2'; + +import SpanLineActionButtons from '../index'; + +// Mock the useCopySpanLink hook +jest.mock('hooks/trace/useCopySpanLink'); + +const mockSpan: Span = { + spanId: 'test-span-id', + name: 'test-span', + serviceName: 'test-service', + durationNano: 1000, + timestamp: 1234567890, + rootSpanId: 'test-root-span-id', + parentSpanId: 'test-parent-span-id', + traceId: 'test-trace-id', + hasError: false, + kind: 0, + references: [], + tagMap: {}, + event: [], + rootName: 'test-root-name', + statusMessage: 'test-status-message', + statusCodeString: 'test-status-code-string', + spanKind: 'test-span-kind', + hasChildren: false, + hasSibling: false, + subTreeNodeCount: 0, + level: 0, +}; + +describe('SpanLineActionButtons', () => { + beforeEach(() => { + // Clear mock before each test + jest.clearAllMocks(); + }); + + it('renders copy link button with correct icon', () => { + (useCopySpanLink as jest.Mock).mockReturnValue({ + onSpanCopy: jest.fn(), + }); + + render(); + + // Check if the button is rendered + const copyButton = screen.getByRole('button'); + expect(copyButton).toBeInTheDocument(); + + // Check if the link icon is rendered + const linkIcon = screen.getByRole('img', { hidden: true }); + expect(linkIcon).toHaveClass('anticon anticon-link'); + }); + + it('calls onSpanCopy when copy button is clicked', () => { + const mockOnSpanCopy = jest.fn(); + (useCopySpanLink as jest.Mock).mockReturnValue({ + onSpanCopy: mockOnSpanCopy, + }); + + render(); + + // Click the copy button + const copyButton = screen.getByRole('button'); + fireEvent.click(copyButton); + + // Verify the copy function was called + expect(mockOnSpanCopy).toHaveBeenCalledTimes(1); + }); + + it('applies correct styling classes', () => { + (useCopySpanLink as jest.Mock).mockReturnValue({ + onSpanCopy: jest.fn(), + }); + + render(); + + // Check if the main container has the correct class + const container = screen + .getByRole('button') + .closest('.span-line-action-buttons'); + expect(container).toHaveClass('span-line-action-buttons'); + + // Check if the button has the correct class + const copyButton = screen.getByRole('button'); + expect(copyButton).toHaveClass('copy-span-btn'); + }); +}); diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx index 3a28b90a39..a003dccc40 100644 --- a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx @@ -151,7 +151,7 @@ function SpanOverview({ ); } -function SpanDuration({ +export function SpanDuration({ span, traceMetadata, setSelectedSpan, diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx new file mode 100644 index 0000000000..838e8e4ebb --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx @@ -0,0 +1,131 @@ +import { fireEvent, screen } from '@testing-library/react'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { render } from 'tests/test-utils'; +import { Span } from 'types/api/trace/getTraceV2'; + +import { SpanDuration } from '../Success'; + +// Mock the hooks +jest.mock('hooks/useSafeNavigate'); +jest.mock('hooks/useUrlQuery'); + +const mockSpan: Span = { + spanId: 'test-span-id', + name: 'test-span', + serviceName: 'test-service', + durationNano: 1160000, // 1ms in nano + timestamp: 1234567890, + rootSpanId: 'test-root-span-id', + parentSpanId: 'test-parent-span-id', + traceId: 'test-trace-id', + hasError: false, + kind: 0, + references: [], + tagMap: {}, + event: [], + rootName: 'test-root-name', + statusMessage: 'test-status-message', + statusCodeString: 'test-status-code-string', + spanKind: 'test-span-kind', + hasChildren: false, + hasSibling: false, + subTreeNodeCount: 0, + level: 0, +}; + +const mockTraceMetadata = { + traceId: 'test-trace-id', + startTime: 1234567000, + endTime: 1234569000, + hasMissingSpans: false, +}; + +describe('SpanDuration', () => { + const mockSetSelectedSpan = jest.fn(); + const mockUrlQuerySet = jest.fn(); + const mockSafeNavigate = jest.fn(); + const mockUrlQueryGet = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock URL query hook + (useUrlQuery as jest.Mock).mockReturnValue({ + set: mockUrlQuerySet, + get: mockUrlQueryGet, + toString: () => 'spanId=test-span-id', + }); + + // Mock safe navigate hook + (useSafeNavigate as jest.Mock).mockReturnValue({ + safeNavigate: mockSafeNavigate, + }); + }); + + it('updates URL and selected span when clicked', () => { + render( + , + ); + + // Find and click the span duration element + const spanElement = screen.getByText('1.16 ms'); + fireEvent.click(spanElement); + + // Verify setSelectedSpan was called with the correct span + expect(mockSetSelectedSpan).toHaveBeenCalledWith(mockSpan); + + // Verify URL query was updated + expect(mockUrlQuerySet).toHaveBeenCalledWith('spanId', 'test-span-id'); + + // Verify navigation was triggered + expect(mockSafeNavigate).toHaveBeenCalledWith({ + search: 'spanId=test-span-id', + }); + }); + + it('shows action buttons on hover', () => { + render( + , + ); + + const spanElement = screen.getByText('1.16 ms'); + + // Initially, action buttons should not be visible + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + + // Hover over the span + fireEvent.mouseEnter(spanElement); + + // Action buttons should now be visible + expect(screen.getByRole('button')).toBeInTheDocument(); + + // Mouse leave should hide the buttons + fireEvent.mouseLeave(spanElement); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('applies interested-span class when span is selected', () => { + render( + , + ); + + const spanElement = screen.getByText('1.16 ms').closest('.span-duration'); + expect(spanElement).toHaveClass('interested-span'); + }); +});