feat: frontend unit test suite setup (#9027)

* feat: update context link modal form init

* feat: add double way sync on urls and param

* feat: minor refactor

* feat: minor refactor

* feat: change contextlinks data structure

* feat: context menu changes init

* feat: context menu hook refactor

* feat: context links processors

* feat: context variables hook added

* feat: add support for field variables

* feat: minor refactor

* feat: minor refactor

* feat: minor refactor

* feat: handle on save

* feat: minor refactor

* feat: snapshot update

* feat: revert qbv5

* feat: aggregation header val

* feat: fix header color

* feat: minor refactor

* feat: minor refactor

* feat: fix breaking changes from qb v5

* feat: change api for breakout opitons

* feat: minor refactor

* feat: minor refactor

* fix: added fix for extractquerypararms when value is string in multivalue operator

* feat: minor refactor

* feat: add back in breakout

* feat: minor refactor

* feat: add substitute var api call to decode vars

* feat: minor fix

* feat: optimize query value comparison in QueryBuilderV2

* feat: minor fix

* feat: minor fix

* feat: test fix

* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added variable in url and made dashboard sync around that and sharable (#7944)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added variable in url and made dashboard sync around that and sharable

* feat: added test cases

* feat: added safety check

* feat: enabled url setting on first load itself

* feat: code refactor

* feat: cleared options query param when on dashboard list page

* feat: resolved conflicts

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: updated test case

* feat: corrected the regex matcher for resolved titles

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* feat: minor refactor

* feat: added test cases

* feat: refactor

* feat: remove consoles

* feat: pass panel types to substitutevars

* feat: cross filtering init

* fix: added fix for query builder filters

* feat: cross filtering add set/unset/create functionality

* feat: test update

* fix: added migration to filter expression for crud operations of variable

* feat: format legend name according to existing format

* feat: breakout test init

* feat: breakout test match query

* feat: context links tests

* feat: minor refactor

* feat: show edit only if user has access

* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: updated test case

* feat: corrected the regex matcher for resolved titles

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* fix: added migration to filter expression for crud operations of variable

* feat: reverted dynamic variable url config changes (#8877)

* Revert "feat: changed query param name"

This reverts commit 62bee5f003bf74b0da1c5951f1b5d0f2c250905d.

* Revert "feat: added user-friendly format to dashboard variable url"

This reverts commit 6de8b1c2e8c6a838941014ea4929e9f5c908d975.

* feat: reverted url var changes

* feat: reverted url changed from usedashboardvarupdate hook

* feat: send empty array for widgetId

* feat: added type in the variables in query_range payload for dynamic

* feat: minor fixes

* fix: added fix for multivalue operator without brackets

* feat: minor fix

* feat: fix failing test

* feat: change revert

* test: added tests for querycontextUtils + querybuilderv2 utils

* fix: added fix for replacing filter with the new value

* fix: added fix for replacing filters + datetimepicker composite query

* test: fixed querybuilderv2 utils test

* feat: handle number dataType in filters

* feat: correct the variable addition to panel format for new qb expression

* feat: remove other queries in breakout

* feat: add metric to traces mapping

* feat: pass proper time range

* feat: update time range logic

* feat: value panel drilldown init

* feat: value panel drilldown init

* feat: enable context links in value panel

* feat: minor fix

* feat: update snapshot

* feat: hide breakout in value panel

* feat: add panel type to view mode

* feat: add support to change panel in breakouts

* feat: panel change for breakout logic added

* chore: fix style

* chore: show variables suggestion while creating context links

* chore: add timestamp to graphs

* chore: add timestamp to table panel

* chore: fix failing tests

* chore: fix infinite re-rendering due to queryRange

* chore: send appropriate time range when signal is metrics

* chore: show variables suggestion while creating context links

* chore: minor refactor

* chore: show trace details link if filter has trace_id

* chore: fix infinite render of table component

* chore: added tests for v2

* fix: context links set from dropdown

* chore: minor refactor

* chore: minor refactor

* chore: fix test

* chore: fix timerange for apm metrics

* fix: get correct timestamp for clicked data

* chore: comment out change to histogram on breakout by number

* chore: change panel type on panel type change in url

* chore: remove consoles

* feat: added dynamic variables creation flow (#7541)

* feat: added dynamic variables creation flow

* feat: added keys and value apis and hooks

* feat: added api and select component changes

* feat: added keys fetching and preview values

* feat: added dynamic variable to variable items

* feat: handled value persistence and tab switches

* feat: added default value and formed a schema for dyn-variables

* feat: added client and server side searches

* feat: corrected the initial load getfieldKey api

* feat: removed fetch on mount restriction

* feat: added dynamic variable to the dashboard details (#7755)

* feat: added dynamic variable to the dashboard details

* feat: added new component to existing variables

* feat: added enhancement to multiselect and select for dyn-variables

* feat: added refetch method between all dynamic-variables

* feat: correct error handling

* feat: correct error handling

* feat: enforced non-empty selectedvalues and default value

* feat: added client and server side searches

* feat: retry on error

* feat: correct error handling

* feat: handle defautl value in existing variables

* feat: lowercase the source for payload

* feat: fixed the incorrect assignment of active indices

* feat: improved handling of all option

* feat: improved the ALL option visuals

* feat: handled default value enforcement in existing variables

* feat: added unix time to values call

* feat: added incomplete data message and info to search

* feat: changed dashboard panel call handling with existing variables

* feat: adjusted the response type and data with the new API schema for values

* feat: code refactor

* feat: made dyn-variable option as the default

* feat: added test cases for dyn variable creation and completion

* feat: updated test cases

* feat: fix lint and test cases

* feat: fix typo

* feat: fixed test case

* feat: added dynamic variable suggestion in where clause

* feat: added test cases for hooks and api call functions

* feat: added test case for querybuildersearchv2 suggestion changes

* feat: code refactor

* feat: corrected the regex matcher for resolved titles

* feat: fixed test cases

* feat: added ability to add/remove variable filter to one or more existing panels

* feat: added widgetselector on variable creation

* feat: show labels in widget selector

* feat: added apply to all and variable removal logical

* feat: refectch only related and affected panels in case of dynamic variables

* feat: added button loader for apply-all

* feat: light-mode styles

* fix: added migration to filter expression for crud operations of variable

* feat: added type in the variables in query_range payload for dynamic

* feat: correct the variable addition to panel format for new qb expression

* feat: added test cases for dynamic variable and add/remove panel feat

* feat: implemented where clause suggestion in new qb v5

* feat: added retries for dyn variable and fixed on-enter selection issue

* feat: added relatedValues and existing query in param related changes

* feat: sanitized data storage and removed duplicates

* fix: fixed typechecks

* feat: updated panel wait and refetch logic and ALL option selection

* feat: fixed variable tabel reordering issue

* feat: added empty name validation in variable creation

* feat: change value to searchtext in values API

* feat: added option for regex in the component, disabled for now

* feat: added beta and not rec. tag in variable tabs

* feat: added check to prevent api and updates calls with same payload

* feat: optimized localstorage for all selection in dynamic variable and updated __all__ case

* feat: resolved variable tables infinite loop update error

* feat: aded variable name auto-update based on attribute name entered for dynamic variables

* feat: modified only/all click behaviour and set all selection always true for dynamic variable

* feat: fix dropdown closing doesn't reset us back to our all available values when we have a search

* feat: handled all state distinction and carry forward in existing variables

* feat: trucate + n more tooltip content to 10

* feat: fixed infinite loop because of dependency of frequently changing object ref in var table

* feat: fixed inconsist search implementations

* feat: reverted only - all updated area implementation

* feat: added more space for search in multiselect component

* feat: checked for variable id instead of variable key for refetch

* feat: improved performance around multiselect component and added confirm modal for apply to all

* feat: rewrite functionality around add and remove panels

* feat: changed color for apply to all modal

* feat: added changes under flag to handle variable specific removal for removeKeysFromExpression func

* feat: added validation in variable edit panel

* chore: fix dynamic variable update in context menu to latest logic

* chore: minor fix

* chore: type fix

* fix: remove unwanted code

* fix: remove unwanted code

* fix: resolved pr comments

* fix: minor fix

* fix: fix tests

* fix: style fix

* fix: hide drilldown options in view mode for non-builder panels

* chore: add global uplot mock

* chore: query builder context update to all provider

* chore: add cursor rules init

* chore: useSafeNavigate mock added

* chore: more cleanups

* chore: remove react-router-v5 mock from setup

* chore: update cursorrules

* chore: add tests readme init

* chore: minor refactor

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
Co-authored-by: Abhi Kumar <ahrefabhi@gmail.com>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <sagar@signoz.io>
This commit is contained in:
Aditya Singh 2025-09-18 12:19:57 +05:30 committed by GitHub
parent 0c25de9560
commit bced4774bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1018 additions and 804 deletions

484
frontend/.cursorrules Normal file
View File

@ -0,0 +1,484 @@
# Persona
You are an expert developer with deep knowledge of Jest, React Testing Library, MSW, and TypeScript, tasked with creating unit tests for this repository.
# Auto-detect TypeScript Usage
Check for TypeScript in the project through tsconfig.json or package.json dependencies.
Adjust syntax based on this detection.
# TypeScript Type Safety for Jest Tests
**CRITICAL**: All Jest tests MUST be fully type-safe with proper TypeScript types.
**Type Safety Requirements:**
- Use proper TypeScript interfaces for all mock data
- Type all Jest mock functions with `jest.MockedFunction<T>`
- Use generic types for React components and hooks
- Define proper return types for mock functions
- Use `as const` for literal types when needed
- Avoid `any` type use proper typing instead
# Unit Testing Focus
Focus on critical functionality (business logic, utility functions, component behavior)
Mock dependencies (API calls, external modules) before imports
Test multiple data scenarios (valid inputs, invalid inputs, edge cases)
Write maintainable tests with descriptive names grouped in describe blocks
# Global vs Local Mocks
**Use Global Mocks for:**
- High-frequency dependencies (20+ test files)
- Core infrastructure (react-router-dom, react-query, antd)
- Standard implementations across the app
- Browser APIs (ResizeObserver, matchMedia, localStorage)
- Utility libraries (date-fns, lodash)
**Use Local Mocks for:**
- Business logic dependencies (5-15 test files)
- Test-specific behavior (different data per test)
- API endpoints with specific responses
- Domain-specific components
- Error scenarios and edge cases
**Global Mock Files Available (from jest.config.ts):**
- `uplot` → `__mocks__/uplotMock.ts`
# Repo-specific Testing Conventions
## Imports
Always import from our harness:
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
```
For API mocks:
```ts
import { server, rest } from 'mocks-server/server';
```
Do not import directly from `@testing-library/react`.
## Router
Use the router built into render:
```ts
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
```
Only mock `useLocation` / `useParams` if the test depends on them.
## Hook Mocks
Pattern:
```ts
import useFoo from 'hooks/useFoo';
jest.mock('hooks/useFoo');
const mockUseFoo = jest.mocked(useFoo);
mockUseFoo.mockReturnValue(/* minimal shape */ as any);
```
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
## MSW
Global MSW server runs automatically.
Override per-test:
```ts
server.use(
rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
);
```
Keep large responses in `mocks-server/__mockdata_`.
## Interactions
- Prefer `userEvent` for real user interactions (click, type, select, tab).
- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
- Always await interactions:
```ts
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /save/i }));
```
```ts
// Example: virtualized list scroll (no userEvent helper)
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
scroller.scrollTop = targetScrollTop;
act(() => { fireEvent.scroll(scroller); });
```
## Timers
❌ No global fake timers.
✅ Per-test only, for debounce/throttle:
```ts
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
await user.type(screen.getByRole('textbox'), 'query');
jest.advanceTimersByTime(400);
jest.useRealTimers();
```
## Queries
Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`).
Fallback: visible text.
Last resort: `data-testid`.
# Example Test (using only configured global mocks)
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
# Anti-patterns
❌ Importing RTL directly
❌ Using global fake timers
❌ Wrapping render in `act(...)`
❌ Mocking infra dependencies locally (router, react-query)
✅ Use our harness (`tests/test-utils`)
✅ Use MSW for API overrides
✅ Use userEvent + await
✅ Pin time only in tests that assert relative dates
# Best Practices
- **Critical Functionality**: Prioritize testing business logic and utilities
- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
- **Data Scenarios**: Always test valid, invalid, and edge cases
- **Descriptive Names**: Make test intent clear
- **Organization**: Group related tests in describe
- **Consistency**: Match repo conventions
- **Edge Cases**: Test null, undefined, unexpected values
- **Limit Scope**: 35 focused tests per file
- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
- **No Any**: Enforce type safety
# Example Test
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
# Anti-patterns
❌ Importing RTL directly
❌ Using global fake timers
❌ Wrapping render in `act(...)`
❌ Mocking infra dependencies locally (router, react-query)
✅ Use our harness (`tests/test-utils`)
✅ Use MSW for API overrides
✅ Use userEvent + await
✅ Pin time only in tests that assert relative dates
# TypeScript Type Safety Examples
## Proper Mock Typing
```ts
// ✅ GOOD - Properly typed mocks
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Type the mock functions
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise<ApiResponse<User>>>;
// Mock implementation with proper typing
mockFetchUser.mockResolvedValue({
data: { id: 1, name: 'John Doe', email: 'john@example.com' },
status: 200,
message: 'Success'
});
// ❌ BAD - Using any type
const mockFetchUser = jest.fn() as any; // Don't do this
```
## React Component Testing with Types
```ts
// ✅ GOOD - Properly typed component testing
interface ComponentProps {
title: string;
data: User[];
onUserSelect: (user: User) => void;
isLoading?: boolean;
}
const TestComponent: React.FC<ComponentProps> = ({ title, data, onUserSelect, isLoading = false }) => {
// Component implementation
};
describe('TestComponent', () => {
it('should render with proper props', () => {
// Arrange - Type the props properly
const mockProps: ComponentProps = {
title: 'Test Title',
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
isLoading: false
};
// Act
render(<TestComponent {...mockProps} />);
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
});
```
## Hook Testing with Types
```ts
// ✅ GOOD - Properly typed hook testing
interface UseUserDataReturn {
user: User | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
const useUserData = (id: number): UseUserDataReturn => {
// Hook implementation
};
describe('useUserData', () => {
it('should return user data with proper typing', () => {
// Arrange
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
mockFetchUser.mockResolvedValue({
data: mockUser,
status: 200,
message: 'Success'
});
// Act
const { result } = renderHook(() => useUserData(1));
// Assert
expect(result.current.user).toEqual(mockUser);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
});
```
## Global Mock Type Safety
```ts
// ✅ GOOD - Type-safe global mocks
// In __mocks__/routerMock.ts
export const mockUseLocation = (overrides: Partial<Location> = {}): Location => ({
pathname: '/traces',
search: '',
hash: '',
state: null,
key: 'test-key',
...overrides,
});
// In test files
const location = useLocation(); // Properly typed from global mock
expect(location.pathname).toBe('/traces');
```
# TypeScript Configuration for Jest
## Required Jest Configuration
```json
// jest.config.ts
{
"preset": "ts-jest/presets/js-with-ts-esm",
"globals": {
"ts-jest": {
"useESM": true,
"isolatedModules": true,
"tsconfig": "<rootDir>/tsconfig.jest.json"
}
},
"extensionsToTreatAsEsm": [".ts", ".tsx"],
"moduleFileExtensions": ["ts", "tsx", "js", "json"]
}
```
## TypeScript Jest Configuration
```json
// tsconfig.jest.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "@testing-library/jest-dom"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"include": [
"src/**/*",
"**/*.test.ts",
"**/*.test.tsx",
"__mocks__/**/*"
]
}
```
## Common Type Safety Patterns
### Mock Function Typing
```ts
// ✅ GOOD - Proper mock function typing
const mockApiCall = jest.fn() as jest.MockedFunction<typeof apiCall>;
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
// ❌ BAD - Using any
const mockApiCall = jest.fn() as any;
```
### Generic Mock Typing
```ts
// ✅ GOOD - Generic mock typing
interface MockApiResponse<T> {
data: T;
status: number;
}
const mockFetchData = jest.fn() as jest.MockedFunction<
<T>(endpoint: string) => Promise<MockApiResponse<T>>
>;
// Usage
mockFetchData<User>('/users').mockResolvedValue({
data: { id: 1, name: 'John' },
status: 200
});
```
### React Testing Library with Types
```ts
// ✅ GOOD - Typed testing utilities
import { render, screen, RenderResult } from '@testing-library/react';
import { ComponentProps } from 'react';
type TestComponentProps = ComponentProps<typeof TestComponent>;
const renderTestComponent = (props: Partial<TestComponentProps> = {}): RenderResult => {
const defaultProps: TestComponentProps = {
title: 'Test',
data: [],
onSelect: jest.fn(),
...props
};
return render(<TestComponent {...defaultProps} />);
};
```
### Error Handling with Types
```ts
// ✅ GOOD - Typed error handling
interface ApiError {
message: string;
code: number;
details?: Record<string, unknown>;
}
const mockApiError: ApiError = {
message: 'API Error',
code: 500,
details: { endpoint: '/users' }
};
mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
```
## Type Safety Checklist
- [ ] All mock functions use `jest.MockedFunction<T>`
- [ ] All mock data has proper interfaces
- [ ] No `any` types in test files
- [ ] Generic types are used where appropriate
- [ ] Error types are properly defined
- [ ] Component props are typed
- [ ] Hook return types are defined
- [ ] API response types are defined
- [ ] Global mocks are type-safe
- [ ] Test utilities are properly typed
# Mock Decision Tree
```
Is it used in 20+ test files?
├─ YES → Use Global Mock
│ ├─ react-router-dom
│ ├─ react-query
│ ├─ antd components
│ └─ browser APIs
└─ NO → Is it business logic?
├─ YES → Use Local Mock
│ ├─ API endpoints
│ ├─ Custom hooks
│ └─ Domain components
└─ NO → Is it test-specific?
├─ YES → Use Local Mock
│ ├─ Error scenarios
│ ├─ Loading states
│ └─ Specific data
└─ NO → Consider Global Mock
└─ If it becomes frequently used
```
# Common Anti-Patterns to Avoid
❌ **Don't mock global dependencies locally:**
```js
// BAD - This is already globally mocked
jest.mock('react-router-dom', () => ({ ... }));
```
❌ **Don't create global mocks for test-specific data:**
```js
// BAD - This should be local
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => specificTestData)
}));
```
✅ **Do use global mocks for infrastructure:**
```js
// GOOD - Use global mock
import { useLocation } from 'react-router-dom';
```
✅ **Do create local mocks for business logic:**
```js
// GOOD - Local mock for specific test needs
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => mockTracesData)
}));
```

View File

@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// Mock for uplot library used in tests
export interface MockUPlotInstance {
setData: jest.Mock;
setSize: jest.Mock;
destroy: jest.Mock;
redraw: jest.Mock;
setSeries: jest.Mock;
}
export interface MockUPlotPaths {
spline: jest.Mock;
bars: jest.Mock;
}
// Create mock instance methods
const createMockUPlotInstance = (): MockUPlotInstance => ({
setData: jest.fn(),
setSize: jest.fn(),
destroy: jest.fn(),
redraw: jest.fn(),
setSeries: jest.fn(),
});
// Create mock paths
const mockPaths: MockUPlotPaths = {
spline: jest.fn(),
bars: jest.fn(),
};
// Mock static methods
const mockTzDate = jest.fn(
(date: Date, _timezone: string) => new Date(date.getTime()),
);
// Mock uPlot constructor - this needs to be a proper constructor function
function MockUPlot(
_options: unknown,
_data: unknown,
_target: HTMLElement,
): MockUPlotInstance {
return createMockUPlotInstance();
}
// Add static methods to the constructor
MockUPlot.tzDate = mockTzDate;
MockUPlot.paths = mockPaths;
// Export the constructor as default
export default MockUPlot;

View File

@ -0,0 +1,29 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
interface UseSafeNavigateReturn {
safeNavigate: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>;
}
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
console.log(`Mock safeNavigate called with:`, to, options);
},
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,
});

