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');
+ });
+});