diff --git a/frontend/src/container/TraceDetail/utils.test.ts b/frontend/src/container/TraceDetail/utils.test.ts
new file mode 100644
index 0000000000..4cb20055bf
--- /dev/null
+++ b/frontend/src/container/TraceDetail/utils.test.ts
@@ -0,0 +1,56 @@
+import { ITraceTree } from 'types/api/trace/getTraceItem';
+
+import { getTreeLevelsCount } from './utils';
+
+describe('traces/getTreeLevelsCount', () => {
+ const createNode = (id: string, children: ITraceTree[] = []): ITraceTree => ({
+ id,
+ name: '',
+ value: 0,
+ time: 0,
+ startTime: 0,
+ tags: [],
+ children,
+ serviceName: '',
+ serviceColour: '',
+ });
+
+ test('should return 0 for empty tree', () => {
+ const emptyTree = null;
+ expect(getTreeLevelsCount((emptyTree as unknown) as ITraceTree)).toBe(0);
+ });
+
+ test('should return 1 for a tree with a single node', () => {
+ const singleNodeTree = createNode('1');
+ expect(getTreeLevelsCount(singleNodeTree)).toBe(1);
+ });
+
+ test('should return correct depth for a balanced tree', () => {
+ const tree = createNode('1', [
+ createNode('2', [createNode('4'), createNode('5')]),
+ createNode('3', [createNode('6'), createNode('7')]),
+ ]);
+
+ expect(getTreeLevelsCount(tree)).toBe(3);
+ });
+
+ test('should return correct depth for an unbalanced tree', () => {
+ const tree = createNode('1', [
+ createNode('2', [
+ createNode('4', [createNode('8', [createNode('11')])]),
+ createNode('5'),
+ ]),
+ createNode('3', [createNode('6'), createNode('7', [createNode('10')])]),
+ ]);
+
+ expect(getTreeLevelsCount(tree)).toBe(5);
+ });
+
+ test('should return correct depth for a tree with single child nodes', () => {
+ const tree = createNode('1', [
+ createNode('2', [createNode('3', [createNode('4', [createNode('5')])])]),
+ ]);
+
+ expect(getTreeLevelsCount(tree)).toBe(5);
+ });
+});
diff --git a/frontend/src/container/TraceDetail/utils.ts b/frontend/src/container/TraceDetail/utils.ts
index 1e978f28ae..c907a1e586 100644
--- a/frontend/src/container/TraceDetail/utils.ts
+++ b/frontend/src/container/TraceDetail/utils.ts
@@ -93,7 +93,12 @@ export const getSortedData = (treeData: ITraceTree): ITraceTree => {
};
export const getTreeLevelsCount = (tree: ITraceTree): number => {
- let levels = 0;
+ if (!tree) {
+ return 0;
+ }
+
+ let levels = 1;
+
const traverse = (treeNode: ITraceTree, level: number): void => {
if (!treeNode) {
return;
diff --git a/frontend/src/hooks/useInterval.test.ts b/frontend/src/hooks/useInterval.test.ts
new file mode 100644
index 0000000000..c6626b2224
--- /dev/null
+++ b/frontend/src/hooks/useInterval.test.ts
@@ -0,0 +1,93 @@
+import { act, renderHook } from '@testing-library/react';
+
+import useInterval from './useInterval';
+
+jest.useFakeTimers();
+
+describe('useInterval', () => {
+ test('calls the callback with a given delay', () => {
+ const callback = jest.fn();
+ const delay = 1000;
+
+ renderHook(() => useInterval(callback, delay));
+
+ expect(callback).toHaveBeenCalledTimes(0);
+
+ act(() => {
+ jest.advanceTimersByTime(delay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ act(() => {
+ jest.advanceTimersByTime(delay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+ });
+
+ test('does not call the callback if not enabled', () => {
+ const callback = jest.fn();
+ const delay = 1000;
+ const enabled = false;
+
+ renderHook(() => useInterval(callback, delay, enabled));
+
+ act(() => {
+ jest.advanceTimersByTime(delay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(0);
+ });
+
+ test('cleans up the interval when unmounted', () => {
+ const callback = jest.fn();
+ const delay = 1000;
+
+ const { unmount } = renderHook(() => useInterval(callback, delay));
+
+ act(() => {
+ jest.advanceTimersByTime(delay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ unmount();
+
+ act(() => {
+ jest.advanceTimersByTime(delay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ test('updates the interval when delay changes', () => {
+ const callback = jest.fn();
+ const initialDelay = 1000;
+ const newDelay = 2000;
+
+ const { rerender } = renderHook(({ delay }) => useInterval(callback, delay), {
+ initialProps: { delay: initialDelay },
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(initialDelay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ rerender({ delay: newDelay });
+
+ act(() => {
+ jest.advanceTimersByTime(initialDelay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ act(() => {
+ jest.advanceTimersByTime(newDelay - initialDelay);
+ });
+
+ expect(callback).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/frontend/src/hooks/usePreviousValue.test.tsx b/frontend/src/hooks/usePreviousValue.test.tsx
new file mode 100644
index 0000000000..efc3073af7
--- /dev/null
+++ b/frontend/src/hooks/usePreviousValue.test.tsx
@@ -0,0 +1,43 @@
+import { renderHook } from '@testing-library/react';
+
+import usePreviousValue from './usePreviousValue';
+
+describe('usePreviousValue', () => {
+ test('returns the previous value of a given variable', () => {
+ const { result, rerender } = renderHook(
+ ({ value }) => usePreviousValue(value),
+ {
+ initialProps: { value: 1 },
+ baseElement: document.body,
+ },
+ );
+
+ expect(result.current).toBeUndefined();
+
+ rerender({ value: 2 });
+
+ expect(result.current).toBe(1);
+
+ rerender({ value: 3 });
+
+ expect(result.current).toBe(2);
+ });
+
+ test('works with different types of values', () => {
+ const { result, rerender } = renderHook(
+ ({ value }) => usePreviousValue(value),
+ {
+ initialProps: { value: 'a' },
+ },
+ );
+
+ expect(result.current).toBeUndefined();
+
+ rerender({ value: 'b' });
+
+ expect(result.current).toBe('a');
+
+ rerender({ value: 'c' });
+ expect(result.current).toBe('b');
+ });
+});
diff --git a/frontend/src/hooks/useUrlQuery.test.tsx b/frontend/src/hooks/useUrlQuery.test.tsx
new file mode 100644
index 0000000000..a24edb3360
--- /dev/null
+++ b/frontend/src/hooks/useUrlQuery.test.tsx
@@ -0,0 +1,57 @@
+import { act, renderHook } from '@testing-library/react';
+import { createMemoryHistory } from 'history';
+import React from 'react';
+import { Router } from 'react-router-dom';
+
+import useUrlQuery from './useUrlQuery';
+
+describe('useUrlQuery', () => {
+ test('returns URLSearchParams object for the current URL search', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/test?param1=value1¶m2=value2'],
+ });
+
+ const { result } = renderHook(() => useUrlQuery(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(result.current.get('param1')).toBe('value1');
+ expect(result.current.get('param2')).toBe('value2');
+ });
+
+ test('updates URLSearchParams object when URL search changes', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/test?param1=value1'],
+ });
+
+ const { result, rerender } = renderHook(() => useUrlQuery(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(result.current.get('param1')).toBe('value1');
+ expect(result.current.get('param2')).toBe(null);
+
+ act(() => {
+ history.push('/test?param1=newValue1¶m2=value2');
+ });
+
+ rerender();
+
+ expect(result.current.get('param1')).toBe('newValue1');
+ expect(result.current.get('param2')).toBe('value2');
+ });
+
+ test('returns empty URLSearchParams object when no query parameters are present', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/test'],
+ });
+
+ const { result } = renderHook(() => useUrlQuery(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(result.current.toString()).toBe('');
+ expect(result.current.get('param1')).toBe(null);
+ expect(result.current.get('param2')).toBe(null);
+ });
+});