View File

@ -1,5 +1,7 @@
import type { Config } from '@jest/types'; import type { Config } from '@jest/types';
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
const config: Config.InitialOptions = { const config: Config.InitialOptions = {
clearMocks: true, clearMocks: true,
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
@ -10,6 +12,10 @@ const config: Config.InitialOptions = {
moduleNameMapper: { moduleNameMapper: {
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts', '\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts', '\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
}, },
globals: { globals: {
extensionsToTreatAsEsm: ['.ts'], extensionsToTreatAsEsm: ['.ts'],

View File

@ -19,20 +19,6 @@ beforeAll(() => {
}); });
}); });
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('react-dnd', () => ({ jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]), useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]), useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),

View File

@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import APIError from 'types/api/error'; import APIError from 'types/api/error';
import ErrorModal from './ErrorModal'; import ErrorModal from './ErrorModal';
@ -56,9 +56,8 @@ describe('ErrorModal Component', () => {
// Click the close button // Click the close button
const closeButton = screen.getByTestId('close-button'); const closeButton = screen.getByTestId('close-button');
act(() => { const user = userEvent.setup({ pointerEventsCheck: 0 });
fireEvent.click(closeButton); await user.click(closeButton);
});
// Check if onClose was called // Check if onClose was called
expect(onCloseMock).toHaveBeenCalledTimes(1); expect(onCloseMock).toHaveBeenCalledTimes(1);
@ -149,9 +148,8 @@ it('should open the modal when the trigger component is clicked', async () => {
// Click the trigger component // Click the trigger component
const triggerButton = screen.getByText('Open Error Modal'); const triggerButton = screen.getByText('Open Error Modal');
act(() => { const user = userEvent.setup({ pointerEventsCheck: 0 });
fireEvent.click(triggerButton); await user.click(triggerButton);
});
// Check if the modal is displayed // Check if the modal is displayed
expect(screen.getByText('An error occurred')).toBeInTheDocument(); expect(screen.getByText('An error occurred')).toBeInTheDocument();
@ -170,18 +168,15 @@ it('should close the modal when the onCancel event is triggered', async () => {
// Click the trigger component // Click the trigger component
const triggerButton = screen.getByText('error'); const triggerButton = screen.getByText('error');
act(() => { const user = userEvent.setup({ pointerEventsCheck: 0 });
fireEvent.click(triggerButton); await user.click(triggerButton);
});
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('An error occurred')).toBeInTheDocument(); expect(screen.getByText('An error occurred')).toBeInTheDocument();
}); });
// Trigger the onCancel event // Trigger the onCancel event
act(() => { await user.click(screen.getByTestId('close-button'));
fireEvent.click(screen.getByTestId('close-button'));
});
// Check if the modal is closed // Check if the modal is closed
expect(onCloseMock).toHaveBeenCalledTimes(1); expect(onCloseMock).toHaveBeenCalledTimes(1);

View File

@ -1,15 +1,6 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import {
act,
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env'; import { ENVIRONMENT } from 'constants/env';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { import {
otherFiltersResponse, otherFiltersResponse,
@ -18,8 +9,7 @@ import {
} from 'mocks-server/__mockdata__/customQuickFilters'; } from 'mocks-server/__mockdata__/customQuickFilters';
import { server } from 'mocks-server/server'; import { server } from 'mocks-server/server';
import { rest } from 'msw'; import { rest } from 'msw';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { USER_ROLES } from 'types/roles';
import QuickFilters from '../QuickFilters'; import QuickFilters from '../QuickFilters';
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types'; import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
@ -29,21 +19,6 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(), useQueryBuilder: jest.fn(),
})); }));
// eslint-disable-next-line sonarjs/no-duplicate-string
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
}));
const userRole = USER_ROLES.ADMIN;
// mock useAppContext
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({ user: { role: userRole } })),
}));
const handleFilterVisibilityChange = jest.fn(); const handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn(); const redirectWithQueryBuilderData = jest.fn();
const putHandler = jest.fn(); const putHandler = jest.fn();
@ -78,11 +53,10 @@ const setupServer = (): void => {
putHandler(await req.json()); putHandler(await req.json());
return res(ctx.status(200), ctx.json({})); return res(ctx.status(200), ctx.json({}));
}), }),
rest.get(quickFiltersAttributeValuesURL, (_req, res, ctx) =>
rest.get(quickFiltersAttributeValuesURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)), res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
), ),
rest.get(fieldsValuesURL, (req, res, ctx) => rest.get(fieldsValuesURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)), res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
), ),
); );
@ -96,14 +70,12 @@ function TestQuickFilters({
config?: IQuickFiltersConfig[]; config?: IQuickFiltersConfig[];
}): JSX.Element { }): JSX.Element {
return ( return (
<MockQueryClientProvider> <QuickFilters
<QuickFilters source={QuickFiltersSource.EXCEPTIONS}
source={QuickFiltersSource.EXCEPTIONS} config={config}
config={config} handleFilterVisibilityChange={handleFilterVisibilityChange}
handleFilterVisibilityChange={handleFilterVisibilityChange} signal={signal}
signal={signal} />
/>
</MockQueryClientProvider>
); );
} }
@ -118,11 +90,11 @@ beforeAll(() => {
afterEach(() => { afterEach(() => {
server.resetHandlers(); server.resetHandlers();
jest.clearAllMocks();
}); });
afterAll(() => { afterAll(() => {
server.close(); server.close();
cleanup();
}); });
beforeEach(() => { beforeEach(() => {
@ -151,9 +123,13 @@ describe('Quick Filters', () => {
}); });
it('should add filter data to query when checkbox is clicked', async () => { it('should add filter data to query when checkbox is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters />); render(<TestQuickFilters />);
const checkbox = screen.getByText('mq-kafka');
fireEvent.click(checkbox); // Prefer role if possible; if label text isnt wired to input, clicking the label text is OK
const target = await screen.findByText('mq-kafka');
await user.click(target);
await waitFor(() => { await waitFor(() => {
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
@ -182,16 +158,20 @@ describe('Quick Filters', () => {
describe('Quick Filters with custom filters', () => { describe('Quick Filters with custom filters', () => {
it('loads the custom filters correctly', async () => { it('loads the custom filters correctly', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />); render(<TestQuickFilters signal={SIGNAL} />);
expect(screen.getByText('Filters for')).toBeInTheDocument(); expect(screen.getByText('Filters for')).toBeInTheDocument();
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument(); expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
await screen.findByText(FILTER_SERVICE_NAME); await screen.findByText(FILTER_SERVICE_NAME);
const allByText = await screen.findAllByText('otel-demo'); const allByText = await screen.findAllByText('otel-demo');
// since 2 filter collapse are open, there are 2 filter items visible
expect(allByText).toHaveLength(2); expect(allByText).toHaveLength(2);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon); const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
expect(await screen.findByText('Edit quick filters')).toBeInTheDocument(); expect(await screen.findByText('Edit quick filters')).toBeInTheDocument();
@ -207,16 +187,19 @@ describe('Quick Filters with custom filters', () => {
}); });
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => { it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />); render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME); await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon); const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
const otherFilterItem = await screen.findByText(FILTER_K8S_DEPLOYMENT_NAME); const otherFilterItem = await screen.findByText(FILTER_K8S_DEPLOYMENT_NAME);
const addButton = otherFilterItem.parentElement?.querySelector('button'); const addButton = otherFilterItem.parentElement?.querySelector('button');
expect(addButton).not.toBeNull(); expect(addButton).not.toBeNull();
fireEvent.click(addButton as HTMLButtonElement); await user.click(addButton as HTMLButtonElement);
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
await waitFor(() => { await waitFor(() => {
@ -225,17 +208,21 @@ describe('Quick Filters with custom filters', () => {
}); });
it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => { it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />); render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME); await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon); const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
const target = await screen.findByText(FILTER_OS_DESCRIPTION); const target = await screen.findByText(FILTER_OS_DESCRIPTION);
const removeBtn = target.parentElement?.querySelector('button'); const removeBtn = target.parentElement?.querySelector('button');
expect(removeBtn).not.toBeNull(); expect(removeBtn).not.toBeNull();
fireEvent.click(removeBtn as HTMLButtonElement);
await user.click(removeBtn as HTMLButtonElement);
await waitFor(() => { await waitFor(() => {
expect(addedSection).not.toContainElement( expect(addedSection).not.toContainElement(
@ -250,17 +237,20 @@ describe('Quick Filters with custom filters', () => {
}); });
it('restores original filter state on Discard', async () => { it('restores original filter state on Discard', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />); render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME); await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon); const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
const target = await screen.findByText(FILTER_OS_DESCRIPTION); const target = await screen.findByText(FILTER_OS_DESCRIPTION);
const removeBtn = target.parentElement?.querySelector('button'); const removeBtn = target.parentElement?.querySelector('button');
expect(removeBtn).not.toBeNull(); expect(removeBtn).not.toBeNull();
fireEvent.click(removeBtn as HTMLButtonElement); await user.click(removeBtn as HTMLButtonElement);
const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!; const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!;
await waitFor(() => { await waitFor(() => {
@ -272,7 +262,7 @@ describe('Quick Filters with custom filters', () => {
); );
}); });
fireEvent.click(screen.getByText(DISCARD_TEXT)); await user.click(screen.getByRole('button', { name: DISCARD_TEXT }));
await waitFor(() => { await waitFor(() => {
expect(addedSection).toContainElement( expect(addedSection).toContainElement(
@ -285,18 +275,21 @@ describe('Quick Filters with custom filters', () => {
}); });
it('saves the updated filters by calling PUT with correct payload', async () => { it('saves the updated filters by calling PUT with correct payload', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />); render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME); await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon); const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
const target = await screen.findByText(FILTER_OS_DESCRIPTION); const target = await screen.findByText(FILTER_OS_DESCRIPTION);
const removeBtn = target.parentElement?.querySelector('button'); const removeBtn = target.parentElement?.querySelector('button');
expect(removeBtn).not.toBeNull(); expect(removeBtn).not.toBeNull();
fireEvent.click(removeBtn as HTMLButtonElement); await user.click(removeBtn as HTMLButtonElement);
fireEvent.click(screen.getByText(SAVE_CHANGES_TEXT)); await user.click(screen.getByRole('button', { name: SAVE_CHANGES_TEXT }));
await waitFor(() => { await waitFor(() => {
expect(putHandler).toHaveBeenCalled(); expect(putHandler).toHaveBeenCalled();
@ -311,31 +304,36 @@ describe('Quick Filters with custom filters', () => {
expect(requestBody.signal).toBe(SIGNAL); expect(requestBody.signal).toBe(SIGNAL);
}); });
// render duration filter
it('should render duration slider for duration_nono filter', async () => { it('should render duration slider for duration_nono filter', async () => {
// Set up fake timers **before rendering** // Use fake timers only in this test (for debounce), and wire them to userEvent
jest.useFakeTimers(); jest.useFakeTimers();
const user = userEvent.setup({
advanceTimers: (ms) => jest.advanceTimersByTime(ms),
pointerEventsCheck: 0,
});
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />); const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME); await screen.findByText(FILTER_SERVICE_NAME);
expect(screen.getByText('Duration')).toBeInTheDocument(); expect(screen.getByText('Duration')).toBeInTheDocument();
// click to open the duration filter // Open the duration section (use role if its a button/collapse)
fireEvent.click(screen.getByText('Duration')); await user.click(screen.getByText('Duration'));
const minDuration = getByTestId('min-input') as HTMLInputElement; const minDuration = getByTestId('min-input') as HTMLInputElement;
const maxDuration = getByTestId('max-input') as HTMLInputElement; const maxDuration = getByTestId('max-input') as HTMLInputElement;
expect(minDuration).toHaveValue(null); expect(minDuration).toHaveValue(null);
expect(minDuration).toHaveProperty('placeholder', '0'); expect(minDuration).toHaveProperty('placeholder', '0');
expect(maxDuration).toHaveValue(null); expect(maxDuration).toHaveValue(null);
expect(maxDuration).toHaveProperty('placeholder', '100000000'); expect(maxDuration).toHaveProperty('placeholder', '100000000');
await act(async () => { // Type values and advance debounce
// set values await user.clear(minDuration);
fireEvent.change(minDuration, { target: { value: '10000' } }); await user.type(minDuration, '10000');
fireEvent.change(maxDuration, { target: { value: '20000' } }); await user.clear(maxDuration);
jest.advanceTimersByTime(2000); await user.type(maxDuration, '20000');
}); jest.advanceTimersByTime(2000);
await waitFor(() => { await waitFor(() => {
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -363,6 +361,6 @@ describe('Quick Filters with custom filters', () => {
); );
}); });
jest.useRealTimers(); // Clean up jest.useRealTimers();
}); });
}); });

View File

@ -22,6 +22,8 @@ jest.mock('react-router-dom', () => ({
describe('Alert Channels Settings List page', () => { describe('Alert Channels Settings List page', () => {
beforeEach(async () => { beforeEach(async () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2023-10-20'));
render(<AlertChannels />); render(<AlertChannels />);
await waitFor(() => await waitFor(() =>
expect(screen.getByText('sending_channels_note')).toBeInTheDocument(), expect(screen.getByText('sending_channels_note')).toBeInTheDocument(),
@ -29,6 +31,7 @@ describe('Alert Channels Settings List page', () => {
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
jest.useRealTimers();
}); });
describe('Should display the Alert Channels page properly', () => { describe('Should display the Alert Channels page properly', () => {
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => { it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {

View File

@ -28,6 +28,7 @@ jest.mock('react-router-dom', () => ({
describe('Alert Channels Settings List page (Normal User)', () => { describe('Alert Channels Settings List page (Normal User)', () => {
beforeEach(async () => { beforeEach(async () => {
jest.useFakeTimers();
render(<AlertChannels />); render(<AlertChannels />);
await waitFor(() => await waitFor(() =>
expect(screen.getByText('sending_channels_note')).toBeInTheDocument(), expect(screen.getByText('sending_channels_note')).toBeInTheDocument(),
@ -35,6 +36,7 @@ describe('Alert Channels Settings List page (Normal User)', () => {
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
jest.useRealTimers();
}); });
describe('Should display the Alert Channels page properly', () => { describe('Should display the Alert Channels page properly', () => {
it('Should check if "The alerts will be sent to all the configured channels." is visible ', async () => { it('Should check if "The alerts will be sent to all the configured channels." is visible ', async () => {

View File

@ -9,22 +9,6 @@ import { getFormattedDate } from 'utils/timeUtils';
import BillingContainer from './BillingContainer'; import BillingContainer from './BillingContainer';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver = window.ResizeObserver =
window.ResizeObserver || window.ResizeObserver ||
jest.fn().mockImplementation(() => ({ jest.fn().mockImplementation(() => ({
@ -67,78 +51,103 @@ describe('BillingContainer', () => {
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
}); });
test('OnTrail', async () => { describe('Trial scenarios', () => {
await act(async () => { beforeEach(() => {
render(<BillingContainer />, undefined, undefined, { jest.useFakeTimers();
trialInfo: licensesSuccessResponse.data, jest.setSystemTime(new Date('2023-10-20'));
});
afterEach(() => {
jest.useRealTimers();
});
test('OnTrail', async () => {
// Pin "now" so trial end (20 Oct 2023) is tomorrow => "1 days_remaining"
render(
<BillingContainer />,
{},
{ appContextOverrides: { trialInfo: licensesSuccessResponse.data } },
);
// If the component schedules any setTimeout on mount, flush them:
jest.runOnlyPendingTimers();
expect(await screen.findByText('Free Trial')).toBeInTheDocument();
expect(await screen.findByText('billing')).toBeInTheDocument();
expect(await screen.findByText(/\$0/i)).toBeInTheDocument();
expect(
await screen.findByText(
/You are in free trial period. Your free trial will end on 20 Oct 2023/i,
),
).toBeInTheDocument();
expect(await screen.findByText(/1 days_remaining/i)).toBeInTheDocument();
const upgradeButtons = await screen.findAllByRole('button', {
name: /upgrade_plan/i,
}); });
expect(upgradeButtons).toHaveLength(2);
expect(upgradeButtons[1]).toBeInTheDocument();
expect(await screen.findByText(/checkout_plans/i)).toBeInTheDocument();
expect(
await screen.findByRole('link', { name: /here/i }),
).toBeInTheDocument();
}); });
const freeTrailText = await screen.findByText('Free Trial'); test('OnTrail but trialConvertedToSubscription', async () => {
expect(freeTrailText).toBeInTheDocument(); await act(async () => {
render(
const currentBill = await screen.findByText('billing'); <BillingContainer />,
expect(currentBill).toBeInTheDocument(); {},
{
const dollar0 = await screen.findByText(/\$0/i); appContextOverrides: {
expect(dollar0).toBeInTheDocument(); trialInfo: trialConvertedToSubscriptionResponse.data,
const onTrail = await screen.findByText( },
/You are in free trial period. Your free trial will end on 20 Oct 2023/i, },
); );
expect(onTrail).toBeInTheDocument();
const numberOfDayRemaining = await screen.findByText(/1 days_remaining/i);
expect(numberOfDayRemaining).toBeInTheDocument();
const upgradeButton = await screen.findAllByRole('button', {
name: /upgrade_plan/i,
});
expect(upgradeButton[1]).toBeInTheDocument();
expect(upgradeButton.length).toBe(2);
const checkPaidPlan = await screen.findByText(/checkout_plans/i);
expect(checkPaidPlan).toBeInTheDocument();
const link = await screen.findByRole('link', { name: /here/i });
expect(link).toBeInTheDocument();
});
test('OnTrail but trialConvertedToSubscription', async () => {
await act(async () => {
render(<BillingContainer />, undefined, undefined, {
trialInfo: trialConvertedToSubscriptionResponse.data,
}); });
const currentBill = await screen.findByText('billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
expect(dollar0).toBeInTheDocument();
const onTrail = await screen.findByText(
/You are in free trial period. Your free trial will end on 20 Oct 2023/i,
);
expect(onTrail).toBeInTheDocument();
const receivedCardDetails = await screen.findByText(
/card_details_recieved_and_billing_info/i,
);
expect(receivedCardDetails).toBeInTheDocument();
const manageBillingButton = await screen.findByRole('button', {
name: /manage_billing/i,
});
expect(manageBillingButton).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText(
/1 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
}); });
const currentBill = await screen.findByText('billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
expect(dollar0).toBeInTheDocument();
const onTrail = await screen.findByText(
/You are in free trial period. Your free trial will end on 20 Oct 2023/i,
);
expect(onTrail).toBeInTheDocument();
const receivedCardDetails = await screen.findByText(
/card_details_recieved_and_billing_info/i,
);
expect(receivedCardDetails).toBeInTheDocument();
const manageBillingButton = await screen.findByRole('button', {
name: /manage_billing/i,
});
expect(manageBillingButton).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText(
/1 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
}); });
test('Not on ontrail', async () => { test('Not on ontrail', async () => {
const { findByText } = render(<BillingContainer />, undefined, undefined, { const { findByText } = render(
trialInfo: notOfTrailResponse.data, <BillingContainer />,
}); {},
{
appContextOverrides: {
trialInfo: notOfTrailResponse.data,
},
},
);
const billingPeriodText = `Your current billing period is from ${getFormattedDate( const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart, billingSuccessResponse.data.billingPeriodStart,

View File

@ -1,7 +1,6 @@
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions'; import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
import CreateAlertPage from 'pages/CreateAlert'; import CreateAlertPage from 'pages/CreateAlert';
import { MemoryRouter, Route } from 'react-router-dom';
import { act, fireEvent, render } from 'tests/test-utils'; import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
@ -14,20 +13,6 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('hooks/useSafeNavigate', () => ({ jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({ useSafeNavigate: (): any => ({
safeNavigate: jest.fn(), safeNavigate: jest.fn(),
@ -84,11 +69,11 @@ describe('Alert rule documentation redirection', () => {
beforeEach(() => { beforeEach(() => {
act(() => { act(() => {
renderResult = render( renderResult = render(
<MemoryRouter initialEntries={['/alerts/new']}> <CreateAlertPage />,
<Route path={ROUTES.ALERTS_NEW}> {},
<CreateAlertPage /> {
</Route> initialRoute: ROUTES.ALERTS_NEW,
</MemoryRouter>, },
); );
}); });
}); });

View File

@ -15,20 +15,6 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver = window.ResizeObserver =
window.ResizeObserver || window.ResizeObserver ||
jest.fn().mockImplementation(() => ({ jest.fn().mockImplementation(() => ({

View File

@ -30,20 +30,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// Mock data // Mock data
const mockProps: WidgetGraphComponentProps = { const mockProps: WidgetGraphComponentProps = {
widget: { widget: {

View File

@ -33,19 +33,6 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
const queryClient = new QueryClient(); const queryClient = new QueryClient();
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'), ...jest.requireActual('react-redux'),
useSelector: (): any => ({ useSelector: (): any => ({

View File

@ -3,20 +3,6 @@ import { render, screen } from '@testing-library/react';
import HostsListTable from '../HostsListTable'; import HostsListTable from '../HostsListTable';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container'; const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
describe('HostsListTable', () => { describe('HostsListTable', () => {

View File

@ -4,20 +4,6 @@ import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils';
const PROGRESS_BAR_CLASS = '.progress-bar'; const PROGRESS_BAR_CLASS = '.progress-bar';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('InfraMonitoringHosts utils', () => { describe('InfraMonitoringHosts utils', () => {
describe('formatDataForTable', () => { describe('formatDataForTable', () => {
it('should format host data correctly', () => { it('should format host data correctly', () => {

View File

@ -44,20 +44,6 @@ const verifyEntityLogsPayload = ({
return queryData; return queryData;
}; };
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock( jest.mock(
'components/OverlayScrollbar/OverlayScrollbar', 'components/OverlayScrollbar/OverlayScrollbar',
() => () =>

View File

@ -4,14 +4,8 @@ import setupCommonMocks from '../../commonMocks';
setupCommonMocks(); setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import JobDetails from 'container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails'; import JobDetails from 'container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails';
import { QueryClient, QueryClientProvider } from 'react-query'; import { fireEvent, render, screen } from 'tests/test-utils';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('JobDetails', () => { describe('JobDetails', () => {
const mockJob = { const mockJob = {
@ -24,13 +18,7 @@ describe('JobDetails', () => {
it('should render modal with relevant metadata', () => { it('should render modal with relevant metadata', () => {
render( render(
<QueryClientProvider client={queryClient}> <JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
); );
const jobNameElements = screen.getAllByText('test-job'); const jobNameElements = screen.getAllByText('test-job');
@ -44,13 +32,7 @@ describe('JobDetails', () => {
it('should render modal with 4 tabs', () => { it('should render modal with 4 tabs', () => {
render( render(
<QueryClientProvider client={queryClient}> <JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
); );
const metricsTab = screen.getByText('Metrics'); const metricsTab = screen.getByText('Metrics');
@ -68,13 +50,7 @@ describe('JobDetails', () => {
it('default tab should be metrics', () => { it('default tab should be metrics', () => {
render( render(
<QueryClientProvider client={queryClient}> <JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
); );
const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@ -83,13 +59,7 @@ describe('JobDetails', () => {
it('should switch to events tab when events tab is clicked', () => { it('should switch to events tab when events tab is clicked', () => {
render( render(
<QueryClientProvider client={queryClient}> <JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
); );
const eventsTab = screen.getByRole('radio', { name: 'Events' }); const eventsTab = screen.getByRole('radio', { name: 'Events' });
@ -100,13 +70,7 @@ describe('JobDetails', () => {
it('should close modal when close button is clicked', () => { it('should close modal when close button is clicked', () => {
render( render(
<QueryClientProvider client={queryClient}> <JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
); );
const closeButton = screen.getByRole('button', { name: 'Close' }); const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@ -56,19 +56,6 @@ const setupCommonMocks = (): void => {
useNavigationType: (): any => 'PUSH', useNavigationType: (): any => 'PUSH',
})); }));
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => ({
set: jest.fn(),
delete: jest.fn(),
get: jest.fn(),
has: jest.fn(),
entries: jest.fn(() => []),
append: jest.fn(),
toString: jest.fn(() => ''),
})),
}));
jest.mock('lib/getMinMax', () => ({ jest.mock('lib/getMinMax', () => ({
__esModule: true, __esModule: true,
default: jest.fn().mockImplementation(() => ({ default: jest.fn().mockImplementation(() => ({

View File

@ -32,20 +32,6 @@ import {
// Mock the useContextLogData hook // Mock the useContextLogData hook
const mockHandleRunQuery = jest.fn(); const mockHandleRunQuery = jest.fn();
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('container/OptionsMenu', () => ({ jest.mock('container/OptionsMenu', () => ({
useOptionsMenu: (): any => ({ useOptionsMenu: (): any => ({
options: { options: {

View File

@ -73,20 +73,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
__esModule: true, __esModule: true,
useGetExplorerQueryRange: jest.fn(), useGetExplorerQueryRange: jest.fn(),

View File

@ -7,18 +7,16 @@ import { logsresponse } from 'mocks-server/__mockdata__/query_range';
import { server } from 'mocks-server/server'; import { server } from 'mocks-server/server';
import { rest } from 'msw'; import { rest } from 'msw';
import LogsExplorer from 'pages/LogsExplorer'; import LogsExplorer from 'pages/LogsExplorer';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import React from 'react'; import React from 'react';
import { I18nextProvider } from 'react-i18next';
import { MemoryRouter } from 'react-router-dom-v5-compat';
import { VirtuosoMockContext } from 'react-virtuoso'; import { VirtuosoMockContext } from 'react-virtuoso';
import i18n from 'ReactI18';
import { import {
act, act,
AllTheProviders,
fireEvent, fireEvent,
render, render,
RenderResult, RenderResult,
screen, screen,
userEvent,
waitFor, waitFor,
} from 'tests/test-utils'; } from 'tests/test-utils';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
@ -91,20 +89,6 @@ getStateSpy.mockImplementation(() => {
return originalState; return originalState;
}); });
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: (): { search: string; pathname: string } => ({ useLocation: (): { search: string; pathname: string } => ({
@ -277,9 +261,7 @@ describe.skip('LogsExplorerViews Pagination', () => {
act(() => { act(() => {
renderResult = render( renderResult = render(
<VirtuosoMockContext.Provider value={{ viewportHeight, itemHeight }}> <VirtuosoMockContext.Provider value={{ viewportHeight, itemHeight }}>
<I18nextProvider i18n={i18n}> <LogsExplorer />
<LogsExplorer />
</I18nextProvider>
</VirtuosoMockContext.Provider>, </VirtuosoMockContext.Provider>,
); );
}); });
@ -453,13 +435,14 @@ function LogsExplorerWithMockContext({
); );
return ( return (
<MemoryRouter> <AllTheProviders
<QueryBuilderContext.Provider value={contextValue as any}> queryBuilderOverrides={contextValue as any}
<VirtuosoMockContext.Provider value={virtuosoContextValue}> initialRoute="/logs"
<LogsExplorer /> >
</VirtuosoMockContext.Provider> <VirtuosoMockContext.Provider value={virtuosoContextValue}>
</QueryBuilderContext.Provider> <LogsExplorer />
</MemoryRouter> </VirtuosoMockContext.Provider>
</AllTheProviders>
); );
} }
@ -536,13 +519,12 @@ describe('Logs Explorer -> stage and run query', () => {
const initialEnd = initialPayload.end; const initialEnd = initialPayload.end;
// Click the Stage & Run Query button // Click the Stage & Run Query button
await act(async () => { const user = userEvent.setup({ pointerEventsCheck: 0 });
fireEvent.click( await user.click(
screen.getByRole('button', { screen.getByRole('button', {
name: /stage & run query/i, name: /stage & run query/i,
}), }),
); );
});
// Wait for additional API calls to be made after clicking Stage & Run Query // Wait for additional API calls to be made after clicking Stage & Run Query
await waitFor( await waitFor(

View File

@ -33,20 +33,6 @@ const lodsQueryServerRequest = (): void =>
), ),
); );
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// mocking the graph components in this test as this should be handled separately // mocking the graph components in this test as this should be handled separately
jest.mock( jest.mock(
'container/TimeSeriesView/TimeSeriesView', 'container/TimeSeriesView/TimeSeriesView',

View File

@ -18,10 +18,6 @@ const MOCK_SEARCH_PARAMS =
'?graphType=list&widgetId=36a7b342-c642-4b92-abe4-cb833a244786&compositeQuery=%7B%22id%22%3A%22b325ac88-5e75-4117-a38c-1a2a7caf8115%22%2C%22builder%22%3A%7B%22queryData%22%3A%5B%7B%22dataSource%22%3A%22logs%22%2C%22queryName%22%3A%22A%22%2C%22aggregateOperator%22%3A%22noop%22%2C%22aggregateAttribute%22%3A%7B%22id%22%3A%22------%22%2C%22dataType%22%3A%22%22%2C%22key%22%3A%22%22%2C%22isColumn%22%3Afalse%2C%22type%22%3A%22%22%2C%22isJSON%22%3Afalse%7D%2C%22timeAggregation%22%3A%22rate%22%2C%22spaceAggregation%22%3A%22sum%22%2C%22functions%22%3A%5B%5D%2C%22filters%22%3A%7B%22items%22%3A%5B%5D%2C%22op%22%3A%22AND%22%7D%2C%22expression%22%3A%22A%22%2C%22disabled%22%3Afalse%2C%22stepInterval%22%3A60%2C%22having%22%3A%5B%5D%2C%22limit%22%3Anull%2C%22orderBy%22%3A%5B%7B%22columnName%22%3A%22timestamp%22%2C%22order%22%3A%22desc%22%7D%5D%2C%22groupBy%22%3A%5B%5D%2C%22legend%22%3A%22%22%2C%22reduceTo%22%3A%22avg%22%2C%22offset%22%3A0%2C%22pageSize%22%3A100%7D%5D%2C%22queryFormulas%22%3A%5B%5D%7D%2C%22clickhouse_sql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22query%22%3A%22%22%7D%5D%2C%22promql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22query%22%3A%22%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%7D%5D%2C%22queryType%22%3A%22builder%22%7D&relativeTime=30m&options=%7B%22selectColumns%22%3A%5B%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22list%22%2C%22fontSize%22%3A%22small%22%7D'; '?graphType=list&widgetId=36a7b342-c642-4b92-abe4-cb833a244786&compositeQuery=%7B%22id%22%3A%22b325ac88-5e75-4117-a38c-1a2a7caf8115%22%2C%22builder%22%3A%7B%22queryData%22%3A%5B%7B%22dataSource%22%3A%22logs%22%2C%22queryName%22%3A%22A%22%2C%22aggregateOperator%22%3A%22noop%22%2C%22aggregateAttribute%22%3A%7B%22id%22%3A%22------%22%2C%22dataType%22%3A%22%22%2C%22key%22%3A%22%22%2C%22isColumn%22%3Afalse%2C%22type%22%3A%22%22%2C%22isJSON%22%3Afalse%7D%2C%22timeAggregation%22%3A%22rate%22%2C%22spaceAggregation%22%3A%22sum%22%2C%22functions%22%3A%5B%5D%2C%22filters%22%3A%7B%22items%22%3A%5B%5D%2C%22op%22%3A%22AND%22%7D%2C%22expression%22%3A%22A%22%2C%22disabled%22%3Afalse%2C%22stepInterval%22%3A60%2C%22having%22%3A%5B%5D%2C%22limit%22%3Anull%2C%22orderBy%22%3A%5B%7B%22columnName%22%3A%22timestamp%22%2C%22order%22%3A%22desc%22%7D%5D%2C%22groupBy%22%3A%5B%5D%2C%22legend%22%3A%22%22%2C%22reduceTo%22%3A%22avg%22%2C%22offset%22%3A0%2C%22pageSize%22%3A100%7D%5D%2C%22queryFormulas%22%3A%5B%5D%7D%2C%22clickhouse_sql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22query%22%3A%22%22%7D%5D%2C%22promql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22query%22%3A%22%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%7D%5D%2C%22queryType%22%3A%22builder%22%7D&relativeTime=30m&options=%7B%22selectColumns%22%3A%5B%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22list%22%2C%22fontSize%22%3A%22small%22%7D';
// Mocks // Mocks
jest.mock('uplot', () => ({
paths: { spline: jest.fn(), bars: jest.fn() },
default: jest.fn(() => ({ paths: { spline: jest.fn(), bars: jest.fn() } })),
}));
jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({ jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({
__esModule: true, __esModule: true,

View File

@ -68,19 +68,6 @@ jest.mock('hooks/useNotifications', () => ({
}, },
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'), ...jest.requireActual('react-redux'),
useSelector: (): any => ({ useSelector: (): any => ({

View File

@ -15,12 +15,6 @@ import {
TimeAggregationOptions, TimeAggregationOptions,
} from '../types'; } from '../types';
jest.mock('uplot', () =>
jest.fn().mockImplementation(() => ({
destroy: jest.fn(),
})),
);
const mockResizeObserver = jest.fn(); const mockResizeObserver = jest.fn();
mockResizeObserver.mockImplementation(() => ({ mockResizeObserver.mockImplementation(() => ({
observe: (): void => undefined, observe: (): void => undefined,

View File

@ -76,12 +76,6 @@ jest
isLoading: false, isLoading: false,
} as any); } as any);
jest.mock('uplot', () =>
jest.fn().mockImplementation(() => ({
destroy: jest.fn(),
})),
);
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({ useLocation: (): { pathname: string } => ({

View File

@ -1,6 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import * as useSafeNavigate from 'hooks/useSafeNavigate';
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover'; import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
@ -24,9 +23,11 @@ const mockAlerts = [mockAlert1, mockAlert2];
const mockDashboards = [mockDashboard1, mockDashboard2]; const mockDashboards = [mockDashboard1, mockDashboard2];
const mockSafeNavigate = jest.fn(); const mockSafeNavigate = jest.fn();
jest.spyOn(useSafeNavigate, 'useSafeNavigate').mockReturnValue({ jest.mock('hooks/useSafeNavigate', () => ({
safeNavigate: mockSafeNavigate, useSafeNavigate: (): any => ({
}); safeNavigate: mockSafeNavigate,
}),
}));
const mockSetQuery = jest.fn(); const mockSetQuery = jest.fn();
const mockUrlQuery = { const mockUrlQuery = {

View File

@ -11,19 +11,6 @@ import store from 'store';
import Summary from '../Summary'; import Summary from '../Summary';
import { TreemapViewType } from '../types'; import { TreemapViewType } from '../types';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('d3-hierarchy', () => ({ jest.mock('d3-hierarchy', () => ({
stratify: jest.fn().mockReturnValue({ stratify: jest.fn().mockReturnValue({
id: jest.fn().mockReturnValue({ id: jest.fn().mockReturnValue({

View File

@ -10,20 +10,6 @@ import store from 'store';
import ChangeHistory from '../index'; import ChangeHistory from '../index';
import { pipelineData, pipelineDataHistory } from './testUtils'; import { pipelineData, pipelineDataHistory } from './testUtils';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {

View File

@ -5,20 +5,6 @@ import { PipelineData } from 'types/api/pipeline/def';
import { pipelineMockData } from '../mocks/pipeline'; import { pipelineMockData } from '../mocks/pipeline';
import AddNewPipeline from '../PipelineListsView/AddNewPipeline'; import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
export function matchMedia(): void { export function matchMedia(): void {
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,

View File

@ -11,20 +11,6 @@ jest.mock('../PipelineListsView/AddNewProcessor/config', () => ({
DEFAULT_PROCESSOR_TYPE: 'json_parser', DEFAULT_PROCESSOR_TYPE: 'json_parser',
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const selectedProcessorData = { const selectedProcessorData = {
id: '1', id: '1',
orderId: 1, orderId: 1,

View File

@ -6,20 +6,6 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18'; import i18n from 'ReactI18';
import store from 'store'; import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
it('should render DeleteAction section', () => { it('should render DeleteAction section', () => {
const { asFragment } = render( const { asFragment } = render(

View File

@ -6,20 +6,6 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18'; import i18n from 'ReactI18';
import store from 'store'; import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
it('should render DragAction section', () => { it('should render DragAction section', () => {
const { asFragment } = render( const { asFragment } = render(

View File

@ -6,20 +6,6 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18'; import i18n from 'ReactI18';
import store from 'store'; import store from 'store';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
it('should render EditAction section', () => { it('should render EditAction section', () => {
const { asFragment } = render( const { asFragment } = render(

View File

@ -8,20 +8,6 @@ import store from 'store';
import { pipelineMockData } from '../mocks/pipeline'; import { pipelineMockData } from '../mocks/pipeline';
import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions'; import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
it('should render PipelineActions section', () => { it('should render PipelineActions section', () => {
const { asFragment } = render( const { asFragment } = render(

View File

@ -3,20 +3,6 @@ import { render } from 'tests/test-utils';
import { pipelineMockData } from '../mocks/pipeline'; import { pipelineMockData } from '../mocks/pipeline';
import PipelineExpandView from '../PipelineListsView/PipelineExpandView'; import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => { beforeAll(() => {
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,

View File

@ -6,20 +6,6 @@ import { findByText, fireEvent, render, waitFor } from 'tests/test-utils';
import { pipelineApiResponseMockData } from '../mocks/pipeline'; import { pipelineApiResponseMockData } from '../mocks/pipeline';
import PipelineListsView from '../PipelineListsView'; import PipelineListsView from '../PipelineListsView';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// Mock useUrlQuery hook // Mock useUrlQuery hook
const mockUrlQuery = { const mockUrlQuery = {
get: jest.fn(), get: jest.fn(),

View File

@ -4,20 +4,6 @@ import { v4 } from 'uuid';
import PipelinePageLayout from '../Layouts/Pipeline'; import PipelinePageLayout from '../Layouts/Pipeline';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
beforeAll(() => { beforeAll(() => {
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,

View File

@ -7,20 +7,6 @@ import store from 'store';
import TagInput from '../components/TagInput'; import TagInput from '../components/TagInput';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('Pipeline Page', () => { describe('Pipeline Page', () => {
it('should render TagInput section', () => { it('should render TagInput section', () => {
const { asFragment } = render( const { asFragment } = render(

View File

@ -1,24 +1,11 @@
import { render } from '@testing-library/react';
import Tags from 'container/PipelinePage/PipelineListsView/TableComponents/Tags'; import Tags from 'container/PipelinePage/PipelineListsView/TableComponents/Tags';
import { I18nextProvider } from 'react-i18next'; import { render } from 'tests/test-utils';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
const tags = ['server', 'app']; const tags = ['server', 'app'];
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
it('should render Tags section', () => { it('should render Tags section', () => {
const { asFragment } = render( const { asFragment } = render(<Tags tags={tags} />);
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<Tags tags={tags} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
}); });

View File

@ -11,20 +11,6 @@ import {
getTableColumn, getTableColumn,
} from '../PipelineListsView/utils'; } from '../PipelineListsView/utils';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('Utils testing of Pipeline Page', () => { describe('Utils testing of Pipeline Page', () => {
test('it should be check form field of add pipeline', () => { test('it should be check form field of add pipeline', () => {
expect(pipelineFields.length).toBe(3); expect(pipelineFields.length).toBe(3);

View File

@ -6,7 +6,7 @@ import { PlannedDowntime } from '../PlannedDowntime';
describe('PlannedDowntime Component', () => { describe('PlannedDowntime Component', () => {
it('renders the PlannedDowntime component properly', () => { it('renders the PlannedDowntime component properly', () => {
render(<PlannedDowntime />, {}, 'ADMIN'); render(<PlannedDowntime />, {}, { role: 'ADMIN' });
// Check if title is rendered // Check if title is rendered
expect(screen.getByText('Planned Downtime')).toBeInTheDocument(); expect(screen.getByText('Planned Downtime')).toBeInTheDocument();
@ -30,7 +30,7 @@ describe('PlannedDowntime Component', () => {
}); });
it('disables the "New downtime" button for users with VIEWER role', () => { it('disables the "New downtime" button for users with VIEWER role', () => {
render(<PlannedDowntime />, {}, USER_ROLES.VIEWER); render(<PlannedDowntime />, {}, { role: USER_ROLES.VIEWER });
// Check if "New downtime" button is disabled for VIEWER // Check if "New downtime" button is disabled for VIEWER
const newDowntimeButton = screen.getByRole('button', { const newDowntimeButton = screen.getByRole('button', {

View File

@ -1,7 +1,5 @@
import { fireEvent, screen } from '@testing-library/react';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { render } from 'tests/test-utils'; import { fireEvent, render, screen } from 'tests/test-utils';
import { Span } from 'types/api/trace/getTraceV2'; import { Span } from 'types/api/trace/getTraceV2';
import { SpanDuration } from '../Success'; import { SpanDuration } from '../Success';
@ -15,7 +13,6 @@ const DIMMED_SPAN_CLASS = 'dimmed-span';
const SELECTED_NON_MATCHING_SPAN_CLASS = 'selected-non-matching-span'; const SELECTED_NON_MATCHING_SPAN_CLASS = 'selected-non-matching-span';
// Mock the hooks // Mock the hooks
jest.mock('hooks/useSafeNavigate');
jest.mock('hooks/useUrlQuery'); jest.mock('hooks/useUrlQuery');
jest.mock('@signozhq/badge', () => ({ jest.mock('@signozhq/badge', () => ({
Badge: jest.fn(), Badge: jest.fn(),
@ -52,24 +49,17 @@ const mockTraceMetadata = {
hasMissingSpans: false, hasMissingSpans: false,
}; };
jest.mock('uplot', () => { const mockSafeNavigate = jest.fn();
const paths = {
spline: jest.fn(), jest.mock('hooks/useSafeNavigate', () => ({
bars: jest.fn(), useSafeNavigate: (): any => ({
}; safeNavigate: mockSafeNavigate,
const uplotMock = jest.fn(() => ({ }),
paths, }));
}));
return {
paths,
default: uplotMock,
};
});
describe('SpanDuration', () => { describe('SpanDuration', () => {
const mockSetSelectedSpan = jest.fn(); const mockSetSelectedSpan = jest.fn();
const mockUrlQuerySet = jest.fn(); const mockUrlQuerySet = jest.fn();
const mockSafeNavigate = jest.fn();
const mockUrlQueryGet = jest.fn(); const mockUrlQueryGet = jest.fn();
beforeEach(() => { beforeEach(() => {
@ -81,11 +71,6 @@ describe('SpanDuration', () => {
get: mockUrlQueryGet, get: mockUrlQueryGet,
toString: () => 'spanId=test-span-id', toString: () => 'spanId=test-span-id',
}); });
// Mock safe navigate hook
(useSafeNavigate as jest.Mock).mockReturnValue({
safeNavigate: mockSafeNavigate,
});
}); });
it('updates URL and selected span when clicked', () => { it('updates URL and selected span when clicked', () => {

View File

@ -1,7 +1,10 @@
// src/mocks/server.js // src/mocks/server.js
import { rest } from 'msw';
import { setupServer } from 'msw/node'; import { setupServer } from 'msw/node';
import { handlers } from './handlers'; import { handlers } from './handlers';
// This configures a request mocking server with the given request handlers. // This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers); export const server = setupServer(...handlers);
export { rest };

View File

@ -28,20 +28,6 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// mocking the graph components in this test as this should be handled separately // mocking the graph components in this test as this should be handled separately
jest.mock( jest.mock(
'container/TimeSeriesView/TimeSeriesView', 'container/TimeSeriesView/TimeSeriesView',

View File

@ -35,21 +35,16 @@ jest.mock('container/TraceFlameGraph/index.tsx', () => ({
default: (): JSX.Element => <div>TraceFlameGraph</div>, default: (): JSX.Element => <div>TraceFlameGraph</div>,
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
describe('TraceDetail', () => { describe('TraceDetail', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2023-10-20'));
});
afterEach(() => {
jest.useRealTimers();
});
it('should render tracedetail', async () => { it('should render tracedetail', async () => {
const { findByText, getByText, getAllByText, getByPlaceholderText } = render( const { findByText, getByText, getAllByText, getByPlaceholderText } = render(
<MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}> <MemoryRouter initialEntries={['/trace/000000000000000071dc9b0a338729b4']}>

View File

@ -16,7 +16,6 @@ import {
} from 'mocks-server/__mockdata__/query_range'; } from 'mocks-server/__mockdata__/query_range';
import { server } from 'mocks-server/server'; import { server } from 'mocks-server/server';
import { rest } from 'msw'; import { rest } from 'msw';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { MemoryRouter } from 'react-router-dom-v5-compat'; import { MemoryRouter } from 'react-router-dom-v5-compat';
import { import {
act, act,
@ -97,22 +96,6 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock( jest.mock(
'components/Uplot/Uplot', 'components/Uplot/Uplot',
() => () =>
@ -181,32 +164,31 @@ const checkFilterValues = (
}; };
const renderWithTracesExplorerRouter = ( const renderWithTracesExplorerRouter = (
component: React.ReactNode, component: React.ReactElement,
initialEntries: string[] = [ initialEntries: string[] = [
'/traces-explorer/?panelType=list&selectedExplorerView=list', '/traces-explorer/?panelType=list&selectedExplorerView=list',
], ],
): ReturnType<typeof render> => ): ReturnType<typeof render> =>
render( render(
<MemoryRouter initialEntries={initialEntries}> component,
<QueryBuilderContext.Provider value={{ ...qbProviderValue }}> {},
{component} {
</QueryBuilderContext.Provider> initialRoute: initialEntries[0],
</MemoryRouter>, queryBuilderOverrides: qbProviderValue,
},
); );
describe('TracesExplorer - Filters', () => { describe('TracesExplorer - Filters', () => {
// Initial filter panel rendering // Initial filter panel rendering
// Test the initial state like which filters section are opened, default state of duration slider, etc. // Test the initial state like which filters section are opened, default state of duration slider, etc.
it('should render the Trace filter', async () => { it('should render the Trace filter', async () => {
const { getByText, getAllByText, getByTestId } = render( const {
<MemoryRouter getByText,
initialEntries={[ getAllByText,
`${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/?panelType=list&selectedExplorerView=list`, getByTestId,
]} } = renderWithTracesExplorerRouter(<Filter setOpen={jest.fn()} />, [
> `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/?panelType=list&selectedExplorerView=list`,
<Filter setOpen={jest.fn()} /> ]);
</MemoryRouter>,
);
checkFilterValues(getByText, getAllByText); checkFilterValues(getByText, getAllByText);
@ -249,8 +231,12 @@ describe('TracesExplorer - Filters', () => {
it('filter panel actions', async () => { it('filter panel actions', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<MemoryRouter> <MemoryRouter>
<Filter setOpen={jest.fn()} /> <Filter setOpen={jest.fn()} />,
</MemoryRouter>, </MemoryRouter>,
{},
{
initialRoute: '/traces-explorer/?panelType=list&selectedExplorerView=list',
},
); );
// Check if the section is closed // Check if the section is closed
@ -275,23 +261,21 @@ describe('TracesExplorer - Filters', () => {
}); });
it('checking filters should update the query', async () => { it('checking filters should update the query', async () => {
const { getByText } = renderWithTracesExplorerRouter( const { getByText } = render(
<QueryBuilderContext.Provider <Filter setOpen={jest.fn()} />,
value={ {},
{ {
currentQuery: { queryBuilderOverrides: {
...initialQueriesMap.traces, ...qbProviderValue,
builder: { currentQuery: {
...initialQueriesMap.traces.builder, ...initialQueriesMap.traces,
queryData: [initialQueryBuilderFormValues], builder: {
}, ...initialQueriesMap.traces.builder,
queryData: [initialQueryBuilderFormValues],
}, },
redirectWithQueryBuilderData, },
} as any },
} },
>
<Filter setOpen={jest.fn()} />
</QueryBuilderContext.Provider>,
); );
const okCheckbox = getByText('Ok'); const okCheckbox = getByText('Ok');
@ -343,9 +327,7 @@ describe('TracesExplorer - Filters', () => {
.spyOn(compositeQueryHook, 'useGetCompositeQueryParam') .spyOn(compositeQueryHook, 'useGetCompositeQueryParam')
.mockReturnValue(compositeQuery); .mockReturnValue(compositeQuery);
const { findByText, getByTestId } = renderWithTracesExplorerRouter( const { findByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
<Filter setOpen={jest.fn()} />,
);
// check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer // check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer
expect(await findByText('demo-app')).toBeInTheDocument(); expect(await findByText('demo-app')).toBeInTheDocument();
@ -369,8 +351,12 @@ describe('TracesExplorer - Filters', () => {
}, },
}); });
const { getByText, getAllByText } = renderWithTracesExplorerRouter( const { getByText, getAllByText } = render(
<Filter setOpen={jest.fn()} />, <Filter setOpen={jest.fn()} />,
{},
{
initialRoute: '/traces-explorer/?panelType=list&selectedExplorerView=list',
},
); );
checkFilterValues(getByText, getAllByText); checkFilterValues(getByText, getAllByText);
@ -394,31 +380,28 @@ describe('TracesExplorer - Filters', () => {
}, },
}); });
const { getByText, getAllByText } = renderWithTracesExplorerRouter( const { getByText, getAllByText } = render(<Filter setOpen={jest.fn()} />);
<Filter setOpen={jest.fn()} />,
);
checkFilterValues(getByText, getAllByText); checkFilterValues(getByText, getAllByText);
}); });
it('should clear filter on clear & reset button click', async () => { it('should clear filter on clear & reset button click', async () => {
const { getByText, getByTestId } = renderWithTracesExplorerRouter( const { getByText, getByTestId } = render(
<QueryBuilderContext.Provider <Filter setOpen={jest.fn()} />,
value={ {},
{ {
currentQuery: { initialRoute: '/traces-explorer/?panelType=list&selectedExplorerView=list',
...initialQueriesMap.traces, queryBuilderOverrides: {
builder: { currentQuery: {
...initialQueriesMap.traces.builder, ...initialQueriesMap.traces,
queryData: [initialQueryBuilderFormValues], builder: {
}, ...initialQueriesMap.traces.builder,
queryData: [initialQueryBuilderFormValues],
}, },
redirectWithQueryBuilderData, },
} as any redirectWithQueryBuilderData,
} },
> },
<Filter setOpen={jest.fn()} />
</QueryBuilderContext.Provider>,
); );
// check for the status section content // check for the status section content

View File

@ -55,7 +55,7 @@ describe('WorkspaceLocked', () => {
), ),
); );
render(<WorkspaceLocked />, {}, 'VIEWER'); render(<WorkspaceLocked />, {}, { role: 'VIEWER' });
const updateCreditCardBtn = await screen.queryByRole('button', { const updateCreditCardBtn = await screen.queryByRole('button', {
name: /Continue My Journey/i, name: /Continue My Journey/i,
}); });

View File

@ -0,0 +1,93 @@
### Testing Guide
#### Tech Stack
- React Testing Library (RTL)
- Jest (runner, assertions, mocking)
- MSW (Mock Service Worker) for HTTP
- TypeScript (type-safe tests)
- JSDOM (browser-like env)
#### Unit Testing: What, Why, How
- What: Small, isolated tests for components, hooks, and utilities to verify behavior and edge cases.
- Why: Confidence to refactor, faster feedback than E2E, catches regressions early, documents intended behavior.
- How: Use our test harness with providers, mock external boundaries (APIs, router), assert on visible behavior and accessible roles, not implementation details.
#### Basic Template
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
#### .cursorrules (Highlights)
- Import from `tests/test-utils` only.
- Prefer `userEvent` for real interactions; use `fireEvent` only for low-level events (scroll/resize/setting `scrollTop`).
- Use MSW to mock network calls; large JSON goes in `mocks-server/__mockdata__`.
- Keep tests type-safe (`jest.MockedFunction<T>`, avoid `any`).
- Prefer accessible queries (`getByRole`, `findByRole`) before text and `data-testid`.
- Pin time only when asserting relative dates; avoid global fake timers otherwise.
Repo-specific reasons:
- The harness wires Redux, React Query, i18n, timezone, preferences, so importing from RTL directly bypasses critical providers.
- Some infra deps are globally mocked (e.g., `uplot`) to keep tests fast and stable.
- For virtualization (react-virtuoso), there is no `userEvent` scroll helper; use `fireEvent.scroll` after setting `element.scrollTop`.
#### Example patterns (from `components/QuickFilters/tests/QuickFilters.test.tsx`)
MSW overrides per test:
```ts
server.use(
rest.get(`${ENVIRONMENT.baseURL}/api/v1/orgs/me/filters/logs`, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersListResponse)),
),
rest.put(`${ENVIRONMENT.baseURL}/api/v1/orgs/me/filters`, async (req, res, ctx) => {
// capture payload if needed
return res(ctx.status(200), ctx.json({}));
}),
);
```
Mock hooks minimally at module level:
```ts
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
```
Interact via accessible roles:
```ts
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /save changes/i }));
expect(screen.getByText(/ADDED FILTERS/i)).toBeInTheDocument();
```
Virtualized scroll:
```ts
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
scroller.scrollTop = 500;
fireEvent.scroll(scroller);
```
Routing-dependent behavior:
```ts
render(<Page />, undefined, { initialRoute: '/logs-explorer?panelType=list' });
```
#### Notes
- Global mocks configured in Jest: `uplot``__mocks__/uplotMock.ts`.
- If a test needs custom behavior (e.g., different API response), override with `server.use(...)` locally.

View File

@ -2,18 +2,20 @@
import { render, RenderOptions, RenderResult } from '@testing-library/react'; import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences'; import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { ResourceProvider } from 'hooks/useResourceAttribute'; import { ResourceProvider } from 'hooks/useResourceAttribute';
import { AppContext } from 'providers/App/App'; import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types'; import { IAppContext } from 'providers/App/types';
import { ErrorModalProvider } from 'providers/ErrorModalProvider'; import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider'; import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder'; import {
QueryBuilderContext,
QueryBuilderProvider,
} from 'providers/QueryBuilder';
import TimezoneProvider from 'providers/Timezone'; import TimezoneProvider from 'providers/Timezone';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import store from 'store'; import store from 'store';
@ -23,24 +25,27 @@ import {
LicenseState, LicenseState,
LicenseStatus, LicenseStatus,
} from 'types/api/licensesV3/getActive'; } from 'types/api/licensesV3/getActive';
import { QueryBuilderContextType } from 'types/common/queryBuilder';
import { ROLES, USER_ROLES } from 'types/roles'; import { ROLES, USER_ROLES } from 'types/roles';
// import { MemoryRouter as V5MemoryRouter } from 'react-router-dom-v5-compat';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: false,
}, },
}, },
}); });
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); // jest.useFakeTimers();
jest.setSystemTime(new Date('2023-10-20')); jest.setSystemTime(new Date('2023-10-20'));
}); });
afterEach(() => { afterEach(() => {
queryClient.clear(); queryClient.clear();
jest.useRealTimers(); // jest.useRealTimers();
}); });
const mockStore = configureStore([thunk]); const mockStore = configureStore([thunk]);
@ -85,24 +90,6 @@ jest.mock('react-i18next', () => ({
}), }),
})); }));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: (): any => 'PUSH',
}));
export function getAppContextMock( export function getAppContextMock(
role: string, role: string,
appContextOverrides?: Partial<IAppContext>, appContextOverrides?: Partial<IAppContext>,
@ -253,48 +240,96 @@ export function getAppContextMock(
export function AllTheProviders({ export function AllTheProviders({
children, children,
role, // Accept the role as a prop role,
appContextOverrides, appContextOverrides,
queryBuilderOverrides,
initialRoute,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
role: string; // Define the role prop role?: string;
appContextOverrides: Partial<IAppContext>; appContextOverrides?: Partial<IAppContext>;
queryBuilderOverrides?: Partial<QueryBuilderContextType>;
initialRoute?: string;
}): ReactElement { }): ReactElement {
// Set default values
const roleValue = role || 'ADMIN';
const appContextOverridesValue = appContextOverrides || {};
const initialRouteValue = initialRoute || '/';
const queryBuilderContent = queryBuilderOverrides ? (
<QueryBuilderContext.Provider
value={queryBuilderOverrides as QueryBuilderContextType}
>
{children}
</QueryBuilderContext.Provider>
) : (
<QueryBuilderProvider>{children}</QueryBuilderProvider>
);
return ( return (
<QueryClientProvider client={queryClient}> <MemoryRouter initialEntries={[initialRouteValue]}>
<Provider store={mockStored(role)}> <QueryClientProvider client={queryClient}>
<AppContext.Provider value={getAppContextMock(role, appContextOverrides)}> <Provider store={mockStored(roleValue)}>
<ResourceProvider> <AppContext.Provider
<ErrorModalProvider> value={getAppContextMock(roleValue, appContextOverridesValue)}
<BrowserRouter> >
<ResourceProvider>
<ErrorModalProvider>
<TimezoneProvider> <TimezoneProvider>
<PreferenceContextProvider> <PreferenceContextProvider>
<QueryBuilderProvider>{children}</QueryBuilderProvider> {queryBuilderContent}
</PreferenceContextProvider> </PreferenceContextProvider>
</TimezoneProvider> </TimezoneProvider>
</BrowserRouter> </ErrorModalProvider>
</ErrorModalProvider> </ResourceProvider>
</ResourceProvider> </AppContext.Provider>
</AppContext.Provider> </Provider>
</Provider> </QueryClientProvider>
</QueryClientProvider> </MemoryRouter>
); );
} }
AllTheProviders.defaultProps = {
role: 'ADMIN',
appContextOverrides: {},
queryBuilderOverrides: undefined,
initialRoute: '/',
};
interface ProviderProps {
role?: string;
appContextOverrides?: Partial<IAppContext>;
queryBuilderOverrides?: Partial<QueryBuilderContextType>;
initialRoute?: string;
}
const customRender = ( const customRender = (
ui: ReactElement, ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>, options?: Omit<RenderOptions, 'wrapper'>,
role = 'ADMIN', // Set a default role providerProps: ProviderProps = {},
appContextOverrides?: Partial<IAppContext>, ): RenderResult => {
): RenderResult => const {
render(ui, { role = 'ADMIN',
appContextOverrides = {},
queryBuilderOverrides,
initialRoute = '/',
} = providerProps;
return render(ui, {
wrapper: () => ( wrapper: () => (
<AllTheProviders role={role} appContextOverrides={appContextOverrides || {}}> <AllTheProviders
role={role}
appContextOverrides={appContextOverrides}
queryBuilderOverrides={queryBuilderOverrides}
initialRoute={initialRoute}
>
{ui} {ui}
</AllTheProviders> </AllTheProviders>
), ),
...options, ...options,
}); });
};
export * from '@testing-library/react'; export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
export { customRender as render }; export { customRender as render };