From bced4774bb26232aaaf72475d2f4fc4d4628ae3b Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 18 Sep 2025 12:19:57 +0530 Subject: [PATCH] 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 Co-authored-by: Abhi Kumar Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Co-authored-by: SagarRajput-7 --- frontend/.cursorrules | 484 ++++++++++++++++++ frontend/__mocks__/uplotMock.ts | 51 ++ frontend/__mocks__/useSafeNavigate.ts | 29 ++ frontend/jest.config.ts | 6 + .../tests/DraggableTableRow.test.tsx | 14 - .../components/ErrorModal/ErrorModal.test.tsx | 21 +- .../QuickFilters/tests/QuickFilters.test.tsx | 124 +++-- .../__tests__/AlertChannels.test.tsx | 3 + .../AlertChannelsNormalUser.test.tsx | 2 + .../BillingContainer.test.tsx | 169 +++--- ...AlertRuleDocumentationRedirection.test.tsx | 25 +- ...malyAlertDocumentationRedirection.test.tsx | 14 - .../GridCard/WidgetGraphComponent.test.tsx | 14 - .../__tests__/HostsList.test.tsx | 13 - .../__tests__/HostsListTable.test.tsx | 14 - .../__tests__/utilts.test.tsx | 14 - .../EntityLogs/__tests__/EntityLogs.test.tsx | 14 - .../Jobs/JobDetails/JobDetails.test.tsx | 48 +- .../__tests__/commonMocks.ts | 13 - .../__tests__/ContextLogRenderer.test.tsx | 14 - .../__tests__/LogsExplorerList.test.tsx | 14 - .../tests/LogsExplorerPagination.test.tsx | 52 +- .../tests/LogsExplorerViews.test.tsx | 14 - .../__tests__/LogsPanelComponent.test.tsx | 4 - .../Explorer/__tests__/Explorer.test.tsx | 13 - .../Inspect/__tests__/GraphView.test.tsx | 6 - .../Inspect/__tests__/Inspect.test.tsx | 6 - .../DashboardsAndAlertsPopover.test.tsx | 9 +- .../Summary/__tests__/Summary.test.tsx | 13 - .../tests/ChangeHistory.test.tsx | 14 - .../tests/AddNewPipeline.test.tsx | 14 - .../tests/AddNewProcessor.test.tsx | 14 - .../PipelinePage/tests/DeleteAction.test.tsx | 14 - .../PipelinePage/tests/DragAction.test.tsx | 14 - .../PipelinePage/tests/EditAction.test.tsx | 14 - .../tests/PipelineActions.test.tsx | 14 - .../tests/PipelineExpandView.test.tsx | 14 - .../tests/PipelineListsView.test.tsx | 14 - .../tests/PipelinePageLayout.test.tsx | 14 - .../PipelinePage/tests/TagInput.test.tsx | 14 - .../PipelinePage/tests/Tags.test.tsx | 17 +- .../PipelinePage/tests/utils.test.ts | 14 - .../__test__/PlannedDowntime.test.tsx | 4 +- .../Success/__tests__/SpanDuration.test.tsx | 31 +- frontend/src/mocks-server/server.ts | 3 + .../__tests__/LogsExplorer.test.tsx | 14 - .../TraceDetail/__test__/TraceDetail.test.tsx | 23 +- .../__test__/TracesExplorer.test.tsx | 127 ++--- .../WorkspaceLocked/WorkspaceLocked.test.tsx | 2 +- frontend/src/tests/README.md | 93 ++++ frontend/src/tests/test-utils.tsx | 123 +++-- 51 files changed, 1018 insertions(+), 804 deletions(-) create mode 100644 frontend/.cursorrules create mode 100644 frontend/__mocks__/uplotMock.ts create mode 100644 frontend/__mocks__/useSafeNavigate.ts create mode 100644 frontend/src/tests/README.md diff --git a/frontend/.cursorrules b/frontend/.cursorrules new file mode 100644 index 000000000000..9cfa908ba60f --- /dev/null +++ b/frontend/.cursorrules @@ -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` +- 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(, 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(, 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**: 3–5 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(, 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 { + data: T; + status: number; + message: string; +} + +// Type the mock functions +const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise>>; +const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise>>; + +// 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 = ({ 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(); + + // 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 => ({ + 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": "/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; +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 { + data: T; + status: number; +} + +const mockFetchData = jest.fn() as jest.MockedFunction< + (endpoint: string) => Promise> +>; + +// Usage +mockFetchData('/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; + +const renderTestComponent = (props: Partial = {}): RenderResult => { + const defaultProps: TestComponentProps = { + title: 'Test', + data: [], + onSelect: jest.fn(), + ...props + }; + + return render(); +}; +``` + +### Error Handling with Types +```ts +// ✅ GOOD - Typed error handling +interface ApiError { + message: string; + code: number; + details?: Record; +} + +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` +- [ ] 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) +})); +``` \ No newline at end of file diff --git a/frontend/__mocks__/uplotMock.ts b/frontend/__mocks__/uplotMock.ts new file mode 100644 index 000000000000..9cf9add9f0c2 --- /dev/null +++ b/frontend/__mocks__/uplotMock.ts @@ -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; diff --git a/frontend/__mocks__/useSafeNavigate.ts b/frontend/__mocks__/useSafeNavigate.ts new file mode 100644 index 000000000000..a1044da052c7 --- /dev/null +++ b/frontend/__mocks__/useSafeNavigate.ts @@ -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 + >, +}); diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 18b0989113ff..1d9255a329e8 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -1,5 +1,7 @@ import type { Config } from '@jest/types'; +const USE_SAFE_NAVIGATE_MOCK_PATH = '/__mocks__/useSafeNavigate.ts'; + const config: Config.InitialOptions = { clearMocks: true, coverageDirectory: 'coverage', @@ -10,6 +12,10 @@ const config: Config.InitialOptions = { moduleNameMapper: { '\\.(css|less|scss)$': '/__mocks__/cssMock.ts', '\\.md$': '/__mocks__/cssMock.ts', + '^uplot$': '/__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: { extensionsToTreatAsEsm: ['.ts'], diff --git a/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx b/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx index bfe099a0ed59..f33db568a3b7 100644 --- a/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx +++ b/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx @@ -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', () => ({ useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]), useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]), diff --git a/frontend/src/components/ErrorModal/ErrorModal.test.tsx b/frontend/src/components/ErrorModal/ErrorModal.test.tsx index 64f880e8cece..cb39768c7d2c 100644 --- a/frontend/src/components/ErrorModal/ErrorModal.test.tsx +++ b/frontend/src/components/ErrorModal/ErrorModal.test.tsx @@ -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 ErrorModal from './ErrorModal'; @@ -56,9 +56,8 @@ describe('ErrorModal Component', () => { // Click the close button const closeButton = screen.getByTestId('close-button'); - act(() => { - fireEvent.click(closeButton); - }); + const user = userEvent.setup({ pointerEventsCheck: 0 }); + await user.click(closeButton); // Check if onClose was called expect(onCloseMock).toHaveBeenCalledTimes(1); @@ -149,9 +148,8 @@ it('should open the modal when the trigger component is clicked', async () => { // Click the trigger component const triggerButton = screen.getByText('Open Error Modal'); - act(() => { - fireEvent.click(triggerButton); - }); + const user = userEvent.setup({ pointerEventsCheck: 0 }); + await user.click(triggerButton); // Check if the modal is displayed 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 const triggerButton = screen.getByText('error'); - act(() => { - fireEvent.click(triggerButton); - }); + const user = userEvent.setup({ pointerEventsCheck: 0 }); + await user.click(triggerButton); await waitFor(() => { expect(screen.getByText('An error occurred')).toBeInTheDocument(); }); // Trigger the onCancel event - act(() => { - fireEvent.click(screen.getByTestId('close-button')); - }); + await user.click(screen.getByTestId('close-button')); // Check if the modal is closed expect(onCloseMock).toHaveBeenCalledTimes(1); diff --git a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx index 252a4d23084a..a251095eb41e 100644 --- a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx +++ b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx @@ -1,15 +1,6 @@ import '@testing-library/jest-dom'; -import { - act, - cleanup, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; import { ENVIRONMENT } from 'constants/env'; -import ROUTES from 'constants/routes'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { otherFiltersResponse, @@ -18,8 +9,7 @@ import { } from 'mocks-server/__mockdata__/customQuickFilters'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; -import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; -import { USER_ROLES } from 'types/roles'; +import { render, screen, userEvent, waitFor } from 'tests/test-utils'; import QuickFilters from '../QuickFilters'; import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types'; @@ -29,21 +19,6 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({ 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 redirectWithQueryBuilderData = jest.fn(); const putHandler = jest.fn(); @@ -78,11 +53,10 @@ const setupServer = (): void => { putHandler(await req.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)), ), - rest.get(fieldsValuesURL, (req, res, ctx) => + rest.get(fieldsValuesURL, (_req, res, ctx) => res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)), ), ); @@ -96,14 +70,12 @@ function TestQuickFilters({ config?: IQuickFiltersConfig[]; }): JSX.Element { return ( - - - + ); } @@ -118,11 +90,11 @@ beforeAll(() => { afterEach(() => { server.resetHandlers(); + jest.clearAllMocks(); }); afterAll(() => { server.close(); - cleanup(); }); beforeEach(() => { @@ -151,9 +123,13 @@ describe('Quick Filters', () => { }); it('should add filter data to query when checkbox is clicked', async () => { + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); - const checkbox = screen.getByText('mq-kafka'); - fireEvent.click(checkbox); + + // Prefer role if possible; if label text isn’t wired to input, clicking the label text is OK + const target = await screen.findByText('mq-kafka'); + await user.click(target); await waitFor(() => { expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( @@ -182,16 +158,20 @@ describe('Quick Filters', () => { describe('Quick Filters with custom filters', () => { it('loads the custom filters correctly', async () => { + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); + expect(screen.getByText('Filters for')).toBeInTheDocument(); expect(screen.getByText(QUERY_NAME)).toBeInTheDocument(); + await screen.findByText(FILTER_SERVICE_NAME); const allByText = await screen.findAllByText('otel-demo'); - // since 2 filter collapse are open, there are 2 filter items visible expect(allByText).toHaveLength(2); 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(); @@ -207,16 +187,19 @@ describe('Quick Filters with custom filters', () => { }); it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => { + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); await screen.findByText(FILTER_SERVICE_NAME); 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 addButton = otherFilterItem.parentElement?.querySelector('button'); expect(addButton).not.toBeNull(); - fireEvent.click(addButton as HTMLButtonElement); + await user.click(addButton as HTMLButtonElement); const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; 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 () => { + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); await screen.findByText(FILTER_SERVICE_NAME); 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 target = await screen.findByText(FILTER_OS_DESCRIPTION); const removeBtn = target.parentElement?.querySelector('button'); expect(removeBtn).not.toBeNull(); - fireEvent.click(removeBtn as HTMLButtonElement); + + await user.click(removeBtn as HTMLButtonElement); await waitFor(() => { expect(addedSection).not.toContainElement( @@ -250,17 +237,20 @@ describe('Quick Filters with custom filters', () => { }); it('restores original filter state on Discard', async () => { + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); await screen.findByText(FILTER_SERVICE_NAME); 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 target = await screen.findByText(FILTER_OS_DESCRIPTION); const removeBtn = target.parentElement?.querySelector('button'); expect(removeBtn).not.toBeNull(); - fireEvent.click(removeBtn as HTMLButtonElement); + await user.click(removeBtn as HTMLButtonElement); const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!; 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(() => { 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 () => { + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); await screen.findByText(FILTER_SERVICE_NAME); 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 removeBtn = target.parentElement?.querySelector('button'); 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(() => { expect(putHandler).toHaveBeenCalled(); @@ -311,31 +304,36 @@ describe('Quick Filters with custom filters', () => { expect(requestBody.signal).toBe(SIGNAL); }); - // render duration filter 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(); + const user = userEvent.setup({ + advanceTimers: (ms) => jest.advanceTimersByTime(ms), + pointerEventsCheck: 0, + }); const { getByTestId } = render(); await screen.findByText(FILTER_SERVICE_NAME); expect(screen.getByText('Duration')).toBeInTheDocument(); - // click to open the duration filter - fireEvent.click(screen.getByText('Duration')); + // Open the duration section (use role if it’s a button/collapse) + await user.click(screen.getByText('Duration')); const minDuration = getByTestId('min-input') as HTMLInputElement; const maxDuration = getByTestId('max-input') as HTMLInputElement; + expect(minDuration).toHaveValue(null); expect(minDuration).toHaveProperty('placeholder', '0'); expect(maxDuration).toHaveValue(null); expect(maxDuration).toHaveProperty('placeholder', '100000000'); - await act(async () => { - // set values - fireEvent.change(minDuration, { target: { value: '10000' } }); - fireEvent.change(maxDuration, { target: { value: '20000' } }); - jest.advanceTimersByTime(2000); - }); + // Type values and advance debounce + await user.clear(minDuration); + await user.type(minDuration, '10000'); + await user.clear(maxDuration); + await user.type(maxDuration, '20000'); + jest.advanceTimersByTime(2000); + await waitFor(() => { expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( expect.objectContaining({ @@ -363,6 +361,6 @@ describe('Quick Filters with custom filters', () => { ); }); - jest.useRealTimers(); // Clean up + jest.useRealTimers(); }); }); diff --git a/frontend/src/container/AllAlertChannels/__tests__/AlertChannels.test.tsx b/frontend/src/container/AllAlertChannels/__tests__/AlertChannels.test.tsx index 32f597a82a12..bf8d5398c21d 100644 --- a/frontend/src/container/AllAlertChannels/__tests__/AlertChannels.test.tsx +++ b/frontend/src/container/AllAlertChannels/__tests__/AlertChannels.test.tsx @@ -22,6 +22,8 @@ jest.mock('react-router-dom', () => ({ describe('Alert Channels Settings List page', () => { beforeEach(async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2023-10-20')); render(); await waitFor(() => expect(screen.getByText('sending_channels_note')).toBeInTheDocument(), @@ -29,6 +31,7 @@ describe('Alert Channels Settings List page', () => { }); afterEach(() => { jest.restoreAllMocks(); + jest.useRealTimers(); }); describe('Should display the Alert Channels page properly', () => { it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => { diff --git a/frontend/src/container/AllAlertChannels/__tests__/AlertChannelsNormalUser.test.tsx b/frontend/src/container/AllAlertChannels/__tests__/AlertChannelsNormalUser.test.tsx index 162f0fb8fbdc..cbdcf223e026 100644 --- a/frontend/src/container/AllAlertChannels/__tests__/AlertChannelsNormalUser.test.tsx +++ b/frontend/src/container/AllAlertChannels/__tests__/AlertChannelsNormalUser.test.tsx @@ -28,6 +28,7 @@ jest.mock('react-router-dom', () => ({ describe('Alert Channels Settings List page (Normal User)', () => { beforeEach(async () => { + jest.useFakeTimers(); render(); await waitFor(() => expect(screen.getByText('sending_channels_note')).toBeInTheDocument(), @@ -35,6 +36,7 @@ describe('Alert Channels Settings List page (Normal User)', () => { }); afterEach(() => { jest.restoreAllMocks(); + jest.useRealTimers(); }); 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 () => { diff --git a/frontend/src/container/BillingContainer/BillingContainer.test.tsx b/frontend/src/container/BillingContainer/BillingContainer.test.tsx index a52e32dd1aa5..efec53d79778 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.test.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.test.tsx @@ -9,22 +9,6 @@ import { getFormattedDate } from 'utils/timeUtils'; 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 || jest.fn().mockImplementation(() => ({ @@ -67,78 +51,103 @@ describe('BillingContainer', () => { expect(currentBill).toBeInTheDocument(); }); - test('OnTrail', async () => { - await act(async () => { - render(, undefined, undefined, { - trialInfo: licensesSuccessResponse.data, + describe('Trial scenarios', () => { + beforeEach(() => { + jest.useFakeTimers(); + 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( + , + {}, + { 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'); - expect(freeTrailText).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 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(, undefined, undefined, { - trialInfo: trialConvertedToSubscriptionResponse.data, + test('OnTrail but trialConvertedToSubscription', async () => { + await act(async () => { + render( + , + {}, + { + appContextOverrides: { + 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 () => { - const { findByText } = render(, undefined, undefined, { - trialInfo: notOfTrailResponse.data, - }); + const { findByText } = render( + , + {}, + { + appContextOverrides: { + trialInfo: notOfTrailResponse.data, + }, + }, + ); const billingPeriodText = `Your current billing period is from ${getFormattedDate( billingSuccessResponse.data.billingPeriodStart, diff --git a/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx b/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx index f7a1b1fb0f07..daa68df3adbf 100644 --- a/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx +++ b/frontend/src/container/CreateAlertRule/AlertRuleDocumentationRedirection.test.tsx @@ -1,7 +1,6 @@ import ROUTES from 'constants/routes'; import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions'; import CreateAlertPage from 'pages/CreateAlert'; -import { MemoryRouter, Route } from 'react-router-dom'; import { act, fireEvent, render } from 'tests/test-utils'; 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', () => ({ useSafeNavigate: (): any => ({ safeNavigate: jest.fn(), @@ -84,11 +69,11 @@ describe('Alert rule documentation redirection', () => { beforeEach(() => { act(() => { renderResult = render( - - - - - , + , + {}, + { + initialRoute: ROUTES.ALERTS_NEW, + }, ); }); }); diff --git a/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx b/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx index 783f6f658531..6b7e43a4bcd3 100644 --- a/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx +++ b/frontend/src/container/CreateAlertRule/AnomalyAlertDocumentationRedirection.test.tsx @@ -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 || jest.fn().mockImplementation(() => ({ diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx index 1f0522546bb5..463a3b0a2191 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx @@ -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 const mockProps: WidgetGraphComponentProps = { widget: { diff --git a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx index 270b2a8cd660..fb55a67df77b 100644 --- a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx +++ b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx @@ -33,19 +33,6 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({ 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.requireActual('react-redux'), useSelector: (): any => ({ diff --git a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx index 8736b1740d87..a385850093dd 100644 --- a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx +++ b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx @@ -3,20 +3,6 @@ import { render, screen } from '@testing-library/react'; 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'; describe('HostsListTable', () => { diff --git a/frontend/src/container/InfraMonitoringHosts/__tests__/utilts.test.tsx b/frontend/src/container/InfraMonitoringHosts/__tests__/utilts.test.tsx index a38681f37256..c06b7483c688 100644 --- a/frontend/src/container/InfraMonitoringHosts/__tests__/utilts.test.tsx +++ b/frontend/src/container/InfraMonitoringHosts/__tests__/utilts.test.tsx @@ -4,20 +4,6 @@ import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils'; 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('formatDataForTable', () => { it('should format host data correctly', () => { diff --git a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs/__tests__/EntityLogs.test.tsx b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs/__tests__/EntityLogs.test.tsx index 0c3bc5793dab..453b8c7b8b57 100644 --- a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs/__tests__/EntityLogs.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs/__tests__/EntityLogs.test.tsx @@ -44,20 +44,6 @@ const verifyEntityLogsPayload = ({ return queryData; }; -jest.mock('uplot', () => { - const paths = { - spline: jest.fn(), - bars: jest.fn(), - }; - const uplotMock = jest.fn(() => ({ - paths, - })); - return { - paths, - default: uplotMock, - }; -}); - jest.mock( 'components/OverlayScrollbar/OverlayScrollbar', () => diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx index 7b32c9226ada..0a650a2dd043 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx @@ -4,14 +4,8 @@ import setupCommonMocks from '../../commonMocks'; setupCommonMocks(); -import { fireEvent, render, screen } from '@testing-library/react'; import JobDetails from 'container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails'; -import { QueryClient, QueryClientProvider } from 'react-query'; -import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; -import store from 'store'; - -const queryClient = new QueryClient(); +import { fireEvent, render, screen } from 'tests/test-utils'; describe('JobDetails', () => { const mockJob = { @@ -24,13 +18,7 @@ describe('JobDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + , ); const jobNameElements = screen.getAllByText('test-job'); @@ -44,13 +32,7 @@ describe('JobDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + , ); const metricsTab = screen.getByText('Metrics'); @@ -68,13 +50,7 @@ describe('JobDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -83,13 +59,7 @@ describe('JobDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -100,13 +70,7 @@ describe('JobDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts b/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts index 41bd0782ace1..244e569248bd 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts @@ -56,19 +56,6 @@ const setupCommonMocks = (): void => { 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', () => ({ __esModule: true, default: jest.fn().mockImplementation(() => ({ diff --git a/frontend/src/container/LogDetailedView/ContextView/__tests__/ContextLogRenderer.test.tsx b/frontend/src/container/LogDetailedView/ContextView/__tests__/ContextLogRenderer.test.tsx index c69e83c030db..5066ca0df609 100644 --- a/frontend/src/container/LogDetailedView/ContextView/__tests__/ContextLogRenderer.test.tsx +++ b/frontend/src/container/LogDetailedView/ContextView/__tests__/ContextLogRenderer.test.tsx @@ -32,20 +32,6 @@ import { // Mock the useContextLogData hook 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', () => ({ useOptionsMenu: (): any => ({ options: { diff --git a/frontend/src/container/LogsExplorerList/__tests__/LogsExplorerList.test.tsx b/frontend/src/container/LogsExplorerList/__tests__/LogsExplorerList.test.tsx index d9fee0694bcd..f30864174c22 100644 --- a/frontend/src/container/LogsExplorerList/__tests__/LogsExplorerList.test.tsx +++ b/frontend/src/container/LogsExplorerList/__tests__/LogsExplorerList.test.tsx @@ -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', () => ({ __esModule: true, useGetExplorerQueryRange: jest.fn(), diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx index f536bf6c6a73..20cb3fc62b8f 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx @@ -7,18 +7,16 @@ import { logsresponse } from 'mocks-server/__mockdata__/query_range'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; import LogsExplorer from 'pages/LogsExplorer'; -import { QueryBuilderContext } from 'providers/QueryBuilder'; import React from 'react'; -import { I18nextProvider } from 'react-i18next'; -import { MemoryRouter } from 'react-router-dom-v5-compat'; import { VirtuosoMockContext } from 'react-virtuoso'; -import i18n from 'ReactI18'; import { act, + AllTheProviders, fireEvent, render, RenderResult, screen, + userEvent, waitFor, } from 'tests/test-utils'; import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -91,20 +89,6 @@ getStateSpy.mockImplementation(() => { 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.requireActual('react-router-dom'), useLocation: (): { search: string; pathname: string } => ({ @@ -277,9 +261,7 @@ describe.skip('LogsExplorerViews Pagination', () => { act(() => { renderResult = render( - - - + , ); }); @@ -453,13 +435,14 @@ function LogsExplorerWithMockContext({ ); return ( - - - - - - - + + + + + ); } @@ -536,13 +519,12 @@ describe('Logs Explorer -> stage and run query', () => { const initialEnd = initialPayload.end; // Click the Stage & Run Query button - await act(async () => { - fireEvent.click( - screen.getByRole('button', { - name: /stage & run query/i, - }), - ); - }); + const user = userEvent.setup({ pointerEventsCheck: 0 }); + await user.click( + screen.getByRole('button', { + name: /stage & run query/i, + }), + ); // Wait for additional API calls to be made after clicking Stage & Run Query await waitFor( diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index be14befffd71..bf9690fb7651 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -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 jest.mock( 'container/TimeSeriesView/TimeSeriesView', diff --git a/frontend/src/container/LogsPanelTable/__tests__/LogsPanelComponent.test.tsx b/frontend/src/container/LogsPanelTable/__tests__/LogsPanelComponent.test.tsx index f3fdb1756d3d..816c6d5d997e 100644 --- a/frontend/src/container/LogsPanelTable/__tests__/LogsPanelComponent.test.tsx +++ b/frontend/src/container/LogsPanelTable/__tests__/LogsPanelComponent.test.tsx @@ -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'; // 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', () => ({ __esModule: true, diff --git a/frontend/src/container/MetricsExplorer/Explorer/__tests__/Explorer.test.tsx b/frontend/src/container/MetricsExplorer/Explorer/__tests__/Explorer.test.tsx index 0457a5cc7b57..8916144ad93c 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/__tests__/Explorer.test.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/__tests__/Explorer.test.tsx @@ -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.requireActual('react-redux'), useSelector: (): any => ({ diff --git a/frontend/src/container/MetricsExplorer/Inspect/__tests__/GraphView.test.tsx b/frontend/src/container/MetricsExplorer/Inspect/__tests__/GraphView.test.tsx index ab57573501bd..6cb51d2b72f6 100644 --- a/frontend/src/container/MetricsExplorer/Inspect/__tests__/GraphView.test.tsx +++ b/frontend/src/container/MetricsExplorer/Inspect/__tests__/GraphView.test.tsx @@ -15,12 +15,6 @@ import { TimeAggregationOptions, } from '../types'; -jest.mock('uplot', () => - jest.fn().mockImplementation(() => ({ - destroy: jest.fn(), - })), -); - const mockResizeObserver = jest.fn(); mockResizeObserver.mockImplementation(() => ({ observe: (): void => undefined, diff --git a/frontend/src/container/MetricsExplorer/Inspect/__tests__/Inspect.test.tsx b/frontend/src/container/MetricsExplorer/Inspect/__tests__/Inspect.test.tsx index 22a8a474c978..be3bf38f36b7 100644 --- a/frontend/src/container/MetricsExplorer/Inspect/__tests__/Inspect.test.tsx +++ b/frontend/src/container/MetricsExplorer/Inspect/__tests__/Inspect.test.tsx @@ -76,12 +76,6 @@ jest isLoading: false, } as any); -jest.mock('uplot', () => - jest.fn().mockImplementation(() => ({ - destroy: jest.fn(), - })), -); - jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: (): { pathname: string } => ({ diff --git a/frontend/src/container/MetricsExplorer/MetricDetails/__tests__/DashboardsAndAlertsPopover.test.tsx b/frontend/src/container/MetricsExplorer/MetricDetails/__tests__/DashboardsAndAlertsPopover.test.tsx index 1f924519d649..f339a6e95260 100644 --- a/frontend/src/container/MetricsExplorer/MetricDetails/__tests__/DashboardsAndAlertsPopover.test.tsx +++ b/frontend/src/container/MetricsExplorer/MetricDetails/__tests__/DashboardsAndAlertsPopover.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { QueryParams } from 'constants/query'; -import * as useSafeNavigate from 'hooks/useSafeNavigate'; import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover'; @@ -24,9 +23,11 @@ const mockAlerts = [mockAlert1, mockAlert2]; const mockDashboards = [mockDashboard1, mockDashboard2]; const mockSafeNavigate = jest.fn(); -jest.spyOn(useSafeNavigate, 'useSafeNavigate').mockReturnValue({ - safeNavigate: mockSafeNavigate, -}); +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: mockSafeNavigate, + }), +})); const mockSetQuery = jest.fn(); const mockUrlQuery = { diff --git a/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx b/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx index 276111c77c6d..48522c686875 100644 --- a/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx @@ -11,19 +11,6 @@ import store from 'store'; import Summary from '../Summary'; 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', () => ({ stratify: jest.fn().mockReturnValue({ id: jest.fn().mockReturnValue({ diff --git a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx index f805efed689a..5b675039a85c 100644 --- a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx +++ b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx @@ -10,20 +10,6 @@ import store from 'store'; import ChangeHistory from '../index'; 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({ defaultOptions: { queries: { diff --git a/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx b/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx index 9e9f245e1b04..36702718f2e1 100644 --- a/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx +++ b/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx @@ -5,20 +5,6 @@ import { PipelineData } from 'types/api/pipeline/def'; import { pipelineMockData } from '../mocks/pipeline'; 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 { Object.defineProperty(window, 'matchMedia', { writable: true, diff --git a/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx b/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx index e1e961e125aa..2f51b7ba892f 100644 --- a/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx +++ b/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx @@ -11,20 +11,6 @@ jest.mock('../PipelineListsView/AddNewProcessor/config', () => ({ 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 = { id: '1', orderId: 1, diff --git a/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx b/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx index 3b2fdfeb34af..451ef8807f6a 100644 --- a/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx +++ b/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx @@ -6,20 +6,6 @@ import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; 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', () => { it('should render DeleteAction section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/DragAction.test.tsx b/frontend/src/container/PipelinePage/tests/DragAction.test.tsx index 9f64714072a8..168b3f042f32 100644 --- a/frontend/src/container/PipelinePage/tests/DragAction.test.tsx +++ b/frontend/src/container/PipelinePage/tests/DragAction.test.tsx @@ -6,20 +6,6 @@ import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; 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', () => { it('should render DragAction section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/EditAction.test.tsx b/frontend/src/container/PipelinePage/tests/EditAction.test.tsx index 56dd77960094..c52991bf6d96 100644 --- a/frontend/src/container/PipelinePage/tests/EditAction.test.tsx +++ b/frontend/src/container/PipelinePage/tests/EditAction.test.tsx @@ -6,20 +6,6 @@ import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; 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', () => { it('should render EditAction section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx index d472f4745c12..83f503107b8f 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx @@ -8,20 +8,6 @@ import store from 'store'; import { pipelineMockData } from '../mocks/pipeline'; 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', () => { it('should render PipelineActions section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx index 818156a72397..b077481619b8 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx @@ -3,20 +3,6 @@ import { render } from 'tests/test-utils'; import { pipelineMockData } from '../mocks/pipeline'; 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(() => { Object.defineProperty(window, 'matchMedia', { writable: true, diff --git a/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx index 3542e0b3165c..f0cba76f621d 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx @@ -6,20 +6,6 @@ import { findByText, fireEvent, render, waitFor } from 'tests/test-utils'; import { pipelineApiResponseMockData } from '../mocks/pipeline'; 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 const mockUrlQuery = { get: jest.fn(), diff --git a/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx b/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx index f99f98368cfc..ff924ff8acb8 100644 --- a/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx @@ -4,20 +4,6 @@ import { v4 } from 'uuid'; 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(() => { Object.defineProperty(window, 'matchMedia', { writable: true, diff --git a/frontend/src/container/PipelinePage/tests/TagInput.test.tsx b/frontend/src/container/PipelinePage/tests/TagInput.test.tsx index e95efb6715ed..24cedc2eb0c8 100644 --- a/frontend/src/container/PipelinePage/tests/TagInput.test.tsx +++ b/frontend/src/container/PipelinePage/tests/TagInput.test.tsx @@ -7,20 +7,6 @@ import store from 'store'; 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', () => { it('should render TagInput section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/Tags.test.tsx b/frontend/src/container/PipelinePage/tests/Tags.test.tsx index 5dc362e957c2..050c6975aef7 100644 --- a/frontend/src/container/PipelinePage/tests/Tags.test.tsx +++ b/frontend/src/container/PipelinePage/tests/Tags.test.tsx @@ -1,24 +1,11 @@ -import { render } from '@testing-library/react'; import Tags from 'container/PipelinePage/PipelineListsView/TableComponents/Tags'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; -import i18n from 'ReactI18'; -import store from 'store'; +import { render } from 'tests/test-utils'; const tags = ['server', 'app']; describe('PipelinePage container test', () => { it('should render Tags section', () => { - const { asFragment } = render( - - - - - - - , - ); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/frontend/src/container/PipelinePage/tests/utils.test.ts b/frontend/src/container/PipelinePage/tests/utils.test.ts index 707ad06c2d1d..c21e8c5a4b59 100644 --- a/frontend/src/container/PipelinePage/tests/utils.test.ts +++ b/frontend/src/container/PipelinePage/tests/utils.test.ts @@ -11,20 +11,6 @@ import { getTableColumn, } 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', () => { test('it should be check form field of add pipeline', () => { expect(pipelineFields.length).toBe(3); diff --git a/frontend/src/container/PlannedDowntime/__test__/PlannedDowntime.test.tsx b/frontend/src/container/PlannedDowntime/__test__/PlannedDowntime.test.tsx index 7e1bda705f3a..9ded9fdc8e07 100644 --- a/frontend/src/container/PlannedDowntime/__test__/PlannedDowntime.test.tsx +++ b/frontend/src/container/PlannedDowntime/__test__/PlannedDowntime.test.tsx @@ -6,7 +6,7 @@ import { PlannedDowntime } from '../PlannedDowntime'; describe('PlannedDowntime Component', () => { it('renders the PlannedDowntime component properly', () => { - render(, {}, 'ADMIN'); + render(, {}, { role: 'ADMIN' }); // Check if title is rendered expect(screen.getByText('Planned Downtime')).toBeInTheDocument(); @@ -30,7 +30,7 @@ describe('PlannedDowntime Component', () => { }); it('disables the "New downtime" button for users with VIEWER role', () => { - render(, {}, USER_ROLES.VIEWER); + render(, {}, { role: USER_ROLES.VIEWER }); // Check if "New downtime" button is disabled for VIEWER const newDowntimeButton = screen.getByRole('button', { diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx index 9a57d8756fd0..8ca7435d7be1 100644 --- a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/__tests__/SpanDuration.test.tsx @@ -1,7 +1,5 @@ -import { fireEvent, screen } from '@testing-library/react'; -import { useSafeNavigate } from 'hooks/useSafeNavigate'; 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 { SpanDuration } from '../Success'; @@ -15,7 +13,6 @@ const DIMMED_SPAN_CLASS = 'dimmed-span'; const SELECTED_NON_MATCHING_SPAN_CLASS = 'selected-non-matching-span'; // Mock the hooks -jest.mock('hooks/useSafeNavigate'); jest.mock('hooks/useUrlQuery'); jest.mock('@signozhq/badge', () => ({ Badge: jest.fn(), @@ -52,24 +49,17 @@ const mockTraceMetadata = { hasMissingSpans: false, }; -jest.mock('uplot', () => { - const paths = { - spline: jest.fn(), - bars: jest.fn(), - }; - const uplotMock = jest.fn(() => ({ - paths, - })); - return { - paths, - default: uplotMock, - }; -}); +const mockSafeNavigate = jest.fn(); + +jest.mock('hooks/useSafeNavigate', () => ({ + useSafeNavigate: (): any => ({ + safeNavigate: mockSafeNavigate, + }), +})); describe('SpanDuration', () => { const mockSetSelectedSpan = jest.fn(); const mockUrlQuerySet = jest.fn(); - const mockSafeNavigate = jest.fn(); const mockUrlQueryGet = jest.fn(); beforeEach(() => { @@ -81,11 +71,6 @@ describe('SpanDuration', () => { get: mockUrlQueryGet, toString: () => 'spanId=test-span-id', }); - - // Mock safe navigate hook - (useSafeNavigate as jest.Mock).mockReturnValue({ - safeNavigate: mockSafeNavigate, - }); }); it('updates URL and selected span when clicked', () => { diff --git a/frontend/src/mocks-server/server.ts b/frontend/src/mocks-server/server.ts index 096e00d32336..e0721635a374 100644 --- a/frontend/src/mocks-server/server.ts +++ b/frontend/src/mocks-server/server.ts @@ -1,7 +1,10 @@ // src/mocks/server.js +import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { handlers } from './handlers'; // This configures a request mocking server with the given request handlers. export const server = setupServer(...handlers); + +export { rest }; diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx index cd7b6e7069ea..7ee4ab7512dd 100644 --- a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx +++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx @@ -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 jest.mock( 'container/TimeSeriesView/TimeSeriesView', diff --git a/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx b/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx index f5af6a705c9d..5db650a70083 100644 --- a/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx +++ b/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx @@ -35,21 +35,16 @@ jest.mock('container/TraceFlameGraph/index.tsx', () => ({ default: (): JSX.Element =>
TraceFlameGraph
, })); -jest.mock('uplot', () => { - const paths = { - spline: jest.fn(), - bars: jest.fn(), - }; - const uplotMock = jest.fn(() => ({ - paths, - })); - return { - paths, - default: uplotMock, - }; -}); - describe('TraceDetail', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2023-10-20')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('should render tracedetail', async () => { const { findByText, getByText, getAllByText, getByPlaceholderText } = render( diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index 163fa5fd6027..25b20fe92c26 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -16,7 +16,6 @@ import { } from 'mocks-server/__mockdata__/query_range'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; -import { QueryBuilderContext } from 'providers/QueryBuilder'; import { MemoryRouter } from 'react-router-dom-v5-compat'; import { 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( 'components/Uplot/Uplot', () => @@ -181,32 +164,31 @@ const checkFilterValues = ( }; const renderWithTracesExplorerRouter = ( - component: React.ReactNode, + component: React.ReactElement, initialEntries: string[] = [ '/traces-explorer/?panelType=list&selectedExplorerView=list', ], ): ReturnType => render( - - - {component} - - , + component, + {}, + { + initialRoute: initialEntries[0], + queryBuilderOverrides: qbProviderValue, + }, ); describe('TracesExplorer - Filters', () => { // Initial filter panel rendering // Test the initial state like which filters section are opened, default state of duration slider, etc. it('should render the Trace filter', async () => { - const { getByText, getAllByText, getByTestId } = render( - - - , - ); + const { + getByText, + getAllByText, + getByTestId, + } = renderWithTracesExplorerRouter(, [ + `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/?panelType=list&selectedExplorerView=list`, + ]); checkFilterValues(getByText, getAllByText); @@ -249,8 +231,12 @@ describe('TracesExplorer - Filters', () => { it('filter panel actions', async () => { const { getByTestId } = render( - + , , + {}, + { + initialRoute: '/traces-explorer/?panelType=list&selectedExplorerView=list', + }, ); // Check if the section is closed @@ -275,23 +261,21 @@ describe('TracesExplorer - Filters', () => { }); it('checking filters should update the query', async () => { - const { getByText } = renderWithTracesExplorerRouter( - , + {}, + { + queryBuilderOverrides: { + ...qbProviderValue, + currentQuery: { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [initialQueryBuilderFormValues], }, - redirectWithQueryBuilderData, - } as any - } - > - - , + }, + }, + }, ); const okCheckbox = getByText('Ok'); @@ -343,9 +327,7 @@ describe('TracesExplorer - Filters', () => { .spyOn(compositeQueryHook, 'useGetCompositeQueryParam') .mockReturnValue(compositeQuery); - const { findByText, getByTestId } = renderWithTracesExplorerRouter( - , - ); + const { findByText, getByTestId } = render(); // 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(); @@ -369,8 +351,12 @@ describe('TracesExplorer - Filters', () => { }, }); - const { getByText, getAllByText } = renderWithTracesExplorerRouter( + const { getByText, getAllByText } = render( , + {}, + { + initialRoute: '/traces-explorer/?panelType=list&selectedExplorerView=list', + }, ); checkFilterValues(getByText, getAllByText); @@ -394,31 +380,28 @@ describe('TracesExplorer - Filters', () => { }, }); - const { getByText, getAllByText } = renderWithTracesExplorerRouter( - , - ); + const { getByText, getAllByText } = render(); checkFilterValues(getByText, getAllByText); }); it('should clear filter on clear & reset button click', async () => { - const { getByText, getByTestId } = renderWithTracesExplorerRouter( - , + {}, + { + initialRoute: '/traces-explorer/?panelType=list&selectedExplorerView=list', + queryBuilderOverrides: { + currentQuery: { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [initialQueryBuilderFormValues], }, - redirectWithQueryBuilderData, - } as any - } - > - - , + }, + redirectWithQueryBuilderData, + }, + }, ); // check for the status section content diff --git a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx index fd279af05cfc..972a27d0b0af 100644 --- a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx +++ b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx @@ -55,7 +55,7 @@ describe('WorkspaceLocked', () => { ), ); - render(, {}, 'VIEWER'); + render(, {}, { role: 'VIEWER' }); const updateCreditCardBtn = await screen.queryByRole('button', { name: /Continue My Journey/i, }); diff --git a/frontend/src/tests/README.md b/frontend/src/tests/README.md new file mode 100644 index 000000000000..e759b3045f3a --- /dev/null +++ b/frontend/src/tests/README.md @@ -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(, 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`, 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(, 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. diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx index 3ca6efcc412b..94d36643373b 100644 --- a/frontend/src/tests/test-utils.tsx +++ b/frontend/src/tests/test-utils.tsx @@ -2,18 +2,20 @@ import { render, RenderOptions, RenderResult } from '@testing-library/react'; import { FeatureKeys } from 'constants/features'; import { ORG_PREFERENCES } from 'constants/orgPreferences'; -import ROUTES from 'constants/routes'; import { ResourceProvider } from 'hooks/useResourceAttribute'; import { AppContext } from 'providers/App/App'; import { IAppContext } from 'providers/App/types'; import { ErrorModalProvider } from 'providers/ErrorModalProvider'; import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider'; -import { QueryBuilderProvider } from 'providers/QueryBuilder'; +import { + QueryBuilderContext, + QueryBuilderProvider, +} from 'providers/QueryBuilder'; import TimezoneProvider from 'providers/Timezone'; import React, { ReactElement } from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import store from 'store'; @@ -23,24 +25,27 @@ import { LicenseState, LicenseStatus, } from 'types/api/licensesV3/getActive'; +import { QueryBuilderContextType } from 'types/common/queryBuilder'; import { ROLES, USER_ROLES } from 'types/roles'; +// import { MemoryRouter as V5MemoryRouter } from 'react-router-dom-v5-compat'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, + retry: false, }, }, }); beforeEach(() => { - jest.useFakeTimers(); + // jest.useFakeTimers(); jest.setSystemTime(new Date('2023-10-20')); }); afterEach(() => { queryClient.clear(); - jest.useRealTimers(); + // jest.useRealTimers(); }); 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( role: string, appContextOverrides?: Partial, @@ -253,48 +240,96 @@ export function getAppContextMock( export function AllTheProviders({ children, - role, // Accept the role as a prop + role, appContextOverrides, + queryBuilderOverrides, + initialRoute, }: { children: React.ReactNode; - role: string; // Define the role prop - appContextOverrides: Partial; + role?: string; + appContextOverrides?: Partial; + queryBuilderOverrides?: Partial; + initialRoute?: string; }): ReactElement { + // Set default values + const roleValue = role || 'ADMIN'; + const appContextOverridesValue = appContextOverrides || {}; + const initialRouteValue = initialRoute || '/'; + + const queryBuilderContent = queryBuilderOverrides ? ( + + {children} + + ) : ( + {children} + ); + return ( - - - - - - + + + + + + - {children} + {queryBuilderContent} - - - - - - + + + + + + ); } +AllTheProviders.defaultProps = { + role: 'ADMIN', + appContextOverrides: {}, + queryBuilderOverrides: undefined, + initialRoute: '/', +}; + +interface ProviderProps { + role?: string; + appContextOverrides?: Partial; + queryBuilderOverrides?: Partial; + initialRoute?: string; +} + const customRender = ( ui: ReactElement, options?: Omit, - role = 'ADMIN', // Set a default role - appContextOverrides?: Partial, -): RenderResult => - render(ui, { + providerProps: ProviderProps = {}, +): RenderResult => { + const { + role = 'ADMIN', + appContextOverrides = {}, + queryBuilderOverrides, + initialRoute = '/', + } = providerProps; + + return render(ui, { wrapper: () => ( - + {ui} ), ...options, }); +}; export * from '@testing-library/react'; +export { default as userEvent } from '@testing-library/user-event'; export { customRender as render };