Compare commits

...

13 Commits

Author SHA1 Message Date
Vinícius Lourenço
d5bf3ee981 test(vitest): second part of migration 2026-05-02 03:08:11 -03:00
Vinícius Lourenço
780fffa0ef test(vitest): first part of migration 2026-05-02 01:52:28 -03:00
Vinicius Lourenço
c6683e075e fix(tsgo): does not accept lint staged args (#11160)
Some checks are pending
build-staging / staging (push) Blocked by required conditions
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-05-01 14:07:13 +00:00
Vinicius Lourenço
3bc936282e feat(tsgo): use tsgo to type-check (#11143) 2026-05-01 12:07:03 +00:00
Vinicius Lourenço
c3f44b31fe chore(unused-files): remove all unused files (#11150)
* chore(unused-files): remove all unused files

* test(logs): removed mocks of old/unused files
2026-05-01 11:36:46 +00:00
primus-bot[bot]
0c9f237369 chore(release): bump to v0.121.1 (#11154)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-04-30 16:17:50 +00:00
Vinicius Lourenço
8b13f004ed Revert "feat(global-time-store): add support to context, url persistence, store persistence, drift handle (#11081)" (#11152)
This reverts commit cc3da72aa5.
2026-04-30 15:46:28 +00:00
Abhi kumar
8c1d13bb38 fix: added fix for groupby being undefined (#11151) 2026-04-30 15:46:05 +00:00
SagarRajput-7
ad8f3328e0 fix(mcp-page): added acitve host url instead of current url on mcp page (#11141)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(mcp-page): added acitve host url instead of current url on mcp page

* fix(mcp-page): configure access and role control

* chore: move get hosts api access to viewers (#11145)

* chore: move get hosts api access to viewers

* chore: update openapi spec

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>

* fix: allowed hosts api to run on all the cloud users

* fix: updated test cases

---------

Co-authored-by: Karan Balani <29383381+balanikaran@users.noreply.github.com>
2026-04-30 13:31:43 +00:00
Vinicius Lourenço
cc3da72aa5 feat(global-time-store): add support to context, url persistence, store persistence, drift handle (#11081)
* feat(global-time-store): add support to context, url persistence, store persistence, drift handle

* chore(fmt): fix issue with format

* refactor(hooks): mark internal and public ones

* refactor(store): adapt to don't need round down

* refactor(global-time): scope queries via name for auto refresh to be isolated

* chore(use-query-cache): add little doc

* chore(global-time): update docs
2026-04-30 11:11:58 +00:00
Nityananda Gohain
755390c4b5 feat: types and handler for llm pricing rules (#10908)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: 1.Types for ai-o11y ricing rules

* fix: changes

* fix: minor changes

* fix: more changes

* fix: new updates

* fix: address comments

* fix: remove nullable

* fix: types

* fix: address comments

* fix: use mustnewuuid

* fix: correct table name

* fix: address comments and move pricing to a single struct

* fix: linting issues
2026-04-30 05:44:12 +00:00
SagarRajput-7
adbd89aae9 fix(platform): fix semantic tokens and component upgrade issue in platform surfaces (#11142)
* fix(platform): fix semantic tokens and component upgrade issue in platform surfaces

* fix: updated signozhq/ui version
2026-04-30 00:31:33 +00:00
primus-bot[bot]
b71de5b561 chore(release): bump to v0.121.0 (#11139)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-04-29 16:08:15 +00:00
710 changed files with 10165 additions and 15748 deletions

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.120.0
image: signoz/signoz:v0.121.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.120.0
image: signoz/signoz:v0.121.1
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.120.0}
image: signoz/signoz:${VERSION:-v0.121.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.120.0}
image: signoz/signoz:${VERSION:-v0.121.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -2632,6 +2632,158 @@ components:
- list
- grouped_list
type: string
LlmpricingruletypesGettablePricingRules:
properties:
items:
items:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
total:
type: integer
required:
- items
- total
- offset
- limit
type: object
LlmpricingruletypesLLMPricingCacheCosts:
properties:
mode:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleCacheMode'
read:
format: double
type: number
write:
format: double
type: number
required:
- mode
type: object
LlmpricingruletypesLLMPricingRule:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
enabled:
type: boolean
id:
type: string
isOverride:
type: boolean
modelName:
type: string
modelPattern:
$ref: '#/components/schemas/LlmpricingruletypesStringSlice'
orgId:
type: string
pricing:
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
provider:
type: string
sourceId:
type: string
syncedAt:
format: date-time
nullable: true
type: string
unit:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- orgId
- modelName
- provider
- modelPattern
- unit
- pricing
- isOverride
- enabled
type: object
LlmpricingruletypesLLMPricingRuleCacheMode:
enum:
- subtract
- additive
- unknown
type: string
LlmpricingruletypesLLMPricingRuleUnit:
enum:
- per_million_tokens
type: string
LlmpricingruletypesLLMRulePricing:
properties:
cache:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingCacheCosts'
input:
format: double
type: number
output:
format: double
type: number
required:
- input
- output
type: object
LlmpricingruletypesStringSlice:
items:
type: string
nullable: true
type: array
LlmpricingruletypesUpdatableLLMPricingRule:
properties:
enabled:
type: boolean
id:
nullable: true
type: string
isOverride:
nullable: true
type: boolean
modelName:
type: string
modelPattern:
items:
type: string
nullable: true
type: array
pricing:
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
provider:
type: string
sourceId:
nullable: true
type: string
unit:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
required:
- modelName
- provider
- modelPattern
- unit
- pricing
- enabled
type: object
LlmpricingruletypesUpdatableLLMPricingRules:
properties:
rules:
items:
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRule'
nullable: true
type: array
required:
- rules
type: object
MetricsexplorertypesInspectMetricsRequest:
properties:
end:
@@ -7675,6 +7827,218 @@ paths:
summary: Create bulk invite
tags:
- users
/api/v1/llm_pricing_rules:
get:
deprecated: false
description: Returns all LLM pricing rules for the authenticated org, with pagination.
operationId: ListLLMPricingRules
parameters:
- in: query
name: offset
schema:
type: integer
- in: query
name: limit
schema:
type: integer
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LlmpricingruletypesGettablePricingRules'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List pricing rules
tags:
- llmpricingrules
put:
deprecated: false
description: Single write endpoint used by both the user and the Zeus sync job.
Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true)
are fully preserved when the request does not provide isOverride; only synced_at
is stamped.
operationId: CreateOrUpdateLLMPricingRules
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRules'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create or update pricing rules
tags:
- llmpricingrules
/api/v1/llm_pricing_rules/{id}:
delete:
deprecated: false
description: Hard-deletes a pricing rule. If auto-synced, it will be recreated
on the next sync cycle.
operationId: DeleteLLMPricingRule
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete a pricing rule
tags:
- llmpricingrules
get:
deprecated: false
description: Returns a single LLM pricing rule by ID.
operationId: GetLLMPricingRule
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get a pricing rule
tags:
- llmpricingrules
/api/v1/logs/promote_paths:
get:
deprecated: false
@@ -16909,9 +17273,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- VIEWER
- tokenizer:
- ADMIN
- VIEWER
summary: Get host info from Zeus.
tags:
- zeus

View File

@@ -1,51 +1,53 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// Mock for uplot library used in tests
import { type Mock, vi } from 'vitest';
export interface MockUPlotInstance {
setData: jest.Mock;
setSize: jest.Mock;
destroy: jest.Mock;
redraw: jest.Mock;
setSeries: jest.Mock;
setData: Mock;
setSize: Mock;
destroy: Mock;
redraw: Mock;
setSeries: Mock;
}
export interface MockUPlotPaths {
spline: jest.Mock;
bars: jest.Mock;
linear: jest.Mock;
stepped: jest.Mock;
spline: Mock;
bars: Mock;
linear: Mock;
stepped: Mock;
}
// Create mock instance methods
const createMockUPlotInstance = (): MockUPlotInstance => ({
setData: jest.fn(),
setSize: jest.fn(),
destroy: jest.fn(),
redraw: jest.fn(),
setSeries: jest.fn(),
setData: vi.fn(),
setSize: vi.fn(),
destroy: vi.fn(),
redraw: vi.fn(),
setSeries: vi.fn(),
});
// Path builder: (self, seriesIdx, idx0, idx1) => paths or null
const createMockPathBuilder = (name: string): jest.Mock =>
jest.fn(() => ({
const createMockPathBuilder = (name: string): Mock =>
vi.fn(() => ({
name, // To test if the correct pathBuilder is used
stroke: jest.fn(),
fill: jest.fn(),
clip: jest.fn(),
stroke: vi.fn(),
fill: vi.fn(),
clip: vi.fn(),
}));
// Create mock paths - linear, spline, stepped needed by UPlotSeriesBuilder.getPathBuilder
const mockPaths = {
spline: jest.fn(() => createMockPathBuilder('spline')),
bars: jest.fn(() => createMockPathBuilder('bars')),
linear: jest.fn(() => createMockPathBuilder('linear')),
stepped: jest.fn((opts?: { align?: number }) =>
spline: vi.fn(() => createMockPathBuilder('spline')),
bars: vi.fn(() => createMockPathBuilder('bars')),
linear: vi.fn(() => createMockPathBuilder('linear')),
stepped: vi.fn((opts?: { align?: number }) =>
createMockPathBuilder(`stepped-(${opts?.align ?? 0})`),
),
};
// Mock static methods
const mockTzDate = jest.fn(
const mockTzDate = vi.fn(
(date: Date, _timezone: string) => new Date(date.getTime()),
);

View File

@@ -1,4 +1,6 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
import { type MockedFunction, vi } from 'vitest';
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
@@ -14,15 +16,15 @@ interface SafeNavigateTo {
type SafeNavigateToType = string | SafeNavigateTo;
interface UseSafeNavigateReturn {
safeNavigate: jest.MockedFunction<
safeNavigate: MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>;
}
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
safeNavigate: vi.fn(
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as jest.MockedFunction<
) as MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,
});

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"project": ["src/**/*.ts", "src/**/*.tsx"],
"ignore": ["src/api/generated/**/*.ts"]
"ignore": ["src/api/generated/**/*.ts", "src/typings/*.ts"]
}

View File

@@ -15,14 +15,13 @@
"lint:generated": "oxlint ./src/api/generated --fix",
"lint:fix": "oxlint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs && patch-package",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:changedsince": "vitest run --changed HEAD~1 --coverage",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
},
@@ -51,7 +50,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.1.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.10",
"@signozhq/ui": "0.0.12",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",
@@ -171,7 +170,7 @@
"@commitlint/config-conventional": "^20.4.2",
"@faker-js/faker": "9.3.0",
"@jest/globals": "30.2.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
@@ -198,9 +197,12 @@
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@typescript/native-preview": "7.0.0-dev.20260421.2",
"@vitest/coverage-v8": "4.1.5",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"eslint-plugin-sonarjs": "4.0.2",
"happy-dom": "20.9.0",
"husky": "^7.0.4",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
@@ -215,6 +217,7 @@
"oxfmt": "0.47.0",
"oxlint": "1.62.0",
"oxlint-tsgolint": "0.22.1",
"patch-package": "8.0.1",
"portfinder-sync": "^0.0.2",
"postcss": "8.5.6",
"postcss-scss": "4.0.9",
@@ -234,13 +237,14 @@
"vite-plugin-checker": "0.12.0",
"vite-plugin-compression": "0.5.1",
"vite-plugin-image-optimizer": "2.0.3",
"vite-tsconfig-paths": "6.1.1"
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.5"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"oxlint --fix",
"oxlint --fix --quiet",
"oxfmt --write",
"sh scripts/typecheck-staged.sh"
"sh -c tsgo --noEmit"
],
"*.(scss|css)": [
"stylelint"
@@ -266,4 +270,4 @@
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

View File

@@ -0,0 +1,46 @@
diff --git a/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js b/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js
index 2480e76..67208c4 100644
--- a/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js
+++ b/node_modules/@mswjs/interceptors/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js
@@ -345,7 +345,14 @@ var createXMLHttpRequestOverride = function (options) {
});
};
XMLHttpRequestOverride.prototype.abort = function () {
- this.log('abort');
+ if (typeof this.log === 'function') {
+ this.log('abort');
+ }
+ if (typeof this.setReadyState !== 'function' ||
+ typeof this.trigger !== 'function' ||
+ typeof this.readyState !== 'number') {
+ return;
+ }
if (this.readyState > this.UNSENT && this.readyState < this.DONE) {
this.setReadyState(this.UNSENT);
this.trigger('abort');
@@ -459,14 +466,17 @@ var createXMLHttpRequestOverride = function (options) {
}
finally { if (e_2) throw e_2.error; }
}
- request.onabort = this.abort;
- request.onerror = this.onerror;
- request.ontimeout = this.ontimeout;
- request.onload = this.onload;
- request.onloadstart = this.onloadstart;
- request.onloadend = this.onloadend;
- request.onprogress = this.onprogress;
- request.onreadystatechange = this.onreadystatechange;
+ request.abort = this.abort.bind(this);
+ request.onabort = this.abort.bind(this);
+ request.onerror = this.onerror ? this.onerror.bind(this) : null;
+ request.ontimeout = this.ontimeout ? this.ontimeout.bind(this) : null;
+ request.onload = this.onload ? this.onload.bind(this) : null;
+ request.onloadstart = this.onloadstart ? this.onloadstart.bind(this) : null;
+ request.onloadend = this.onloadend ? this.onloadend.bind(this) : null;
+ request.onprogress = this.onprogress ? this.onprogress.bind(this) : null;
+ request.onreadystatechange = this.onreadystatechange
+ ? this.onreadystatechange.bind(this)
+ : null;
};
/**
* Propagates the mock XMLHttpRequest instance listeners

View File

@@ -1,25 +0,0 @@
files="";
# lint-staged will pass all files in $1 $2 $3 etc. iterate and concat.
for var in "$@"
do
files="$files \"$var\","
done
# create temporary tsconfig which includes only passed files
str="{
\"extends\": \"./tsconfig.json\",
\"include\": [ \"src/typings/**/*.ts\",\"src/**/*.d.ts\", \"./babel.config.js\", \"./jest.config.ts\", \"./.eslintrc.js\",\"./__mocks__\",\"./public\",\"./tests\",\"./commitlint.config.ts\",\"./webpack.config.js\",\"./webpack.config.prod.js\",\"./jest.setup.ts\",\"./**/*.d.ts\",$files]
}"
echo $str > tsconfig.tmp
# run typecheck using temp config
tsc -p ./tsconfig.tmp
# capture exit code of tsc
code=$?
# delete temp config
rm ./tsconfig.tmp
exit $code

View File

@@ -1,3 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter, Route, Switch, useLocation } from 'react-router-dom';
@@ -22,13 +23,13 @@ import { ROLES, USER_ROLES } from 'types/roles';
import PrivateRoute from '../Private';
// Mock localStorage APIs
const mockLocalStorage: Record<string, string> = {};
jest.mock('api/browser/localstorage/get', () => ({
const mockLocalStorage = vi.hoisted((): Record<string, string> => ({}));
vi.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: (key: string): string | null => mockLocalStorage[key] || null,
}));
jest.mock('api/browser/localstorage/set', () => ({
vi.mock('api/browser/localstorage/set', () => ({
__esModule: true,
default: (key: string, value: string): void => {
mockLocalStorage[key] = value;
@@ -36,27 +37,29 @@ jest.mock('api/browser/localstorage/set', () => ({
}));
// Mock useGetTenantLicense hook
let mockIsCloudUser = true;
jest.mock('hooks/useGetTenantLicense', () => ({
const mockTenantLicense = vi.hoisted(() => ({ isCloudUser: true }));
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: (): {
isCloudUser: boolean;
isEnterpriseSelfHostedUser: boolean;
isCommunityUser: boolean;
isCommunityEnterpriseUser: boolean;
} => ({
isCloudUser: mockIsCloudUser,
isEnterpriseSelfHostedUser: !mockIsCloudUser,
isCloudUser: mockTenantLicense.isCloudUser,
isEnterpriseSelfHostedUser: !mockTenantLicense.isCloudUser,
isCommunityUser: false,
isCommunityEnterpriseUser: false,
}),
}));
// Mock react-query for users fetch
let mockUsersData: { email: string }[] = [];
jest.mock('api/generated/services/users', () => ({
...jest.requireActual('api/generated/services/users'),
useListUsers: jest.fn(() => ({
data: { data: mockUsersData },
const mockUsers = vi.hoisted((): { data: { email: string }[] } => ({
data: [],
}));
vi.mock('api/generated/services/users', async () => ({
...(await vi.importActual('api/generated/services/users')),
useListUsers: vi.fn(() => ({
data: { data: mockUsers.data },
isFetching: false,
})),
}));
@@ -176,13 +179,13 @@ function createMockAppContext(
orgPreferencesFetchError: null,
changelog: null,
showChangelogModal: false,
activeLicenseRefetch: jest.fn(),
updateUser: jest.fn(),
updateOrgPreferences: jest.fn(),
updateUserPreferenceInContext: jest.fn(),
updateOrg: jest.fn(),
updateChangelog: jest.fn(),
toggleChangelogModal: jest.fn(),
activeLicenseRefetch: vi.fn(),
updateUser: vi.fn(),
updateOrgPreferences: vi.fn(),
updateUserPreferenceInContext: vi.fn(),
updateOrg: vi.fn(),
updateChangelog: vi.fn(),
toggleChangelogModal: vi.fn(),
versionData: { version: '1.0.0', ee: 'Y', setupCompleted: true },
hasEditPermission: true,
...overrides,
@@ -202,7 +205,7 @@ function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
isCloudUser = true,
} = options;
mockIsCloudUser = isCloudUser;
mockTenantLicense.isCloudUser = isCloudUser;
const contextValue = createMockAppContext(appContext);
@@ -245,11 +248,11 @@ function assertRendersChildren(): void {
describe('PrivateRoute', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
queryClient.clear();
Object.keys(mockLocalStorage).forEach((key) => delete mockLocalStorage[key]);
mockIsCloudUser = true;
mockUsersData = [];
mockTenantLicense.isCloudUser = true;
mockUsers.data = [];
});
describe('Old Routes Handling', () => {
@@ -1014,7 +1017,7 @@ describe('PrivateRoute', () => {
describe('Onboarding Flow (Cloud Users)', () => {
it('should redirect to onboarding when first user has not completed onboarding', async () => {
// Set up exactly one user (not admin@signoz.cloud) to trigger first user check
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1053,7 +1056,7 @@ describe('PrivateRoute', () => {
it('should not redirect to onboarding when onboarding is already complete', async () => {
// Set up first user condition - this ensures the ONLY reason we don't redirect
// is because isOnboardingComplete is true
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1124,7 +1127,7 @@ describe('PrivateRoute', () => {
it('should not redirect to onboarding when workspace is blocked and accessing billing', async () => {
// This tests the scenario where admin tries to access billing to fix payment
// while workspace is blocked and onboarding is not complete
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.BILLING,
@@ -1149,7 +1152,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is blocked and accessing settings', async () => {
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.SETTINGS,
@@ -1173,7 +1176,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is suspended (DEFAULTED)', async () => {
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1200,7 +1203,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is access restricted (TERMINATED)', async () => {
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1227,7 +1230,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is access restricted (EXPIRED)', async () => {
mockUsersData = [{ email: 'test@example.com' }];
mockUsers.data = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,

View File

@@ -1,22 +1,23 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* localstorage/get — lazy migration tests.
*
* basePath is memoized at module init, so each describe block re-imports the
* module with a fresh DOM state via jest.isolateModules.
* basePath is memoized at module init, so each test re-imports the module with
* a fresh DOM state via vi.resetModules and dynamic import.
*/
type GetModule = typeof import('../get');
function loadGetModule(href: string): GetModule {
async function loadGetModule(href: string): Promise<GetModule> {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.append(base);
let mod!: GetModule;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../get');
});
vi.resetModules();
const mod = await import('../get');
base.remove();
return mod;
}
@@ -28,19 +29,19 @@ afterEach(() => {
});
describe('get — root path "/"', () => {
it('reads the bare key', () => {
const { default: get } = loadGetModule('/');
it('reads the bare key', async () => {
const { default: get } = await loadGetModule('/');
localStorage.setItem('AUTH_TOKEN', 'tok');
expect(get('AUTH_TOKEN')).toBe('tok');
});
it('returns null when key is absent', () => {
const { default: get } = loadGetModule('/');
it('returns null when key is absent', async () => {
const { default: get } = await loadGetModule('/');
expect(get('MISSING')).toBeNull();
});
it('does NOT promote bare keys (no-op at root)', () => {
const { default: get } = loadGetModule('/');
it('does NOT promote bare keys (no-op at root)', async () => {
const { default: get } = await loadGetModule('/');
localStorage.setItem('THEME', 'light');
get('THEME');
// bare key must still be present — no migration at root
@@ -49,19 +50,19 @@ describe('get — root path "/"', () => {
});
describe('get — prefixed path "/signoz/"', () => {
it('reads an already-scoped key directly', () => {
const { default: get } = loadGetModule('/signoz/');
it('reads an already-scoped key directly', async () => {
const { default: get } = await loadGetModule('/signoz/');
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
expect(get('AUTH_TOKEN')).toBe('scoped-tok');
});
it('returns null when neither scoped nor bare key exists', () => {
const { default: get } = loadGetModule('/signoz/');
it('returns null when neither scoped nor bare key exists', async () => {
const { default: get } = await loadGetModule('/signoz/');
expect(get('MISSING')).toBeNull();
});
it('lazy-migrates bare key to scoped key on first read', () => {
const { default: get } = loadGetModule('/signoz/');
it('lazy-migrates bare key to scoped key on first read', async () => {
const { default: get } = await loadGetModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'old-tok');
const result = get('AUTH_TOKEN');
@@ -71,8 +72,8 @@ describe('get — prefixed path "/signoz/"', () => {
expect(localStorage.getItem('AUTH_TOKEN')).toBeNull();
});
it('scoped key takes precedence over bare key', () => {
const { default: get } = loadGetModule('/signoz/');
it('scoped key takes precedence over bare key', async () => {
const { default: get } = await loadGetModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'bare-tok');
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
@@ -81,8 +82,8 @@ describe('get — prefixed path "/signoz/"', () => {
expect(localStorage.getItem('AUTH_TOKEN')).toBe('bare-tok');
});
it('subsequent reads after migration use scoped key (no double-write)', () => {
const { default: get } = loadGetModule('/signoz/');
it('subsequent reads after migration use scoped key (no double-write)', async () => {
const { default: get } = await loadGetModule('/signoz/');
localStorage.setItem('THEME', 'dark');
get('THEME'); // triggers migration
@@ -94,31 +95,15 @@ describe('get — prefixed path "/signoz/"', () => {
});
describe('get — two-prefix isolation', () => {
it('/signoz/ and /testing/ do not share migrated values', () => {
it('/signoz/ and /testing/ do not share migrated values', async () => {
localStorage.setItem('THEME', 'light');
const base1 = document.createElement('base');
base1.setAttribute('href', '/signoz/');
document.head.append(base1);
let getSignoz!: GetModule['default'];
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
getSignoz = require('../get').default;
});
base1.remove();
const { default: getSignoz } = await loadGetModule('/signoz/');
// migrate bare → /signoz/THEME
getSignoz('THEME');
const base2 = document.createElement('base');
base2.setAttribute('href', '/testing/');
document.head.append(base2);
let getTesting!: GetModule['default'];
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
getTesting = require('../get').default;
});
base2.remove();
const { default: getTesting } = await loadGetModule('/testing/');
// /testing/ prefix: bare key already gone, scoped key does not exist
expect(getTesting('THEME')).toBeNull();

View File

@@ -1,3 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* sessionstorage/get — lazy migration tests.
* Mirrors the localStorage get tests; same logic, different storage.
@@ -5,17 +7,13 @@
type GetModule = typeof import('../get');
function loadGetModule(href: string): GetModule {
async function loadGetModule(href: string): Promise<GetModule> {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.append(base);
let mod!: GetModule;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../get');
});
return mod;
vi.resetModules();
return import('../get');
}
afterEach(() => {
@@ -26,19 +24,19 @@ afterEach(() => {
});
describe('get — root path "/"', () => {
it('reads the bare key', () => {
const { default: get } = loadGetModule('/');
it('reads the bare key', async () => {
const { default: get } = await loadGetModule('/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
expect(get('retry-lazy-refreshed')).toBe('true');
});
it('returns null when key is absent', () => {
const { default: get } = loadGetModule('/');
it('returns null when key is absent', async () => {
const { default: get } = await loadGetModule('/');
expect(get('MISSING')).toBeNull();
});
it('does NOT promote bare keys at root', () => {
const { default: get } = loadGetModule('/');
it('does NOT promote bare keys at root', async () => {
const { default: get } = await loadGetModule('/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
get('retry-lazy-refreshed');
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBe('true');
@@ -46,19 +44,19 @@ describe('get — root path "/"', () => {
});
describe('get — prefixed path "/signoz/"', () => {
it('reads an already-scoped key directly', () => {
const { default: get } = loadGetModule('/signoz/');
it('reads an already-scoped key directly', async () => {
const { default: get } = await loadGetModule('/signoz/');
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'true');
expect(get('retry-lazy-refreshed')).toBe('true');
});
it('returns null when neither scoped nor bare key exists', () => {
const { default: get } = loadGetModule('/signoz/');
it('returns null when neither scoped nor bare key exists', async () => {
const { default: get } = await loadGetModule('/signoz/');
expect(get('MISSING')).toBeNull();
});
it('lazy-migrates bare key to scoped key on first read', () => {
const { default: get } = loadGetModule('/signoz/');
it('lazy-migrates bare key to scoped key on first read', async () => {
const { default: get } = await loadGetModule('/signoz/');
sessionStorage.setItem('retry-lazy-refreshed', 'true');
const result = get('retry-lazy-refreshed');
@@ -68,8 +66,8 @@ describe('get — prefixed path "/signoz/"', () => {
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBeNull();
});
it('scoped key takes precedence over bare key', () => {
const { default: get } = loadGetModule('/signoz/');
it('scoped key takes precedence over bare key', async () => {
const { default: get } = await loadGetModule('/signoz/');
sessionStorage.setItem('retry-lazy-refreshed', 'bare');
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'scoped');

View File

@@ -1,15 +1,18 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import axios from 'api';
import { getFieldKeys } from '../getFieldKeys';
// Mock the API instance
jest.mock('api', () => ({
get: jest.fn(),
vi.mock('api', () => ({
default: {
get: vi.fn(),
},
}));
describe('getFieldKeys API', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
const mockSuccessResponse = {
@@ -28,7 +31,7 @@ describe('getFieldKeys API', () => {
it('should call API with correct parameters when no args provided', async () => {
// Mock successful API response
(axios.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
vi.mocked(axios.get).mockResolvedValueOnce(mockSuccessResponse);
// Call function with no parameters
await getFieldKeys();
@@ -41,7 +44,7 @@ describe('getFieldKeys API', () => {
it('should call API with signal parameter when provided', async () => {
// Mock successful API response
(axios.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
vi.mocked(axios.get).mockResolvedValueOnce(mockSuccessResponse);
// Call function with signal parameter
await getFieldKeys('traces');
@@ -54,7 +57,7 @@ describe('getFieldKeys API', () => {
it('should call API with name parameter when provided', async () => {
// Mock successful API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -76,7 +79,7 @@ describe('getFieldKeys API', () => {
it('should call API with both signal and name when provided', async () => {
// Mock successful API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -98,7 +101,7 @@ describe('getFieldKeys API', () => {
it('should return properly formatted response', async () => {
// Mock API to return our response
(axios.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
vi.mocked(axios.get).mockResolvedValueOnce(mockSuccessResponse);
// Call the function
const result = await getFieldKeys('traces');

View File

@@ -1,20 +1,23 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import axios from 'api';
import { getFieldValues } from '../getFieldValues';
// Mock the API instance
jest.mock('api', () => ({
get: jest.fn(),
vi.mock('api', () => ({
default: {
get: vi.fn(),
},
}));
describe('getFieldValues API', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should call the API with correct parameters (no options)', async () => {
// Mock API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -38,7 +41,7 @@ describe('getFieldValues API', () => {
it('should call the API with signal parameter', async () => {
// Mock API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -62,7 +65,7 @@ describe('getFieldValues API', () => {
it('should call the API with name parameter', async () => {
// Mock API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -86,7 +89,7 @@ describe('getFieldValues API', () => {
it('should call the API with value parameter', async () => {
// Mock API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -110,7 +113,7 @@ describe('getFieldValues API', () => {
it('should call the API with time range parameters', async () => {
// Mock API response
(axios.get as jest.Mock).mockResolvedValueOnce({
vi.mocked(axios.get).mockResolvedValueOnce({
status: 200,
data: {
status: 'success',
@@ -162,7 +165,7 @@ describe('getFieldValues API', () => {
},
};
(axios.get as jest.Mock).mockResolvedValueOnce(mockResponse);
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse);
// Call the function
const result = await getFieldValues('traces', 'mixed.values');
@@ -193,7 +196,7 @@ describe('getFieldValues API', () => {
};
// Mock API to return our response
(axios.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
vi.mocked(axios.get).mockResolvedValueOnce(mockApiResponse);
// Call the function
const result = await getFieldValues('traces', 'service.name');

View File

@@ -0,0 +1,399 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
DeleteLLMPricingRulePathParameters,
GetLLMPricingRule200,
GetLLMPricingRulePathParameters,
ListLLMPricingRules200,
ListLLMPricingRulesParams,
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns all LLM pricing rules for the authenticated org, with pagination.
* @summary List pricing rules
*/
export const listLLMPricingRules = (
params?: ListLLMPricingRulesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListLLMPricingRules200>({
url: `/api/v1/llm_pricing_rules`,
method: 'GET',
params,
signal,
});
};
export const getListLLMPricingRulesQueryKey = (
params?: ListLLMPricingRulesParams,
) => {
return [`/api/v1/llm_pricing_rules`, ...(params ? [params] : [])] as const;
};
export const getListLLMPricingRulesQueryOptions = <
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListLLMPricingRulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLLMPricingRules>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListLLMPricingRulesQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listLLMPricingRules>>
> = ({ signal }) => listLLMPricingRules(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listLLMPricingRules>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListLLMPricingRulesQueryResult = NonNullable<
Awaited<ReturnType<typeof listLLMPricingRules>>
>;
export type ListLLMPricingRulesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List pricing rules
*/
export function useListLLMPricingRules<
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListLLMPricingRulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLLMPricingRules>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListLLMPricingRulesQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List pricing rules
*/
export const invalidateListLLMPricingRules = async (
queryClient: QueryClient,
params?: ListLLMPricingRulesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListLLMPricingRulesQueryKey(params) },
options,
);
return queryClient;
};
/**
* Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.
* @summary Create or update pricing rules
*/
export const createOrUpdateLLMPricingRules = (
llmpricingruletypesUpdatableLLMPricingRulesDTO: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/llm_pricing_rules`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: llmpricingruletypesUpdatableLLMPricingRulesDTO,
});
};
export const getCreateOrUpdateLLMPricingRulesMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
> => {
const mutationKey = ['createOrUpdateLLMPricingRules'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> }
> = (props) => {
const { data } = props ?? {};
return createOrUpdateLLMPricingRules(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateOrUpdateLLMPricingRulesMutationResult = NonNullable<
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>
>;
export type CreateOrUpdateLLMPricingRulesMutationBody =
BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>;
export type CreateOrUpdateLLMPricingRulesMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create or update pricing rules
*/
export const useCreateOrUpdateLLMPricingRules = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
> => {
const mutationOptions =
getCreateOrUpdateLLMPricingRulesMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.
* @summary Delete a pricing rule
*/
export const deleteLLMPricingRule = ({
id,
}: DeleteLLMPricingRulePathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/llm_pricing_rules/${id}`,
method: 'DELETE',
});
};
export const getDeleteLLMPricingRuleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
> => {
const mutationKey = ['deleteLLMPricingRule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
{ pathParams: DeleteLLMPricingRulePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteLLMPricingRule(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteLLMPricingRuleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteLLMPricingRule>>
>;
export type DeleteLLMPricingRuleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete a pricing rule
*/
export const useDeleteLLMPricingRule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
> => {
const mutationOptions = getDeleteLLMPricingRuleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a single LLM pricing rule by ID.
* @summary Get a pricing rule
*/
export const getLLMPricingRule = (
{ id }: GetLLMPricingRulePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetLLMPricingRule200>({
url: `/api/v1/llm_pricing_rules/${id}`,
method: 'GET',
signal,
});
};
export const getGetLLMPricingRuleQueryKey = ({
id,
}: GetLLMPricingRulePathParameters) => {
return [`/api/v1/llm_pricing_rules/${id}`] as const;
};
export const getGetLLMPricingRuleQueryOptions = <
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLLMPricingRulePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetLLMPricingRuleQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getLLMPricingRule>>
> = ({ signal }) => getLLMPricingRule({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetLLMPricingRuleQueryResult = NonNullable<
Awaited<ReturnType<typeof getLLMPricingRule>>
>;
export type GetLLMPricingRuleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a pricing rule
*/
export function useGetLLMPricingRule<
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLLMPricingRulePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetLLMPricingRuleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get a pricing rule
*/
export const invalidateGetLLMPricingRule = async (
queryClient: QueryClient,
{ id }: GetLLMPricingRulePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetLLMPricingRuleQueryKey({ id }) },
options,
);
return queryClient;
};

View File

@@ -3413,6 +3413,170 @@ export enum InframonitoringtypesResponseTypeDTO {
list = 'list',
grouped_list = 'grouped_list',
}
export interface LlmpricingruletypesGettablePricingRulesDTO {
/**
* @type array
* @nullable true
*/
items: LlmpricingruletypesLLMPricingRuleDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset: number;
/**
* @type integer
*/
total: number;
}
export interface LlmpricingruletypesLLMPricingCacheCostsDTO {
mode: LlmpricingruletypesLLMPricingRuleCacheModeDTO;
/**
* @type number
* @format double
*/
read?: number;
/**
* @type number
* @format double
*/
write?: number;
}
export interface LlmpricingruletypesLLMPricingRuleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
*/
id: string;
/**
* @type boolean
*/
isOverride: boolean;
/**
* @type string
*/
modelName: string;
modelPattern: LlmpricingruletypesStringSliceDTO;
/**
* @type string
*/
orgId: string;
pricing: LlmpricingruletypesLLMRulePricingDTO;
/**
* @type string
*/
provider: string;
/**
* @type string
*/
sourceId?: string;
/**
* @type string
* @format date-time
* @nullable true
*/
syncedAt?: Date | null;
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export enum LlmpricingruletypesLLMPricingRuleCacheModeDTO {
subtract = 'subtract',
additive = 'additive',
unknown = 'unknown',
}
export enum LlmpricingruletypesLLMPricingRuleUnitDTO {
per_million_tokens = 'per_million_tokens',
}
export interface LlmpricingruletypesLLMRulePricingDTO {
cache?: LlmpricingruletypesLLMPricingCacheCostsDTO;
/**
* @type number
* @format double
*/
input: number;
/**
* @type number
* @format double
*/
output: number;
}
/**
* @nullable
*/
export type LlmpricingruletypesStringSliceDTO = string[] | null;
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
* @nullable true
*/
id?: string | null;
/**
* @type boolean
* @nullable true
*/
isOverride?: boolean | null;
/**
* @type string
*/
modelName: string;
/**
* @type array
* @nullable true
*/
modelPattern: string[] | null;
pricing: LlmpricingruletypesLLMRulePricingDTO;
/**
* @type string
*/
provider: string;
/**
* @type string
* @nullable true
*/
sourceId?: string | null;
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
}
export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
/**
* @type array
* @nullable true
*/
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
@@ -7004,6 +7168,41 @@ export type CreateInvite201 = {
status: string;
};
export type ListLLMPricingRulesParams = {
/**
* @type integer
* @description undefined
*/
offset?: number;
/**
* @type integer
* @description undefined
*/
limit?: number;
};
export type ListLLMPricingRules200 = {
data: LlmpricingruletypesGettablePricingRulesDTO;
/**
* @type string
*/
status: string;
};
export type DeleteLLMPricingRulePathParameters = {
id: string;
};
export type GetLLMPricingRulePathParameters = {
id: string;
};
export type GetLLMPricingRule200 = {
data: LlmpricingruletypesLLMPricingRuleDTO;
/**
* @type string
*/
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
/**
* @type array

View File

@@ -15,6 +15,7 @@ import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
import { retryRequestAfterAuth } from 'api/interceptors';
import { Logout } from './utils';
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
@@ -129,13 +130,10 @@ export const interceptorRejected = async (
afterLogin(response.data.accessToken, response.data.refreshToken, true);
try {
const reResponse = await axios({
...value.config,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessToken}`,
},
});
const reResponse = await retryRequestAfterAuth(
value.config,
response.data.accessToken,
);
return await Promise.resolve(reResponse);
} catch (error) {

View File

@@ -1,46 +1,65 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import axios, { AxiosHeaders, AxiosResponse } from 'axios';
import { interceptorRejected } from './index';
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: jest.fn(() => 'mock-token'),
}));
jest.mock('api/v2/sessions/rotate/post', () => ({
__esModule: true,
default: jest.fn(() =>
const { retryRequestMock, postRotateMock } = vi.hoisted(() => ({
retryRequestMock: vi.fn(),
postRotateMock: vi.fn(() =>
Promise.resolve({
data: { accessToken: 'new-token', refreshToken: 'new-refresh' },
}),
),
}));
jest.mock('AppRoutes/utils', () => ({
vi.mock('api/interceptors', () => ({
__esModule: true,
default: jest.fn(),
retryRequestAfterAuth: retryRequestMock,
}));
jest.mock('axios', () => {
const actualAxios = jest.requireActual('axios');
const mockAxios = jest.fn().mockResolvedValue({ data: 'success' });
vi.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: vi.fn(() => 'mock-token'),
}));
vi.mock('api/v2/sessions/rotate/post', () => ({
__esModule: true,
default: postRotateMock,
}));
vi.mock('AppRoutes/utils', () => ({
__esModule: true,
default: vi.fn(),
}));
vi.mock('axios', async () => {
const actual = await vi.importActual<typeof import('axios')>('axios');
return {
...actualAxios,
default: Object.assign(mockAxios, {
...actualAxios.default,
isAxiosError: jest.fn().mockReturnValue(true),
create: actualAxios.create,
...actual,
default: Object.assign(actual.default, {
isAxiosError: vi.fn(() => true),
}),
__esModule: true,
};
});
describe('interceptorRejected', () => {
let interceptorRejected: (value: AxiosResponse) => Promise<AxiosResponse>;
beforeAll(async () => {
vi.resetModules();
const mod = await import('./index');
interceptorRejected = mod.interceptorRejected;
});
beforeEach(() => {
jest.clearAllMocks();
(axios as unknown as jest.Mock).mockResolvedValue({ data: 'success' });
(axios.isAxiosError as unknown as jest.Mock).mockReturnValue(true);
vi.clearAllMocks();
retryRequestMock.mockResolvedValue({
data: 'success',
} as unknown as AxiosResponse<{ data: string }>);
(
axios.isAxiosError as unknown as {
mockReturnValue: (value: boolean) => void;
}
).mockReturnValue(true);
});
it('should preserve array payload structure when retrying a 401 request', async () => {
@@ -75,11 +94,12 @@ describe('interceptorRejected', () => {
// Expected to reject after retry
}
const mockAxiosFn = axios as unknown as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(Array.isArray(JSON.parse(retryCallConfig.data))).toBe(true);
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(arrayPayload);
expect(retryRequestMock).toHaveBeenCalledTimes(1);
const retryCallConfig = retryRequestMock.mock.calls[0][0];
expect(Array.isArray(JSON.parse(retryCallConfig.data as string))).toBe(true);
expect(JSON.parse(retryCallConfig.data as string)).toStrictEqual(
arrayPayload,
);
});
it('should preserve object payload structure when retrying a 401 request', async () => {
@@ -111,10 +131,11 @@ describe('interceptorRejected', () => {
// Expected to reject after retry
}
const mockAxiosFn = axios as unknown as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(objectPayload);
expect(retryRequestMock).toHaveBeenCalledTimes(1);
const retryCallConfig = retryRequestMock.mock.calls[0][0];
expect(JSON.parse(retryCallConfig.data as string)).toStrictEqual(
objectPayload,
);
});
it('should handle undefined data gracefully when retrying', async () => {
@@ -144,9 +165,8 @@ describe('interceptorRejected', () => {
// Expected to reject after retry
}
const mockAxiosFn = axios as unknown as jest.Mock;
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(retryRequestMock).toHaveBeenCalledTimes(1);
const retryCallConfig = retryRequestMock.mock.calls[0][0];
expect(retryCallConfig.data).toBeUndefined();
});
});

View File

@@ -0,0 +1,17 @@
import axios, {
AxiosHeaders,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
export async function retryRequestAfterAuth(
valueConfig: InternalAxiosRequestConfig,
accessToken: string,
): Promise<AxiosResponse> {
const headers = new AxiosHeaders(valueConfig.headers);
headers.set('Authorization', `Bearer ${accessToken}`);
return axios({
...valueConfig,
headers,
});
}

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,

View File

@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
@@ -20,9 +22,9 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
jest.mock('lib/getStartEndRangeTime', () => ({
vi.mock('lib/getStartEndRangeTime', () => ({
__esModule: true,
default: jest.fn(() => ({ start: '100', end: '200' })),
default: vi.fn(() => ({ start: '100', end: '200' })),
}));
describe('prepareQueryRangePayloadV5', () => {
@@ -515,9 +517,7 @@ describe('prepareQueryRangePayloadV5', () => {
});
it('maps groupBy, order, having, aggregations and filter for logs builder query', () => {
const getStartEndRangeTime = jest.requireMock('lib/getStartEndRangeTime')
.default as jest.Mock;
getStartEndRangeTime.mockReturnValueOnce({
vi.mocked(getStartEndRangeTime).mockReturnValueOnce({
start: '1754623641',
end: '1754645241',
});

View File

@@ -1,15 +1,16 @@
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import getLocal from '../../../api/browser/localstorage/get';
import AppLoading from '../AppLoading';
jest.mock('../../../api/browser/localstorage/get', () => ({
vi.mock('../../../api/browser/localstorage/get', () => ({
__esModule: true,
default: jest.fn(),
default: vi.fn(),
}));
// Access the mocked function
const mockGet = getLocal as unknown as jest.Mock;
const mockGet = vi.mocked(getLocal);
describe('AppLoading', () => {
const SIGNOZ_TEXT = 'SigNoz';
@@ -18,12 +19,12 @@ describe('AppLoading', () => {
const CONTAINER_SELECTOR = '.app-loading-container';
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should render loading screen with dark theme by default', () => {
// Mock localStorage to return dark theme (or undefined for default)
mockGet.mockReturnValue(undefined);
mockGet.mockReturnValue(null);
render(<AppLoading />);
@@ -40,14 +41,17 @@ describe('AppLoading', () => {
it('should have proper structure and content', () => {
// Mock localStorage to return dark theme
mockGet.mockReturnValue(undefined);
mockGet.mockReturnValue(null);
render(<AppLoading />);
// Check for brand logo
const logo = screen.getByAltText(SIGNOZ_TEXT);
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute('src', 'test-file-stub');
expect(logo).toHaveAttribute(
'src',
expect.stringContaining('data:image/svg+xml'),
);
// Check for brand title
const title = screen.getByText(SIGNOZ_TEXT);

View File

@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { fireEvent, render, screen } from '@testing-library/react';
import { USER_PREFERENCES } from 'constants/userPreferences';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import {
ChangelogSchema,
DeploymentType,
} from 'types/api/changelog/getChangelogByVersion';
import { describe, expect, it, vi } from 'vitest';
import ChangelogModal from '../ChangelogModal';
@@ -37,27 +37,25 @@ const mockChangelog: ChangelogSchema = {
};
// Mock react-markdown to just render children as plain text
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
vi.mock('react-markdown', () => ({
default: function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
}));
// mock useAppContext
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({
updateUserPreferenceInContext: jest.fn(),
vi.mock('providers/App/App', () => ({
useAppContext: vi.fn(() => ({
updateUserPreferenceInContext: vi.fn(),
userPreferences: [
{
name: USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
name: 'last_seen_changelog_version',
value: 'v1.0.0',
},
],
})),
}));
function renderChangelog(onClose: () => void = jest.fn()): void {
function renderChangelog(onClose: () => void = vi.fn()): void {
render(
<MockQueryClientProvider>
<ChangelogModal changelog={mockChangelog} onClose={onClose} />
@@ -78,14 +76,14 @@ describe('ChangelogModal', () => {
});
it('calls onClose when Skip for now is clicked', () => {
const onClose = jest.fn();
const onClose = vi.fn();
renderChangelog(onClose);
fireEvent.click(screen.getByText('Skip for now'));
expect(onClose).toHaveBeenCalled();
});
it('opens migration docs when Update my workspace is clicked', () => {
window.open = jest.fn();
window.open = vi.fn();
renderChangelog();
fireEvent.click(screen.getByText('Update my workspace'));
expect(window.open).toHaveBeenCalledWith(
@@ -100,7 +98,7 @@ describe('ChangelogModal', () => {
const scrollBtn = screen.getByTestId('scroll-more-btn');
const contentDiv = screen.getByTestId('changelog-content');
if (contentDiv) {
contentDiv.scrollTo = jest.fn();
contentDiv.scrollTo = vi.fn();
}
fireEvent.click(scrollBtn);
if (contentDiv) {

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import {
ChangelogSchema,
DeploymentType,
@@ -9,13 +10,11 @@ import {
import ChangelogRenderer from '../components/ChangelogRenderer';
// Mock react-markdown to just render children as plain text
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
vi.mock('react-markdown', () => ({
default: function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
}));
const mockChangelog: ChangelogSchema = {
id: 1,

View File

@@ -1,16 +1,40 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import CodeBlock from './CodeBlock';
const mockCopyToClipboard = jest.fn();
const { mockCopyToClipboard } = vi.hoisted(() => ({
mockCopyToClipboard: vi.fn(),
}));
jest.mock('react-use', () => ({
vi.mock('react-use', () => ({
useCopyToClipboard: (): [unknown, (text: string) => void] => [
undefined,
mockCopyToClipboard,
],
}));
vi.mock('@signozhq/icons', () => ({
Check: (): null => null,
Copy: (): null => null,
}));
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Button: ({
prefix,
...props
}: {
prefix?: ReactNode;
[key: string]: unknown;
}): ReturnType<typeof React.createElement> =>
React.createElement('button', props, prefix),
};
});
describe('CodeBlock', () => {
beforeEach(() => {
mockCopyToClipboard.mockReset();
@@ -33,7 +57,7 @@ describe('CodeBlock', () => {
});
it('copies code and triggers callback', async () => {
const onCopy = jest.fn();
const onCopy = vi.fn();
render(<CodeBlock code="SELECT * FROM logs;" onCopy={onCopy} />);
fireEvent.click(screen.getByRole('button', { name: /copy code/i }));

View File

@@ -1,33 +0,0 @@
.error-state-container {
height: 240px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 3px;
.error-state-container-content {
display: flex;
flex-direction: column;
gap: 8px;
.error-state-text {
font-size: 14px;
font-weight: 500;
}
.error-state-additional-messages {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
.error-state-additional-text {
font-size: 12px;
font-weight: 400;
margin-left: 8px;
}
}
}
}

View File

@@ -1,59 +0,0 @@
import { Typography } from 'antd';
import APIError from '../../types/api/error';
import './Common.styles.scss';
interface ErrorStateComponentProps {
message?: string;
error?: APIError;
}
const defaultProps: Partial<ErrorStateComponentProps> = {
message: undefined,
error: undefined,
};
function ErrorStateComponent({
message,
error,
}: ErrorStateComponentProps): JSX.Element {
// Handle API Error object
if (error) {
const mainMessage = error.getErrorMessage();
const additionalErrors = error.getErrorDetails().error.errors || [];
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{mainMessage}</Typography>
{additionalErrors.length > 0 && (
<div className="error-state-additional-messages">
{additionalErrors.map((additionalError) => (
<Typography
key={`error-${additionalError.message}`}
className="error-state-additional-text"
>
{additionalError.message}
</Typography>
))}
</div>
)}
</div>
</div>
);
}
// Handle simple string message (backwards compatibility)
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{message}</Typography>
</div>
</div>
);
}
ErrorStateComponent.defaultProps = defaultProps;
export default ErrorStateComponent;

View File

@@ -1,28 +1,49 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
vi.mock('@signozhq/icons', () => ({
X: ({ size: _size }: any): JSX.Element => <span aria-hidden="true" />,
}));
const mockToast = jest.mocked(toast);
vi.mock('@signozhq/ui', () => ({
Button: ({
children,
loading: _loading,
variant: _variant,
color: _color,
...props
}: any): JSX.Element => <button {...props}>{children}</button>,
DialogFooter: ({ children, ...props }: any): JSX.Element => (
<div {...props}>{children}</div>
),
DialogWrapper: ({ title, open, children }: any): JSX.Element | null =>
open ? (
<div role="dialog" aria-label={title}>
{children}
</div>
) : null,
Input: (props: any): JSX.Element => <input {...props} />,
toast: { success: vi.fn(), error: vi.fn() },
}));
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const mockToast = vi.mocked(toast);
const showErrorModal = vi.hoisted(() => vi.fn());
vi.mock('providers/ErrorModalProvider', async () => ({
__esModule: true,
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
...(await vi.importActual('providers/ErrorModalProvider')),
useErrorModal: vi.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
@@ -40,7 +61,7 @@ function renderModal(): ReturnType<typeof render> {
describe('CreateServiceAccountModal', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
server.use(
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: {} })),
@@ -126,12 +147,16 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const dialog = await screen.findByRole('dialog', {
await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitForElementToBeRemoved(dialog);
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -1,36 +1,37 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import dayjs from 'dayjs';
import { describe, expect, it, vi } from 'vitest';
import * as timeUtils from 'utils/timeUtils';
import CustomTimePicker from './CustomTimePicker';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useLocation: jest.fn().mockReturnValue({
useLocation: vi.fn().mockReturnValue({
pathname: '/test-path',
}),
};
});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(() => jest.fn()),
useSelector: jest.fn(() => ({
vi.mock('react-redux', async () => ({
...(await vi.importActual('react-redux')),
useDispatch: vi.fn(() => vi.fn()),
useSelector: vi.fn(() => ({
minTime: 0,
maxTime: Date.now(),
})),
}));
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');
vi.mock('providers/Timezone', async () => {
const actual = await vi.importActual('providers/Timezone');
return {
...actual,
useTimezone: jest.fn().mockReturnValue({
useTimezone: vi.fn().mockReturnValue({
timezone: {
value: 'UTC',
offset: '+00:00',
@@ -45,6 +46,30 @@ jest.mock('providers/Timezone', () => {
};
});
vi.mock('@signozhq/ui', () => ({
Button: ({
children,
prefix,
variant: _variant,
color: _color,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
prefix?: React.ReactNode;
variant?: string;
color?: string;
}): JSX.Element => (
<button type="button" {...props}>
{prefix}
{children}
</button>
),
Calendar: (): JSX.Element => <div data-testid="mock-calendar" />,
}));
vi.mock('hooks/useZoomOut', () => ({
useZoomOut: vi.fn(() => vi.fn()),
}));
interface WrapperProps {
initialValue?: string;
showLiveLogs?: boolean;
@@ -123,8 +148,8 @@ describe('CustomTimePicker', () => {
});
it('applies valid shorthand on Enter', () => {
const onValid = jest.fn();
const onError = jest.fn();
const onValid = vi.fn();
const onError = vi.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
@@ -141,9 +166,9 @@ describe('CustomTimePicker', () => {
});
it('sets error and updates custom time status for invalid shorthand exceeding max allowed window', () => {
const onValid = jest.fn();
const onError = jest.fn();
const onCustomTimeStatusUpdate = jest.fn();
const onValid = vi.fn();
const onError = vi.fn();
const onCustomTimeStatusUpdate = vi.fn();
render(
<Wrapper
@@ -166,8 +191,8 @@ describe('CustomTimePicker', () => {
});
it('treats close after change like pressing Enter (blur + chevron)', () => {
const onValid = jest.fn();
const onError = jest.fn();
const onValid = vi.fn();
const onError = vi.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
@@ -191,8 +216,8 @@ describe('CustomTimePicker', () => {
});
it('applies epoch start/end range on Enter via onCustomDateHandler', () => {
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
const onCustomDateHandler = vi.fn();
const onError = vi.fn();
render(
<Wrapper onCustomDateHandler={onCustomDateHandler} onError={onError} />,
@@ -213,9 +238,9 @@ describe('CustomTimePicker', () => {
});
it('uses validateTimeRange result for generic formatted ranges (valid case)', () => {
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
const validateTimeRangeSpy = vi.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = vi.fn();
const onError = vi.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: true,
@@ -244,9 +269,9 @@ describe('CustomTimePicker', () => {
});
it('uses validateTimeRange result for generic formatted ranges (invalid case)', () => {
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onValid = jest.fn();
const onError = jest.fn();
const validateTimeRangeSpy = vi.spyOn(timeUtils, 'validateTimeRange');
const onValid = vi.fn();
const onError = vi.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: false,

View File

@@ -1,4 +0,0 @@
.custom-date-picker {
display: flex;
flex-direction: column;
}

View File

@@ -1,105 +0,0 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { DatePicker } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
LexicalContext,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import dayjs, { Dayjs } from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import './RangePickerModal.styles.scss';
interface RangePickerModalProps {
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
const {
setCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
selectedTime,
onTimeChange,
} = props;
const { RangePicker } = DatePicker;
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// Using any type here because antd's DatePicker expects its own internal Dayjs type
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
const disabledDate = (current: any): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
const { timezone } = useTimezone();
const rangeValue: [Dayjs, Dayjs] = useMemo(
() => [
dayjs(minTime / 1000_000).tz(timezone.value),
dayjs(maxTime / 1000_000).tz(timezone.value),
],
[maxTime, minTime, timezone.value],
);
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
format={(date: Dayjs): string =>
date.tz(timezone.value).format(DATE_TIME_FORMATS.ISO_DATETIME)
}
onOk={onModalOkHandler}
data-1p-ignore
{...(selectedTime === 'custom' &&
!onTimeChange && {
value: rangeValue,
})}
// use default value if onTimeChange is provided
{...(selectedTime === 'custom' &&
onTimeChange && {
defaultValue: rangeValue,
})}
/>
</div>
);
}
RangePickerModal.defaultProps = {
onTimeChange: undefined,
};
export default RangePickerModal;

View File

@@ -2,23 +2,33 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import CustomTimePicker from '../CustomTimePicker';
const MS_PER_MIN = 60 * 1000;
const NOW_MS = 1705312800000;
const mockDispatch = jest.fn();
const mockSafeNavigate = jest.fn();
const mockUrlQueryDelete = jest.fn();
const mockUrlQuerySet = jest.fn();
const {
MS_PER_MIN,
NOW_MS,
mockDispatch,
mockSafeNavigate,
mockUrlQueryDelete,
mockUrlQuerySet,
} = vi.hoisted(() => ({
MS_PER_MIN: 60 * 1000,
NOW_MS: 1705312800000,
mockDispatch: vi.fn(),
mockSafeNavigate: vi.fn(),
mockUrlQueryDelete: vi.fn(),
mockUrlQuerySet: vi.fn(),
}));
interface MockAppState {
globalTime: Pick<GlobalReducer, 'minTime' | 'maxTime'>;
}
jest.mock('react-redux', () => ({
useDispatch: (): jest.Mock => mockDispatch,
vi.mock('react-redux', () => ({
useDispatch: (): Mock => mockDispatch,
useSelector: (selector: (state: MockAppState) => unknown): unknown => {
const mockState: MockAppState = {
globalTime: {
@@ -30,8 +40,8 @@ jest.mock('react-redux', () => ({
},
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
@@ -43,7 +53,7 @@ interface MockUrlQuery {
toString: () => string;
}
jest.mock('hooks/useUrlQuery', () => ({
vi.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): MockUrlQuery => ({
delete: mockUrlQueryDelete,
@@ -53,26 +63,46 @@ jest.mock('hooks/useUrlQuery', () => ({
}),
}));
jest.mock('providers/Timezone', () => ({
vi.mock('providers/Timezone', () => ({
useTimezone: (): { timezone: { value: string; offset: string } } => ({
timezone: { value: 'UTC', offset: 'UTC' },
}),
}));
jest.mock('react-router-dom', () => ({
vi.mock('react-router-dom', () => ({
useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
}));
vi.mock('@signozhq/ui', () => ({
Button: ({
children,
prefix,
variant: _variant,
color: _color,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
prefix?: React.ReactNode;
variant?: string;
color?: string;
}): JSX.Element => (
<button type="button" {...props}>
{prefix}
{children}
</button>
),
Calendar: (): JSX.Element => <div data-testid="mock-calendar" />,
}));
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const now = Date.now();
const defaultProps = {
onSelect: jest.fn(),
onError: jest.fn(),
onSelect: vi.fn(),
onError: vi.fn(),
selectedValue: '15m',
selectedTime: '15m',
onValidCustomDateChange: jest.fn(),
onValidCustomDateChange: vi.fn(),
open: false,
setOpen: jest.fn(),
setOpen: vi.fn(),
items: [
{ value: '15m', label: 'Last 15 minutes' },
{ value: '1h', label: 'Last 1 hour' },
@@ -83,12 +113,12 @@ const defaultProps = {
describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
vi.clearAllMocks();
vi.spyOn(Date, 'now').mockReturnValue(NOW_MS);
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('should render zoom out button when showLiveLogs is false', () => {

View File

@@ -1,93 +0,0 @@
.details-drawer {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--l1-border);
}
.ant-drawer-header {
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
.ant-drawer-header-title {
display: flex;
align-items: center;
.ant-drawer-close {
margin-inline-end: 0px;
padding: 0px;
padding-right: 16px;
border-right: 1px solid var(--l1-border);
}
.ant-drawer-title {
padding-left: 16px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.ant-drawer-body {
padding: 16px;
background: var(--l2-background);
&::-webkit-scrollbar {
width: 0.1rem;
}
}
.details-drawer-tabs {
margin-top: 32px;
.ant-tabs-tab {
display: flex;
align-items: center;
justify-content: center;
width: 114px;
height: 32px;
flex-shrink: 0;
padding: 7px 20px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
color: var(--l1-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
.ant-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0px;
}
.ant-btn:hover {
background: unset;
}
}
.ant-tabs-tab-active {
background: var(--l3-background);
}
.ant-tabs-tab + .ant-tabs-tab {
margin-left: 0px;
}
.ant-tabs-nav::before {
border-bottom: 0px;
}
.ant-tabs-ink-bar {
background: none;
}
}
}

View File

@@ -1,57 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { Drawer, Tabs, TabsProps } from 'antd';
import cx from 'classnames';
import './DetailsDrawer.styles.scss';
interface IDetailsDrawerProps {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
title: string;
descriptiveContent: JSX.Element;
defaultActiveKey: string;
items: TabsProps['items'];
detailsDrawerClassName?: string;
tabBarExtraContent?: JSX.Element;
}
function DetailsDrawer(props: IDetailsDrawerProps): JSX.Element {
const {
open,
setOpen,
title,
descriptiveContent,
defaultActiveKey,
detailsDrawerClassName,
items,
tabBarExtraContent,
} = props;
return (
<Drawer
width="60%"
open={open}
afterOpenChange={setOpen}
mask={false}
title={title}
onClose={(): void => setOpen(false)}
className="details-drawer"
>
<div>{descriptiveContent}</div>
<Tabs
items={items}
addIcon
defaultActiveKey={defaultActiveKey}
animated
className={cx('details-drawer-tabs', detailsDrawerClassName)}
tabBarExtraContent={tabBarExtraContent}
/>
</Drawer>
);
}
DetailsDrawer.defaultProps = {
detailsDrawerClassName: '',
tabBarExtraContent: null,
};
export default DetailsDrawer;

View File

@@ -1,3 +1,6 @@
import type { Mock } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
@@ -13,25 +16,28 @@ import '@testing-library/jest-dom';
import { DownloadFormats, DownloadRowCounts } from './constants';
import DownloadOptionsMenu from './DownloadOptionsMenu';
const mockDownloadExportData = jest.fn().mockResolvedValue(undefined);
jest.mock('api/v1/download/downloadExportData', () => ({
const { mockDownloadExportData, mockUseQueryBuilder } = vi.hoisted(() => ({
mockDownloadExportData: vi.fn().mockResolvedValue(undefined),
mockUseQueryBuilder: vi.fn(),
}));
vi.mock('api/v1/download/downloadExportData', () => ({
downloadExportData: (...args: any[]): any => mockDownloadExportData(...args),
default: (...args: any[]): any => mockDownloadExportData(...args),
}));
jest.mock('antd', () => {
const actual = jest.requireActual('antd');
vi.mock('antd', async () => {
const actual = await vi.importActual<typeof import('antd')>('antd');
return {
...actual,
message: {
success: jest.fn(),
error: jest.fn(),
success: vi.fn(),
error: vi.fn(),
},
};
});
const mockUseQueryBuilder = jest.fn();
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
vi.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): any => mockUseQueryBuilder(),
}));
@@ -95,8 +101,8 @@ describe.each([
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
(message.success as jest.Mock).mockReset();
(message.error as jest.Mock).mockReset();
(message.success as Mock).mockReset();
(message.error as Mock).mockReset();
mockUseQueryBuilder.mockReturnValue({
stagedQuery: createMockStagedQuery(dataSource),
});
@@ -307,7 +313,11 @@ describe.each([
fireEvent.click(screen.getByText('Export'));
expect(screen.getByTestId(testId)).toBeDisabled();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.getByRole('dialog').closest('.ant-popover')).toHaveStyle({
pointerEvents: 'none',
});
});
resolveDownload!();
@@ -323,7 +333,7 @@ describe('DownloadOptionsMenu for traces with queryTraceOperator', () => {
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
(message.success as jest.Mock).mockReset();
(message.success as Mock).mockReset();
});
it('applies limit and clears groupBy on queryTraceOperator entries', async () => {

View File

@@ -1,27 +1,28 @@
import { render } from '@testing-library/react';
import { Table } from 'antd';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import DraggableTableRow from '..';
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
vi.mock('react-dnd', () => ({
useDrop: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
useDrag: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
}));
describe('DraggableTableRow Snapshot test', () => {

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
exports[`DraggableTableRow Snapshot test > should render DraggableTableRow 1`] = `
<DocumentFragment>
<div
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"

View File

@@ -1,14 +1,16 @@
import { describe, expect, it, vi } from 'vitest';
import { dragHandler, dropHandler } from '../utils';
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
vi.mock('react-dnd', () => ({
useDrop: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
useDrag: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
}));
describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return true', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(true),
isOver: vi.fn().mockReturnValueOnce(true),
} as never;
const dropDataTruthy = dropHandler(monitor);
@@ -17,7 +19,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return false', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(false),
isOver: vi.fn().mockReturnValueOnce(false),
} as never;
const dropDataFalsy = dropHandler(monitor);
@@ -26,7 +28,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dragHandler return true', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(true),
isDragging: vi.fn().mockReturnValueOnce(true),
} as never;
const dragDataTruthy = dragHandler(monitor);
@@ -35,7 +37,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dragHandler return false', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(false),
isDragging: vi.fn().mockReturnValueOnce(false),
} as never;
const dragDataFalsy = dragHandler(monitor);

View File

@@ -46,6 +46,7 @@ function DeleteMemberDialog({
color="destructive"
disabled={isDeleting}
onClick={onConfirm}
loading={isDeleting}
>
<Trash2 size={12} />
{isDeleting ? 'Processing...' : title}
@@ -63,7 +64,6 @@ function DeleteMemberDialog({
}}
title={title}
width="narrow"
className="alert-dialog delete-dialog"
showCloseButton={false}
disableOutsideClick={false}
footer={footer}

View File

@@ -28,18 +28,6 @@
cursor: default;
}
&__input {
height: 32px;
background: var(--l2-background);
border-color: var(--l1-border);
color: var(--l1-foreground);
box-shadow: none;
&::placeholder {
color: var(--l3-foreground);
}
}
&__input-wrapper {
display: flex;
align-items: center;
@@ -48,7 +36,7 @@
padding: var(--padding-1) var(--padding-2);
border-radius: 2px;
background: var(--l2-background);
border: 1px solid var(--l1-border);
border: 1px solid var(--border);
box-sizing: border-box;
&--disabled {
@@ -65,8 +53,8 @@
}
&__email-text {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
@@ -178,36 +166,6 @@
}
}
.delete-dialog {
background: var(--l2-background);
border: 1px solid var(--l1-border);
[data-slot='dialog-title'] {
color: var(--l1-foreground);
}
&__body {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.065px;
margin: 0;
strong {
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
}
&__footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-4);
margin-top: var(--margin-6);
}
}
.reset-link-dialog {
background: var(--l2-background);
border: 1px solid var(--l1-border);
@@ -264,13 +222,6 @@
}
&__copy-btn {
flex-shrink: 0;
height: 32px;
border-radius: 0 2px 2px 0;
border-top: none;
border-right: none;
border-bottom: none;
border-left: 1px solid var(--l1-border);
min-width: 64px;
}
}

View File

@@ -224,7 +224,7 @@ function EditMemberDrawer({
try {
await rawRetry();
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
refetchUser();
void refetchUser();
} catch (err) {
setSaveErrors((prev) =>
prev.map((e) =>
@@ -250,7 +250,7 @@ function EditMemberDrawer({
});
}
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
refetchUser();
void refetchUser();
} catch (err) {
setSaveErrors((prev) =>
prev.map((e) =>
@@ -319,7 +319,7 @@ function EditMemberDrawer({
}),
];
});
refetchUser();
void refetchUser();
},
});
} else {
@@ -340,7 +340,7 @@ function EditMemberDrawer({
onComplete();
}
refetchUser();
void refetchUser();
} finally {
setIsSaving(false);
}
@@ -465,7 +465,6 @@ function EditMemberDrawer({
prev.filter((err) => err.context !== 'Name update'),
);
}}
className="edit-member-drawer__input"
placeholder="Enter name"
disabled={isRootUser || isDeleted}
/>
@@ -631,7 +630,7 @@ function EditMemberDrawer({
</div>
<div className="edit-member-drawer__footer-right">
<Button variant="solid" color="secondary" onClick={handleClose}>
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
@@ -641,6 +640,7 @@ function EditMemberDrawer({
color="primary"
disabled={!isDirty || isSaving || isRootUser}
onClick={handleSave}
loading={isSaving}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>

View File

@@ -44,9 +44,8 @@ function ResetLinkDialog({
<span className="reset-link-dialog__link-text">{resetLink}</span>
</div>
<Button
variant="outlined"
variant="link"
color="secondary"
size="sm"
onClick={onCopy}
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
className="reset-link-dialog__copy-btn"

View File

@@ -1,6 +1,9 @@
import type { ReactNode } from 'react';
import type { ChangeEventHandler, ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import { toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import {
useCreateResetPasswordToken,
useDeleteUser,
@@ -15,88 +18,138 @@ import {
listRolesSuccessResponse,
managedRoles,
} from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useGetUser: jest.fn(),
useUpdateUser: jest.fn(),
useUpdateMyUserV2: jest.fn(),
useSetRoleByUserID: jest.fn(),
useGetResetPasswordToken: jest.fn(),
useCreateResetPasswordToken: jest.fn(),
vi.mock('api/generated/services/role', async () => {
const actual = await vi.importActual<
typeof import('api/generated/services/role')
>('api/generated/services/role');
return {
...actual,
useListRoles: vi.fn(),
};
});
vi.mock('api/generated/services/users', () => ({
useDeleteUser: vi.fn(),
useGetUser: vi.fn(),
useUpdateUser: vi.fn(),
useUpdateMyUserV2: vi.fn(),
useSetRoleByUserID: vi.fn(),
useGetResetPasswordToken: vi.fn(),
useCreateResetPasswordToken: vi.fn(),
}));
jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: jest.fn(),
vi.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: vi.fn(),
}));
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
DialogWrapper: ({
children,
footer,
open,
title,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
title?: string;
}): JSX.Element | null =>
open ? (
<div role="dialog" aria-label={title}>
{children}
{footer}
</div>
) : null,
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
<div>{children}</div>
),
toast: {
success: jest.fn(),
error: jest.fn(),
},
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): Mock => vi.fn(),
}));
const mockCopyToClipboard = jest.fn();
const mockCopyState = { value: undefined, error: undefined };
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
jest.mock('react-use', () => ({
return {
Badge: ({ children }: { children?: ReactNode }): JSX.Element =>
React.createElement('span', null, children),
Button: ({
children,
disabled,
onClick,
prefix,
}: {
children?: ReactNode;
disabled?: boolean;
onClick?: () => void;
prefix?: ReactNode;
}): JSX.Element =>
React.createElement('button', { disabled, onClick }, prefix, children),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? React.createElement('div', null, children, footer) : null,
DialogWrapper: ({
children,
footer,
open,
title,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
title?: string;
}): JSX.Element | null =>
open
? React.createElement(
'div',
{ role: 'dialog', 'aria-label': title },
children,
footer,
)
: null,
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element =>
React.createElement('div', null, children),
Input: ({
disabled,
id,
onChange,
placeholder,
value,
}: {
disabled?: boolean;
id?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
placeholder?: string;
value?: string;
}): JSX.Element =>
React.createElement('input', {
disabled,
id,
onChange,
placeholder,
value,
}),
toast: {
success: vi.fn(),
error: vi.fn(),
},
};
});
const { mockCopyToClipboard, mockCopyState, showErrorModal } = vi.hoisted(
() => ({
mockCopyToClipboard: vi.fn(),
mockCopyState: { value: undefined, error: undefined },
showErrorModal: vi.fn(),
}),
);
vi.mock('react-use', () => ({
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
mockCopyState,
mockCopyToClipboard,
],
}));
const ROLES_ENDPOINT = '*/api/v1/roles';
const mockDeleteMutate = vi.fn();
const mockCreateTokenMutateAsync = vi.fn();
const mockDeleteMutate = jest.fn();
const mockCreateTokenMutateAsync = jest.fn();
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
vi.mock('providers/ErrorModalProvider', async () => ({
__esModule: true,
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
...(await vi.importActual<typeof import('providers/ErrorModalProvider')>(
'providers/ErrorModalProvider',
)),
useErrorModal: vi.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
@@ -155,8 +208,8 @@ function renderDrawer(
<EditMemberDrawer
member={activeMember}
open
onClose={jest.fn()}
onComplete={jest.fn()}
onClose={vi.fn()}
onComplete={vi.fn()}
{...props}
/>,
);
@@ -164,38 +217,43 @@ function renderDrawer(
describe('EditMemberDrawer', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockCopyState.value = undefined;
mockCopyState.error = undefined;
showErrorModal.mockClear();
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
(useGetUser as jest.Mock).mockReturnValue({
(useListRoles as Mock).mockReturnValue({
data: listRolesSuccessResponse,
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
isFetching: false,
isSuccess: true,
status: 'success',
});
(useGetUser as Mock).mockReturnValue({
data: mockFetchedUser,
isLoading: false,
refetch: jest.fn(),
refetch: vi.fn(),
});
(useUpdateUser as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
(useUpdateUser as Mock).mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
isLoading: false,
});
(useUpdateMyUserV2 as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
(useUpdateMyUserV2 as Mock).mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
isLoading: false,
});
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
(useSetRoleByUserID as Mock).mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
isLoading: false,
});
(useDeleteUser as jest.Mock).mockReturnValue({
(useDeleteUser as Mock).mockReturnValue({
mutate: mockDeleteMutate,
isLoading: false,
});
// Token query: valid token for invited members
(useGetResetPasswordToken as jest.Mock).mockReturnValue({
(useGetResetPasswordToken as Mock).mockReturnValue({
data: {
data: {
token: 'invite-tok-valid',
@@ -215,20 +273,18 @@ describe('EditMemberDrawer', () => {
expiresAt: new Date(Date.now() + 86400000).toISOString(),
},
});
(useCreateResetPasswordToken as jest.Mock).mockReturnValue({
(useCreateResetPasswordToken as Mock).mockReturnValue({
mutateAsync: mockCreateTokenMutateAsync,
isLoading: false,
});
});
afterEach(() => {
server.resetHandlers();
});
it('renders active member details and disables Save when form is not dirty', () => {
it('renders active member details and disables Save when form is not dirty', async () => {
renderDrawer();
expect(screen.getByDisplayValue('Alice Smith')).toBeInTheDocument();
await expect(
screen.findByDisplayValue('Alice Smith'),
).resolves.toBeInTheDocument();
expect(screen.getByText('alice@signoz.io')).toBeInTheDocument();
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
expect(
@@ -237,11 +293,11 @@ describe('EditMemberDrawer', () => {
});
it('enables Save after editing name and calls updateUser on confirm', async () => {
const onComplete = jest.fn();
const onComplete = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockMutateAsync = jest.fn().mockResolvedValue({});
const mockMutateAsync = vi.fn().mockResolvedValue({});
(useUpdateUser as jest.Mock).mockReturnValue({
(useUpdateUser as Mock).mockReturnValue({
mutateAsync: mockMutateAsync,
isLoading: false,
});
@@ -267,7 +323,7 @@ describe('EditMemberDrawer', () => {
});
it('does not close the drawer after a successful save', async () => {
const onClose = jest.fn();
const onClose = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer({ onClose });
@@ -289,18 +345,18 @@ describe('EditMemberDrawer', () => {
});
it('selecting a different role calls setRole with the new role name', async () => {
const onComplete = jest.fn();
const onComplete = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockSet = jest.fn().mockResolvedValue({});
const mockSet = vi.fn().mockResolvedValue({});
(useSetRoleByUserID as jest.Mock).mockReturnValue({
(useSetRoleByUserID as Mock).mockReturnValue({
mutateAsync: mockSet,
isLoading: false,
});
renderDrawer({ onComplete });
// Open the roles dropdown and select signoz-editor
await screen.findByTitle('signoz-admin');
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-editor'));
@@ -318,18 +374,18 @@ describe('EditMemberDrawer', () => {
});
it('does not call removeRole when the role is changed', async () => {
const onComplete = jest.fn();
const onComplete = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockSet = jest.fn().mockResolvedValue({});
const mockSet = vi.fn().mockResolvedValue({});
(useSetRoleByUserID as jest.Mock).mockReturnValue({
(useSetRoleByUserID as Mock).mockReturnValue({
mutateAsync: mockSet,
isLoading: false,
});
renderDrawer({ onComplete });
// Switch from signoz-admin to signoz-viewer using single-select
await screen.findByTitle('signoz-admin');
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-viewer'));
@@ -347,10 +403,10 @@ describe('EditMemberDrawer', () => {
});
it('shows delete confirm dialog and calls deleteUser for active members', async () => {
const onComplete = jest.fn();
const onComplete = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
(useDeleteUser as Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
@@ -393,7 +449,7 @@ describe('EditMemberDrawer', () => {
});
it('shows "Regenerate Invite Link" when token is expired', () => {
(useGetResetPasswordToken as jest.Mock).mockReturnValue({
(useGetResetPasswordToken as Mock).mockReturnValue({
data: {
data: {
token: 'old-tok',
@@ -413,7 +469,7 @@ describe('EditMemberDrawer', () => {
});
it('shows "Generate Invite Link" when no token exists', () => {
(useGetResetPasswordToken as jest.Mock).mockReturnValue({
(useGetResetPasswordToken as Mock).mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
@@ -427,10 +483,10 @@ describe('EditMemberDrawer', () => {
});
it('calls deleteUser after confirming revoke invite for invited members', async () => {
const onComplete = jest.fn();
const onComplete = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
(useDeleteUser as Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
@@ -457,11 +513,11 @@ describe('EditMemberDrawer', () => {
});
it('calls updateUser when saving name change for an invited member', async () => {
const onComplete = jest.fn();
const onComplete = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockMutateAsync = jest.fn().mockResolvedValue({});
const mockMutateAsync = vi.fn().mockResolvedValue({});
(useGetUser as jest.Mock).mockReturnValue({
(useGetUser as Mock).mockReturnValue({
data: {
data: {
...mockFetchedUser.data,
@@ -477,9 +533,9 @@ describe('EditMemberDrawer', () => {
},
},
isLoading: false,
refetch: jest.fn(),
refetch: vi.fn(),
});
(useUpdateUser as jest.Mock).mockReturnValue({
(useUpdateUser as Mock).mockReturnValue({
mutateAsync: mockMutateAsync,
isLoading: false,
});
@@ -504,7 +560,7 @@ describe('EditMemberDrawer', () => {
});
describe('error handling', () => {
const mockConvertToApiError = jest.mocked(convertToApiError);
const mockConvertToApiError = vi.mocked(convertToApiError);
beforeEach(() => {
mockConvertToApiError.mockReturnValue({
@@ -515,8 +571,8 @@ describe('EditMemberDrawer', () => {
it('shows SaveErrorItem when updateUser fails for name change', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useUpdateUser as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockRejectedValue(new Error('server error')),
(useUpdateUser as Mock).mockReturnValue({
mutateAsync: vi.fn().mockRejectedValue(new Error('server error')),
isLoading: false,
});
@@ -540,7 +596,7 @@ describe('EditMemberDrawer', () => {
it('shows API error message when deleteUser fails for active member', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
(useDeleteUser as Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),
@@ -571,7 +627,7 @@ describe('EditMemberDrawer', () => {
it('shows API error message when deleteUser fails for invited member', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
(useDeleteUser as Mock).mockImplementation((options) => ({
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),
@@ -634,10 +690,10 @@ describe('EditMemberDrawer', () => {
describe('root user', () => {
beforeEach(() => {
(useGetUser as jest.Mock).mockReturnValue({
(useGetUser as Mock).mockReturnValue({
data: rootMockFetchedUser,
isLoading: false,
refetch: jest.fn(),
refetch: vi.fn(),
});
});
@@ -717,7 +773,7 @@ describe('EditMemberDrawer', () => {
it('copies the link to clipboard and shows "Copied!" on the button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockToast = jest.mocked(toast);
const mockToast = vi.mocked(toast);
renderDrawer();

View File

@@ -1,10 +1,39 @@
import { render, screen } from '@testing-library/react';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import Editor from './index';
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: jest.fn(),
vi.mock('hooks/useDarkMode', () => ({
useIsDarkMode: vi.fn(),
}));
vi.mock('@monaco-editor/react', () => ({
default: ({ height }: { height?: string }): JSX.Element => (
<section
style={{
display: 'flex',
position: 'relative',
textAlign: 'initial',
width: '100%',
height,
}}
>
<div
style={{
display: 'flex',
height: '100%',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
Loading...
</div>
<div style={{ width: '100%', display: 'none' }} />
</section>
),
}));
describe('Editor', () => {
@@ -34,7 +63,7 @@ describe('Editor', () => {
});
it('renders with dark mode theme', () => {
(useIsDarkMode as jest.Mock).mockImplementation(() => true);
(useIsDarkMode as Mock).mockImplementation(() => true);
const { container } = render(<Editor value="dark mode text" />);
@@ -42,7 +71,7 @@ describe('Editor', () => {
});
it('renders with light mode theme', () => {
(useIsDarkMode as jest.Mock).mockImplementation(() => false);
(useIsDarkMode as Mock).mockImplementation(() => false);
const { container } = render(<Editor value="light mode text" />);

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Editor renders correctly with custom props 1`] = `
exports[`Editor > renders correctly with custom props 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 50vh;"
@@ -17,7 +17,7 @@ exports[`Editor renders correctly with custom props 1`] = `
</div>
`;
exports[`Editor renders correctly with default props 1`] = `
exports[`Editor > renders correctly with default props 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
@@ -34,7 +34,7 @@ exports[`Editor renders correctly with default props 1`] = `
</div>
`;
exports[`Editor renders with dark mode theme 1`] = `
exports[`Editor > renders with dark mode theme 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
@@ -51,7 +51,7 @@ exports[`Editor renders with dark mode theme 1`] = `
</div>
`;
exports[`Editor renders with light mode theme 1`] = `
exports[`Editor > renders with light mode theme 1`] = `
<div>
<section
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"

View File

@@ -1,13 +1,22 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import withErrorBoundary, {
WithErrorBoundaryOptions,
} from '../withErrorBoundary';
// Mock dependencies before imports
jest.mock('@sentry/react', () => {
const ReactMock = jest.requireActual('react');
vi.mock('@sentry/react', async () => {
const ReactMock = await vi.importActual<typeof import('react')>('react');
class MockErrorBoundary extends ReactMock.Component<
{
@@ -34,8 +43,8 @@ jest.mock('@sentry/react', () => {
const { beforeCapture, onError } = this.props;
if (beforeCapture) {
const mockScope = {
setTag: jest.fn(),
setLevel: jest.fn(),
setTag: vi.fn(),
setLevel: vi.fn(),
};
beforeCapture(mockScope);
}
@@ -64,15 +73,11 @@ jest.mock('@sentry/react', () => {
};
});
jest.mock(
'../../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback',
() =>
function MockErrorBoundaryFallback(): JSX.Element {
return (
<div data-testid="default-error-fallback">Default Error Fallback</div>
);
},
);
vi.mock('../../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback', () => ({
default: function MockErrorBoundaryFallback(): JSX.Element {
return <div data-testid="default-error-fallback">Default Error Fallback</div>;
},
}));
// Test component that can throw errors
interface TestComponentProps {
@@ -105,7 +110,7 @@ describe('withErrorBoundary', () => {
// Suppress console errors for cleaner test output
const originalError = console.error;
beforeAll(() => {
console.error = jest.fn();
console.error = vi.fn();
});
afterAll(() => {
@@ -113,7 +118,7 @@ describe('withErrorBoundary', () => {
});
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should wrap component with ErrorBoundary and render successfully', () => {
@@ -162,7 +167,7 @@ describe('withErrorBoundary', () => {
it('should call custom error handler when error occurs', () => {
// Arrange
const mockErrorHandler = jest.fn();
const mockErrorHandler = vi.fn();
const options: WithErrorBoundaryOptions = {
onError: mockErrorHandler,
};

View File

@@ -1,143 +0,0 @@
import { useState } from 'react';
import { Button } from 'antd';
import { withErrorBoundary } from './index';
/**
* Example component that can throw errors
*/
function ProblematicComponent(): JSX.Element {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
throw new Error('This is a test error from ProblematicComponent!');
}
return (
<div style={{ padding: '20px' }}>
<h3>Problematic Component</h3>
<p>This component can throw errors when the button is clicked.</p>
<Button type="primary" onClick={(): void => setShouldThrow(true)} danger>
Trigger Error
</Button>
</div>
);
}
/**
* Basic usage - wraps component with default error boundary
*/
export const SafeProblematicComponent = withErrorBoundary(ProblematicComponent);
/**
* Usage with custom fallback component
*/
function CustomErrorFallback(): JSX.Element {
return (
<div
style={{ padding: '20px', border: '1px solid red', borderRadius: '4px' }}
>
<h4 style={{ color: 'red' }}>Custom Error Fallback</h4>
<p>Something went wrong in this specific component!</p>
<Button onClick={(): void => window.location.reload()}>Reload Page</Button>
</div>
);
}
export const SafeProblematicComponentWithCustomFallback = withErrorBoundary(
ProblematicComponent,
{
fallback: <CustomErrorFallback />,
},
);
/**
* Usage with custom error handler
*/
export const SafeProblematicComponentWithErrorHandler = withErrorBoundary(
ProblematicComponent,
{
onError: (error, errorInfo) => {
console.error('Custom error handler:', error);
console.error('Error info:', errorInfo);
// You could also send to analytics, logging service, etc.
},
sentryOptions: {
tags: {
section: 'dashboard',
priority: 'high',
},
level: 'error',
},
},
);
/**
* Example of wrapping an existing component from the codebase
*/
function ExistingComponent({
title,
data,
}: {
title: string;
data: any[];
}): JSX.Element {
// This could be any existing component that might throw errors
return (
<div>
<h4>{title}</h4>
<ul>
{data.map((item, index) => (
// eslint-disable-next-line react/no-array-index-key
<li key={index}>{item.name}</li>
))}
</ul>
</div>
);
}
export const SafeExistingComponent = withErrorBoundary(ExistingComponent, {
sentryOptions: {
tags: {
component: 'ExistingComponent',
feature: 'data-display',
},
},
});
/**
* Usage examples in a container component
*/
export function ErrorBoundaryExamples(): JSX.Element {
const sampleData = [
{ name: 'Item 1' },
{ name: 'Item 2' },
{ name: 'Item 3' },
];
return (
<div style={{ padding: '20px' }}>
<h2>Error Boundary HOC Examples</h2>
<div style={{ marginBottom: '20px' }}>
<h3>1. Basic Usage</h3>
<SafeProblematicComponent />
</div>
<div style={{ marginBottom: '20px' }}>
<h3>2. With Custom Fallback</h3>
<SafeProblematicComponentWithCustomFallback />
</div>
<div style={{ marginBottom: '20px' }}>
<h3>3. With Custom Error Handler</h3>
<SafeProblematicComponentWithErrorHandler />
</div>
<div style={{ marginBottom: '20px' }}>
<h3>4. Wrapped Existing Component</h3>
<SafeExistingComponent title="Sample Data" data={sampleData} />
</div>
</div>
);
}

View File

@@ -1,19 +1,26 @@
import { describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import APIError from 'types/api/error';
import ErrorModal from './ErrorModal';
// Mock the query client to return version data
const mockVersionData = {
const mockVersionData = vi.hoisted(() => ({
payload: {
ee: 'Y',
version: '1.0.0',
},
};
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
}));
vi.mock('react-query', async () => ({
...(await vi.importActual('react-query')),
useQueryClient: (): { getQueryData: () => typeof mockVersionData } => ({
getQueryData: jest.fn(() => mockVersionData),
getQueryData: vi.fn(() => mockVersionData),
}),
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const mockError: APIError = new APIError({
@@ -31,7 +38,7 @@ const mockError: APIError = new APIError({
});
describe('ErrorModal Component', () => {
it('should render the modal when open is true', () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
// Check if the error message is displayed
expect(screen.getByText('An error occurred')).toBeInTheDocument();
@@ -41,14 +48,14 @@ describe('ErrorModal Component', () => {
});
it('should not render the modal when open is false', () => {
render(<ErrorModal error={mockError} open={false} onClose={jest.fn()} />);
render(<ErrorModal error={mockError} open={false} onClose={vi.fn()} />);
// Check that the modal content is not in the document
expect(screen.queryByText('An error occurred')).not.toBeInTheDocument();
});
it('should call onClose when the close button is clicked', async () => {
const onCloseMock = jest.fn();
const onCloseMock = vi.fn();
render(<ErrorModal error={mockError} open onClose={onCloseMock} />);
// Click the close button
@@ -61,14 +68,14 @@ describe('ErrorModal Component', () => {
});
it('should display version data if available', async () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
// Check if the version data is displayed
expect(screen.getByText('ENTERPRISE')).toBeInTheDocument();
expect(screen.getByText('1.0.0')).toBeInTheDocument();
});
it('should render the messages count badge when there are multiple errors', () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
// Check if the messages count badge is displayed
expect(screen.getByText('MESSAGES')).toBeInTheDocument();
@@ -82,7 +89,7 @@ describe('ErrorModal Component', () => {
});
it('should render the open docs button when URL is provided', async () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
// Check if the open docs button is displayed
const openDocsButton = screen.getByTestId('error-docs-button');
@@ -95,7 +102,7 @@ describe('ErrorModal Component', () => {
});
it('should not display scroll for more if there are less than 10 messages', () => {
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
expect(screen.queryByText('Scroll for more')).not.toBeInTheDocument();
});
@@ -113,7 +120,7 @@ describe('ErrorModal Component', () => {
},
});
render(<ErrorModal error={longError} open onClose={jest.fn()} />);
render(<ErrorModal error={longError} open onClose={vi.fn()} />);
// Check if the scroll hint is displayed
expect(screen.getByText('Scroll for more')).toBeInTheDocument();
@@ -125,7 +132,7 @@ it('should render the trigger component if provided', () => {
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={jest.fn()}
onClose={vi.fn()}
/>,
);
@@ -139,7 +146,7 @@ it('should open the modal when the trigger component is clicked', async () => {
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={jest.fn()}
onClose={vi.fn()}
/>,
);
@@ -153,14 +160,14 @@ it('should open the modal when the trigger component is clicked', async () => {
});
it('should render the default trigger tag if no trigger component is provided', () => {
render(<ErrorModal error={mockError} onClose={jest.fn()} />);
render(<ErrorModal error={mockError} onClose={vi.fn()} />);
// Check if the default trigger tag is rendered
expect(screen.getByText('error')).toBeInTheDocument();
});
it('should close the modal when the onCancel event is triggered', async () => {
const onCloseMock = jest.fn();
const onCloseMock = vi.fn();
render(<ErrorModal error={mockError} onClose={onCloseMock} />);
// Click the trigger component
@@ -179,9 +186,7 @@ it('should close the modal when the onCancel event is triggered', async () => {
expect(onCloseMock).toHaveBeenCalledTimes(1);
await waitFor(() => {
// check if the modal is not visible
const modal = document.getElementsByClassName('ant-modal');
const style = window.getComputedStyle(modal[0]);
expect(style.display).toBe('none');
expect(modal[0]).toHaveClass('ant-zoom-leave');
});
});

View File

@@ -3,57 +3,63 @@ import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { describe, expect, it, vi } from 'vitest';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = jest.fn();
const historyReplace = vi.hoisted(() => vi.fn());
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
vi.mock('react-router-dom', async () => {
const actual =
await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
jest.mock('hooks/useSafeNavigate', () => ({
return {
...actual,
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...actual.useHistory(),
replace: historyReplace,
}),
};
});
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
safeNavigate: vi.fn(),
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
vi.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: vi.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: jest.fn(() => ({
vi.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: vi.fn(() => ({
data: { data: { data: viewMockData } },
isLoading: false,
error: null,
isRefetching: false,
refetch: jest.fn(),
refetch: vi.fn(),
})),
}));
jest.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: jest.fn(() => ({
mutateAsync: jest.fn(),
vi.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
}));
jest.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: jest.fn(() => ({
mutateAsync: jest.fn(),
vi.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
@@ -66,8 +72,8 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
updateColumns: vi.fn(),
updateFormatting: vi.fn(),
}),
}));

View File

@@ -2,21 +2,22 @@ import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { describe, expect, it, vi } from 'vitest';
import { viewMockData } from '../__mock__/viewData';
import MenuItemGenerator from '../MenuItemGenerator';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
vi.mock('antd', async () => ({
...(await vi.importActual('antd')),
useForm: vi.fn().mockReturnValue({
onFinish: vi.fn(),
}),
}));
@@ -29,7 +30,7 @@ describe('MenuItemGenerator', () => {
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
refetchAllView={vi.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
@@ -47,7 +48,7 @@ describe('MenuItemGenerator', () => {
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
refetchAllView={vi.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>

View File

@@ -1,14 +1,16 @@
import { describe, expect, it, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from 'react-query';
import { fireEvent, render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import SaveViewWithName from '../SaveViewWithName';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
vi.mock('react-router-dom', async () => ({
...(await vi.importActual<typeof import('react-router-dom')>(
'react-router-dom',
)),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
pathname: `${process.env.FRONTEND_API_ENDPOINT}/services/`,
}),
}));
@@ -20,13 +22,13 @@ const queryClient = new QueryClient({
},
});
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
vi.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: vi.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: jest.fn(() => ({
mutateAsync: jest.fn(),
vi.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
}));
@@ -36,8 +38,8 @@ describe('SaveViewWithName', () => {
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
handlePopOverClose={vi.fn()}
refetchAllView={vi.fn()}
/>
</QueryClientProvider>,
);
@@ -50,8 +52,8 @@ describe('SaveViewWithName', () => {
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
handlePopOverClose={vi.fn()}
refetchAllView={vi.fn()}
/>
</QueryClientProvider>,
);

View File

@@ -7,6 +7,7 @@ import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
import { createCustomTimeRange } from 'store/globalTime/utils';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { beforeEach, describe, expect, it } from 'vitest';
import { GlobalTimeStoreAdapter } from '../GlobalTimeStoreAdapter';

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest';
import dayjs from 'dayjs';
import { convertTimeRange, TIME_UNITS } from '../xAxisConfig';

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest';
import { PrecisionOptionsEnum } from '../types';
import { getYAxisFormattedValue } from '../yAxisConfig';

View File

@@ -1,4 +1,5 @@
import { ReactElement } from 'react';
import { describe, expect, it, vi } from 'vitest';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
@@ -12,6 +13,12 @@ import { render, screen, waitFor } from 'tests/test-utils';
import { GuardAuthZ } from './GuardAuthZ';
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: () => void } => ({
safeNavigate: (): void => {},
}),
}));
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import AnnouncementsModal from '../AnnouncementsModal';

View File

@@ -1,4 +1,6 @@
// Mock dependencies before imports
import { describe, expect, beforeEach, it, vi } from 'vitest';
import type { Mock, Mocked, MockedFunction } from 'vitest';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui';
import { render, screen } from '@testing-library/react';
@@ -9,39 +11,38 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
jest.mock('api/common/logEvent', () => ({
vi.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
default: vi.fn(() => Promise.resolve()),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
}));
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
vi.mock('@signozhq/ui', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
success: vi.fn(),
error: vi.fn(),
},
}));
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: vi.fn(),
}));
jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
vi.mock('container/Integrations/utils', () => ({
handleContactSupport: vi.fn(),
}));
const mockLogEvent = logEvent as jest.MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockHandleContactSupport = handleContactSupport as jest.Mock;
const mockToast = toast as jest.Mocked<typeof toast>;
const mockLogEvent = logEvent as MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as Mock;
const mockUseGetTenantLicense = useGetTenantLicense as Mock;
const mockHandleContactSupport = handleContactSupport as Mock;
const mockToast = toast as Mocked<typeof toast>;
const mockOnClose = jest.fn();
const mockOnClose = vi.fn();
const mockLocation = {
pathname: '/test-path',
@@ -49,7 +50,7 @@ const mockLocation = {
describe('FeedbackModal', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,

View File

@@ -1,23 +1,47 @@
// Mock dependencies before imports
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import HeaderRightSection from '../HeaderRightSection';
jest.mock('api/common/logEvent', () => ({
vi.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
default: vi.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
}));
jest.mock('../FeedbackModal', () => ({
vi.mock('antd', async () => {
const actual = await vi.importActual<typeof import('antd')>('antd');
return {
...actual,
Popover: ({
children,
content,
open,
}: {
children: ReactNode;
content: ReactNode;
open?: boolean;
}): JSX.Element => (
<>
{children}
{open ? content : null}
</>
),
};
});
vi.mock('../FeedbackModal', () => ({
__esModule: true,
default: ({ onClose }: { onClose: () => void }): JSX.Element => (
<div data-testid="feedback-modal">
@@ -28,27 +52,27 @@ jest.mock('../FeedbackModal', () => ({
),
}));
jest.mock('../ShareURLModal', () => ({
vi.mock('../ShareURLModal', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="share-modal">Share URL Modal</div>
),
}));
jest.mock('../AnnouncementsModal', () => ({
vi.mock('../AnnouncementsModal', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="announcements-modal">Announcements Modal</div>
),
}));
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: vi.fn(),
}));
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockLogEvent = logEvent as Mock;
const mockUseLocation = useLocation as Mock;
const mockUseGetTenantLicense = useGetTenantLicense as Mock;
const defaultProps = {
enableAnnouncements: true,
@@ -62,7 +86,7 @@ const mockLocation = {
describe('HeaderRightSection', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockUseLocation.mockReturnValue(mockLocation);
// Default to licensed user (Enterprise or Cloud)
mockUseGetTenantLicense.mockReturnValue({
@@ -177,7 +201,9 @@ describe('HeaderRightSection', () => {
// Close feedback modal
const closeFeedbackButton = screen.getByText('Close Feedback');
await user.click(closeFeedbackButton);
expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument();
});
});
it('should close other modals when opening feedback modal', async () => {
@@ -197,7 +223,9 @@ describe('HeaderRightSection', () => {
await user.click(feedbackButton!);
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
expect(screen.queryByTestId('share-modal')).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('share-modal')).not.toBeInTheDocument();
});
});
it('should show feedback button for Cloud users when feedback is enabled', () => {

View File

@@ -1,6 +1,6 @@
// Mock dependencies before imports
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { describe, expect, it, beforeEach, vi } from 'vitest';
import type { Mock } from 'vitest';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
@@ -12,35 +12,39 @@ import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
jest.mock('api/common/logEvent', () => ({
const hoistedReduxMocks = vi.hoisted(() => ({
useSelectorMock: vi.fn(),
}));
vi.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
default: vi.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
matchPath: jest.fn(),
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
matchPath: vi.fn(),
}));
jest.mock('hooks/useUrlQuery', () => ({
vi.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(),
default: vi.fn(),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
vi.mock('react-redux', async () => ({
...(await vi.importActual<typeof import('react-redux')>('react-redux')),
useSelector: hoistedReduxMocks.useSelectorMock,
}));
jest.mock('lib/getMinMax', () => ({
vi.mock('lib/getMinMax', () => ({
__esModule: true,
default: jest.fn(),
default: vi.fn(),
}));
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useCopyToClipboard: jest.fn(),
vi.mock('react-use', async () => ({
...(await vi.importActual('react-use')),
useCopyToClipboard: vi.fn(),
}));
// Mock window.location
@@ -53,29 +57,29 @@ Object.defineProperty(window, 'location', {
writable: true,
});
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseUrlQuery = useUrlQuery as jest.Mock;
const mockUseSelector = useSelector as jest.Mock;
const mockGetMinMax = GetMinMax as jest.Mock;
const mockUseCopyToClipboard = useCopyToClipboard as jest.Mock;
const mockMatchPath = matchPath as jest.Mock;
const mockLogEvent = logEvent as Mock;
const mockUseLocation = useLocation as Mock;
const mockUseUrlQuery = useUrlQuery as Mock;
const mockUseSelector = hoistedReduxMocks.useSelectorMock as Mock;
const mockGetMinMax = GetMinMax as Mock;
const mockUseCopyToClipboard = useCopyToClipboard as Mock;
const mockMatchPath = matchPath as Mock;
const mockUrlQuery = {
get: jest.fn(),
set: jest.fn(),
delete: jest.fn(),
toString: jest.fn(() => 'param=value'),
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
toString: vi.fn(() => 'param=value'),
};
const mockHandleCopyToClipboard = jest.fn();
const mockHandleCopyToClipboard = vi.fn();
const TEST_PATH = '/test-path';
const ENABLE_ABSOLUTE_TIME_TEXT = 'Enable absolute time';
describe('ShareURLModal', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockUseLocation.mockReturnValue({
pathname: TEST_PATH,

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Style } from '@signozhq/design-tokens';
import { ChevronDown, CircleAlert, Plus, Trash2, X } from '@signozhq/icons';
import { ChevronDown, Plus, Trash2, X } from '@signozhq/icons';
import {
Button,
Callout,
@@ -294,10 +294,8 @@ function InviteMembersModal({
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
>
{getValidationErrorMessage()}
</Callout>
title={getValidationErrorMessage()}
/>
</div>
)}
</div>

View File

@@ -1,3 +1,6 @@
import type { ChangeEventHandler, ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
import { StatusCodes } from 'http-status-codes';
@@ -12,38 +15,114 @@ const makeApiError = (message: string, code = StatusCodes.CONFLICT): APIError =>
error: { code: 'already_exists', message, url: '', errors: [] },
});
jest.mock('api/v1/invite/create');
jest.mock('api/v1/invite/bulk/create');
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: jest.fn(),
error: jest.fn(),
},
type MockButtonProps = {
children?: ReactNode;
disabled?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
'aria-label'?: string;
};
type MockInputProps = {
autoComplete?: string;
className?: string;
name?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
placeholder?: string;
type?: string;
value?: string;
};
type MockDialogProps = {
children?: ReactNode;
className?: string;
open?: boolean;
};
const showErrorModal = vi.hoisted(() => vi.fn());
vi.mock('api/v1/invite/create');
vi.mock('api/v1/invite/bulk/create');
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Button: ({
children,
disabled,
onClick,
type = 'button',
'aria-label': ariaLabel,
}: MockButtonProps): JSX.Element =>
React.createElement(
'button',
{ 'aria-label': ariaLabel, disabled, onClick, type },
children,
),
Callout: ({ title }: { title?: ReactNode }): JSX.Element =>
React.createElement('div', null, title),
DialogFooter: ({ children, className }: MockDialogProps): JSX.Element =>
React.createElement('div', { className }, children),
DialogWrapper: ({
children,
className,
open,
}: MockDialogProps): JSX.Element | null =>
open ? React.createElement('div', { className }, children) : null,
Input: ({
autoComplete,
className,
name,
onChange,
placeholder,
type,
value,
}: MockInputProps): JSX.Element =>
React.createElement('input', {
autoComplete,
className,
name,
onChange,
placeholder,
type,
value,
}),
toast: {
success: vi.fn(),
error: vi.fn(),
},
};
});
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
vi.mock('providers/ErrorModalProvider', async () => ({
__esModule: true,
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
...(await vi.importActual<typeof import('providers/ErrorModalProvider')>(
'providers/ErrorModalProvider',
)),
useErrorModal: vi.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
}));
const mockSendInvite = jest.mocked(sendInvite);
const mockInviteUsers = jest.mocked(inviteUsers);
const mockSendInvite = vi.mocked(sendInvite);
const mockInviteUsers = vi.mocked(inviteUsers);
const defaultProps = {
open: true,
onClose: jest.fn(),
onComplete: jest.fn(),
onClose: vi.fn(),
onComplete: vi.fn(),
};
describe('InviteMembersModal', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
showErrorModal.mockClear();
mockSendInvite.mockResolvedValue({
httpStatusCode: 200,
@@ -138,7 +217,7 @@ describe('InviteMembersModal', () => {
it('uses sendInvite (single) when only one row is filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = jest.fn();
const onComplete = vi.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);
@@ -243,7 +322,7 @@ describe('InviteMembersModal', () => {
it('uses inviteUsers (bulk) when multiple rows are filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = jest.fn();
const onComplete = vi.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);

View File

@@ -1,4 +1,5 @@
import React, { ComponentType, Suspense } from 'react';
import { ComponentType, lazy as reactLazy, Suspense } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
render,
screen,
@@ -7,6 +8,16 @@ import {
import Loadable from './index';
vi.mock('react', async (importOriginal) => {
const actual = await importOriginal<typeof import('react')>();
const lazy = vi.fn(actual.lazy);
return {
...actual,
lazy,
};
});
// Sample component to be loaded lazily
function SampleComponent(): JSX.Element {
return <div>Sample Component</div>;
@@ -22,6 +33,10 @@ const loadSampleComponent = (): Promise<{
});
describe('Loadable', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('should render the lazily loaded component', async () => {
const LoadableSampleComponent = Loadable(loadSampleComponent);
@@ -38,12 +53,9 @@ describe('Loadable', () => {
});
it('should call lazy with the provided import path', () => {
const reactLazySpy = jest.spyOn(React, 'lazy');
Loadable(loadSampleComponent);
expect(reactLazySpy).toHaveBeenCalledTimes(1);
expect(reactLazySpy).toHaveBeenCalledWith(expect.any(Function));
reactLazySpy.mockRestore();
expect(vi.mocked(reactLazy)).toHaveBeenCalledTimes(1);
expect(vi.mocked(reactLazy)).toHaveBeenCalledWith(expect.any(Function));
});
});

View File

@@ -1,13 +0,0 @@
.query-builder-search-wrapper {
margin-top: 10px;
border: 1px solid var(--l1-border);
border-bottom: none;
.ant-select-selector {
border: none !important;
input {
font-size: 12px;
}
}
}

View File

@@ -1,79 +0,0 @@
import { Dispatch, SetStateAction, useEffect } from 'react';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import './QueryBuilderSearchWrapper.styles.scss';
function QueryBuilderSearchWrapper({
log,
filters,
contextQuery,
isEdit,
suffixIcon,
setFilters,
setContextQuery,
}: QueryBuilderSearchWraperProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
useEffect(() => {
setContextQuery(initialContextQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSearch = (tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length ||
!contextQuery
) {
return;
}
const nextQuery: Query = {
...contextQuery,
builder: {
...contextQuery.builder,
queryData: contextQuery.builder.queryData.map((item) => ({
...item,
filters: tagFilters,
})),
},
};
setFilters({ ...tagFilters });
setContextQuery({ ...nextQuery });
};
if (!contextQuery || !isEdit) {
return <></>;
}
return (
<QueryBuilderSearch
query={contextQuery?.builder.queryData[0]}
onChange={handleSearch}
className="query-builder-search-wrapper"
suffixIcon={suffixIcon}
/>
);
}
interface QueryBuilderSearchWraperProps {
log: ILog;
isEdit: boolean;
contextQuery: Query | undefined;
setContextQuery: Dispatch<SetStateAction<Query | undefined>>;
filters: TagFilter | null;
setFilters: Dispatch<SetStateAction<TagFilter | null>>;
suffixIcon?: React.ReactNode;
}
QueryBuilderSearchWrapper.defaultProps = {
suffixIcon: undefined,
};
export default QueryBuilderSearchWrapper;

View File

@@ -1,5 +1,6 @@
import { render } from '@testing-library/react';
import { FontSize } from 'container/OptionsMenu/types';
import { describe, expect, it } from 'vitest';
import LogStateIndicator from './LogStateIndicator';

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';

View File

@@ -1,3 +0,0 @@
import { CSSProperties } from 'react';
export const rawLineStyle: CSSProperties = {};

View File

@@ -1,8 +0,0 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const ButtonContainer = styled(Button)`
&&& {
padding-left: 0;
}
`;

View File

@@ -1,11 +1,13 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FontSize } from 'container/OptionsMenu/types';
import { fireEvent, render, waitFor } from 'tests/test-utils';
import LogsFormatOptionsMenu from '../LogsFormatOptionsMenu';
const mockUpdateFormatting = jest.fn();
const mockUpdateFormatting = vi.hoisted(() => vi.fn());
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
@@ -18,11 +20,17 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateColumns: vi.fn(),
updateFormatting: mockUpdateFormatting,
}),
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
describe('LogsFormatOptionsMenu (unit)', () => {
beforeEach(() => {
mockUpdateFormatting.mockClear();
@@ -31,9 +39,9 @@ describe('LogsFormatOptionsMenu (unit)', () => {
function setup(): {
getByTestId: ReturnType<typeof render>['getByTestId'];
findItemByLabel: (label: string) => Element | undefined;
formatOnChange: jest.Mock<any, any>;
maxLinesOnChange: jest.Mock<any, any>;
fontSizeOnChange: jest.Mock<any, any>;
formatOnChange: ReturnType<typeof vi.fn>;
maxLinesOnChange: ReturnType<typeof vi.fn>;
fontSizeOnChange: ReturnType<typeof vi.fn>;
} {
const items = [
{ key: 'raw', label: 'Raw', data: { title: 'max lines per row' } },
@@ -41,9 +49,9 @@ describe('LogsFormatOptionsMenu (unit)', () => {
{ key: 'table', label: 'Column', data: { title: 'columns' } },
];
const formatOnChange = jest.fn();
const maxLinesOnChange = jest.fn();
const fontSizeOnChange = jest.fn();
const formatOnChange = vi.fn();
const maxLinesOnChange = vi.fn();
const fontSizeOnChange = vi.fn();
const { getByTestId } = render(
<LogsFormatOptionsMenu
@@ -57,11 +65,11 @@ describe('LogsFormatOptionsMenu (unit)', () => {
isFetching: false,
value: [],
options: [],
onFocus: jest.fn(),
onBlur: jest.fn(),
onSearch: jest.fn(),
onSelect: jest.fn(),
onRemove: jest.fn(),
onFocus: vi.fn(),
onBlur: vi.fn(),
onSearch: vi.fn(),
onSelect: vi.fn(),
onRemove: vi.fn(),
},
}}
/>,

View File

@@ -1,8 +1,27 @@
import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { MockedFunction } from 'vitest';
import { MemberStatus } from 'container/MembersSettings/utils';
import { render, screen, userEvent } from 'tests/test-utils';
import MembersTable, { MemberRow } from '../MembersTable';
vi.mock('@signozhq/ui', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
Badge: ({ children }: { children?: ReactNode }): JSX.Element =>
React.createElement('span', null, children),
};
});
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const mockActiveMembers: MemberRow[] = [
{
id: 'user-1',
@@ -34,13 +53,13 @@ const defaultProps = {
currentPage: 1,
pageSize: 20,
searchQuery: '',
onPageChange: jest.fn(),
onRowClick: jest.fn(),
onPageChange: vi.fn(),
onRowClick: vi.fn(),
};
describe('MembersTable', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('renders member rows with name, email, and ACTIVE status', () => {
@@ -65,9 +84,7 @@ describe('MembersTable', () => {
});
it('calls onRowClick with the member data when a row is clicked', async () => {
const onRowClick = jest.fn() as jest.MockedFunction<
(member: MemberRow) => void
>;
const onRowClick = vi.fn() as MockedFunction<(member: MemberRow) => void>;
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
@@ -87,7 +104,7 @@ describe('MembersTable', () => {
});
it('renders DELETED badge and calls onRowClick when a deleted member row is clicked', async () => {
const onRowClick = jest.fn();
const onRowClick = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const deletedMember: MemberRow = {
id: 'user-del',

View File

@@ -1,3 +1,4 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import MessageTip from './index';

View File

@@ -1,15 +1,8 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MessageTip custom action 1`] = `
.c0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MessageTip > custom action 1`] = `
<div
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-2i2tap"
class="ant-alert ant-alert-info ant-alert-with-description sc-aXZVg bzzGSj css-dev-only-do-not-override-2i2tap"
data-show="true"
role="alert"
>

View File

@@ -1,13 +0,0 @@
.custom-multiselect-dropdown {
.divider {
height: 1px;
background-color: #e8e8e8;
margin: 4px 0;
}
.all-option {
font-weight: 500;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 8px;
}
}

View File

@@ -1,11 +1,14 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { VirtuosoMockContext } from 'react-virtuoso';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CustomMultiSelect from '../CustomMultiSelect';
import type { CustomMultiSelectProps } from '../types';
import type { MockedFunction } from 'vitest';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (
@@ -17,10 +20,18 @@ const renderWithVirtuoso = (
</VirtuosoMockContext.Provider>,
);
const expectDropdownToBeClosingOrHidden = (dropdown: Element | null): void => {
expect(dropdown).toBeInTheDocument();
expect(dropdown?.className).toMatch(
/ant-select-dropdown-hidden|ant-slide-up-leave/,
);
};
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(() => Promise.resolve()),
},
});
@@ -51,12 +62,18 @@ const mockGroupedOptions = [
describe('CustomMultiSelect - Comprehensive Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnChange: jest.Mock;
let mockOnChange: MockedFunction<
NonNullable<CustomMultiSelectProps['onChange']>
>;
beforeEach(() => {
user = userEvent.setup();
mockOnChange = jest.fn();
jest.clearAllMocks();
mockOnChange = vi.fn();
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
// ===== 1. CUSTOM VALUES SUPPORT =====
@@ -805,7 +822,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 7. SAVE AND SELECTION TRIGGERS =====
describe('Save and Selection Triggers (ST)', () => {
it('ST-01: ESC triggers save action', async () => {
const mockDropdownChange = jest.fn();
const mockDropdownChange = vi.fn();
renderWithVirtuoso(
<CustomMultiSelect
@@ -832,8 +849,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Dropdown should be hidden (not completely removed from DOM)
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(dropdown).toHaveStyle('pointer-events: none');
expectDropdownToBeClosingOrHidden(dropdown);
});
});
@@ -924,7 +940,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expectDropdownToBeClosingOrHidden(dropdown);
expect(searchInput).toHaveValue('');
});
});
@@ -1157,7 +1173,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
// The dropdown should be hidden with the hidden class
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expectDropdownToBeClosingOrHidden(dropdown);
});
});
});
@@ -1268,7 +1284,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = jest.fn().mockImplementation(
const mockOnChangeWithDelay = vi.fn().mockImplementation(
() =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), 100);
@@ -1491,7 +1507,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expectDropdownToBeClosingOrHidden(dropdown);
});
});
});

View File

@@ -1,3 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { VirtuosoMockContext } from 'react-virtuoso';
import {
fireEvent,
@@ -10,7 +11,7 @@ import {
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
@@ -34,11 +35,11 @@ const RETRY_BUTTON_SELECTOR = '.navigation-icons .anticon-reload';
describe('CustomMultiSelect - Retry Functionality', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should show retry button when 5xx error occurs and error message is displayed', async () => {
const mockOnRetry = jest.fn();
const mockOnRetry = vi.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(
@@ -66,7 +67,7 @@ describe('CustomMultiSelect - Retry Functionality', () => {
});
it('should show retry button when 4xx error occurs and error message is displayed (current behavior)', async () => {
const mockOnRetry = jest.fn();
const mockOnRetry = vi.fn();
const errorMessage = 'Bad Request (400)';
renderWithVirtuoso(
@@ -93,7 +94,7 @@ describe('CustomMultiSelect - Retry Functionality', () => {
});
it('should call onRetry function when retry button is clicked', async () => {
const mockOnRetry = jest.fn();
const mockOnRetry = vi.fn();
const errorMessage = 'Internal Server Error (500)';
renderWithVirtuoso(

View File

@@ -1,3 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { VirtuosoMockContext } from 'react-virtuoso';
import {
fireEvent,
@@ -11,7 +12,7 @@ import userEvent from '@testing-library/user-event';
import CustomMultiSelect from '../CustomMultiSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
@@ -49,7 +50,7 @@ const mockGroupedOptions = [
describe('CustomMultiSelect Component', () => {
it('renders with placeholder', () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
renderWithVirtuoso(
<CustomMultiSelect
placeholder="Select multiple options"
@@ -64,7 +65,7 @@ describe('CustomMultiSelect Component', () => {
});
it('opens dropdown when clicked', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={handleChange} />,
);
@@ -83,7 +84,7 @@ describe('CustomMultiSelect Component', () => {
});
it('selects multiple options', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
// Start with option1 already selected
renderWithVirtuoso(
@@ -112,7 +113,7 @@ describe('CustomMultiSelect Component', () => {
});
it('selects ALL options when ALL is clicked', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -156,7 +157,7 @@ describe('CustomMultiSelect Component', () => {
});
it('removes a tag when clicked', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}

View File

@@ -1,15 +1,18 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CustomSelect from '../CustomSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(() => Promise.resolve()),
},
});
@@ -40,12 +43,12 @@ const mockGroupedOptions = [
describe('CustomSelect - Comprehensive Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnChange: jest.Mock;
let mockOnChange: Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnChange = jest.fn();
jest.clearAllMocks();
mockOnChange = vi.fn();
vi.clearAllMocks();
});
// ===== 1. CUSTOM VALUES SUPPORT =====
@@ -679,8 +682,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// Dropdown should close
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(combobox).toHaveAttribute('aria-expanded', 'false');
});
});
@@ -831,7 +833,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 13. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = jest.fn().mockImplementation(
const mockOnChangeWithDelay = vi.fn().mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(resolve, 100);
@@ -1076,8 +1078,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// Dropdown should close after selection in single select
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(combobox).toHaveAttribute('aria-expanded', 'false');
});
});
});

View File

@@ -1,9 +1,10 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import CustomSelect from '../CustomSelect';
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
// Mock options data
const mockOptions = [
@@ -31,7 +32,7 @@ const mockGroupedOptions = [
describe('CustomSelect Component', () => {
it('renders with placeholder and options', () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
render(
<CustomSelect
placeholder="Test placeholder"
@@ -46,7 +47,7 @@ describe('CustomSelect Component', () => {
});
it('opens dropdown when clicked', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Click to open the dropdown
@@ -62,7 +63,7 @@ describe('CustomSelect Component', () => {
});
it('calls onChange when option is selected', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Open dropdown
@@ -114,7 +115,7 @@ describe('CustomSelect Component', () => {
});
it('renders grouped options correctly', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
render(<CustomSelect options={mockGroupedOptions} onChange={handleChange} />);
// Open dropdown
@@ -168,7 +169,7 @@ describe('CustomSelect Component', () => {
});
it('supports keyboard navigation', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Open dropdown using keyboard
@@ -185,7 +186,7 @@ describe('CustomSelect Component', () => {
});
it('handles selection via keyboard', async () => {
const handleChange = jest.fn();
const handleChange = vi.fn();
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
// Open dropdown

View File

@@ -1,3 +1,4 @@
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
@@ -10,9 +11,9 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
// Mock the dashboard variables query
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
vi.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
__esModule: true,
default: jest.fn(() =>
default: vi.fn(() =>
Promise.resolve({
payload: {
variableValues: ['option1', 'option2', 'option3', 'option4'],
@@ -22,7 +23,7 @@ jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
}));
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
// Constants
const TEST_VARIABLE_NAME = 'test_variable';
@@ -76,12 +77,12 @@ function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
describe('VariableItem Integration Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnValueUpdate: jest.Mock;
let mockOnValueUpdate: Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnValueUpdate = jest.fn();
jest.clearAllMocks();
mockOnValueUpdate = vi.fn();
vi.clearAllMocks();
});
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
@@ -408,8 +409,7 @@ describe('VariableItem Integration Tests', () => {
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(combobox).toHaveAttribute('aria-expanded', 'false');
expect(searchInput).toHaveValue('');
});
});
@@ -577,8 +577,7 @@ describe('VariableItem Integration Tests', () => {
await user.keyboard('{Escape}');
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(combobox).toHaveAttribute('aria-expanded', 'false');
});
});
});

View File

@@ -1,20 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
import store from 'store';
import { describe, expect, it } from 'vitest';
import { render } from 'tests/test-utils';
import NotFound from './index';
describe('Not Found page test', () => {
it('should render Not Found page without errors', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<NotFound />
</Provider>
</MemoryRouter>,
);
const { asFragment } = render(<NotFound />);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -1,125 +1,31 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Not Found page test should render Not Found page without errors 1`] = `
exports[`Not Found page test > should render Not Found page without errors 1`] = `
<DocumentFragment>
.c3 {
border: 2px solid #2f80ed;
box-sizing: border-box;
border-radius: 10px;
width: 400px;
background: inherit;
font-style: normal;
font-weight: normal;
font-size: 24px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding-top: 14px;
padding-bottom: 14px;
color: #2f80ed;
}
.c0 {
min-height: 80vh;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c2 {
font-style: normal;
font-weight: 300;
font-size: 18px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
color: #828282;
text-align: center;
margin: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1 {
min-height: 50px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
margin-bottom: 30px;
margin-top: 20px;
}
<div
class="c0"
<div
class="sc-gEvEer jnIQEo"
>
<img
alt="not-found"
src="test-file-stub"
src="/src/assets/Images/notFound404.png"
style="max-height: 480px; max-width: 480px;"
/>
<div
class="c1"
class="sc-fqkvVR dmgRTJ"
>
<p
class="c2"
class="sc-eqUAAy keriGu"
>
Ah, seems like we reached a dead end!
</p>
<p
class="c2"
class="sc-eqUAAy keriGu"
>
Page Not Found
</p>
</div>
<a
class="c3"
class="sc-aXZVg hSWmhs"
href="/home"
tabindex="0"
>

View File

@@ -1,7 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import OverflowInputToolTip from './OverflowInputToolTip';
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const TOOLTIP_INNER_SELECTOR = '.ant-tooltip-inner';
// Utility to mock overflow behaviour on inputs / elements.
// Stubs HTMLElement.prototype.clientWidth, scrollWidth and offsetWidth used by component.
@@ -41,7 +49,7 @@ function queryTooltipInner(): HTMLElement | null {
describe('OverflowInputToolTip', () => {
beforeEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {

View File

@@ -1,19 +0,0 @@
.loading-panel-data {
padding: 24px 0;
height: 240px;
display: flex;
justify-content: center;
align-items: flex-start;
.loading-panel-data-content {
display: flex;
align-items: flex-start;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -1,17 +0,0 @@
import { Typography } from 'antd';
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
import './PanelDataLoading.styles.scss';
export function PanelDataLoading(): JSX.Element {
return (
<div className="loading-panel-data">
<div className="loading-panel-data-content">
<img className="loading-gif" src={loadingPlaneUrl} alt="wait-icon" />
<Typography.Text>Fetching data...</Typography.Text>
</div>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import { Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
createTraceOperatorContext,
@@ -183,11 +184,11 @@ describe('traceOperatorContextUtils', () => {
describe('getTraceOperatorContextAtCursor', () => {
beforeEach(() => {
// Reset console.error mock
jest.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('should return default context for empty query', () => {

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';

View File

@@ -1,11 +1,20 @@
import { EditorView } from '@uiw/react-codemirror';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import * as getKeySuggestionsModule from 'api/querySuggestions/getKeySuggestions';
import * as getValueSuggestionsModule from 'api/querySuggestions/getValueSuggestion';
import { initialQueriesMap } from 'constants/queryBuilder';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
import type { MockedFunction, MockInstance } from 'vitest';
import {
beforeAll,
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import QuerySearch from '../QuerySearch/QuerySearch';
@@ -13,7 +22,6 @@ const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
// Mock DOM APIs that CodeMirror needs
beforeAll(() => {
// Mock getClientRects and getBoundingClientRect for Range objects
const mockRect: DOMRect = {
width: 100,
height: 20,
@@ -26,7 +34,6 @@ beforeAll(() => {
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
@@ -34,7 +41,6 @@ beforeAll(() => {
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
@@ -45,7 +51,6 @@ beforeAll(() => {
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
@@ -54,7 +59,6 @@ beforeAll(() => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
@@ -75,25 +79,28 @@ beforeAll(() => {
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
});
jest.mock('hooks/useDarkMode', () => ({
vi.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
vi.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
vi.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = vi.fn();
return {
__esModule: true,
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
@@ -101,92 +108,85 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
};
});
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
getKeySuggestions: jest.fn().mockResolvedValue({
data: {
data: { keys: {} as Record<string, QueryKeyDataSuggestionsProps[]> },
},
}),
}));
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
getValueSuggestions: jest.fn().mockResolvedValue({
data: { data: { values: { stringValues: [], numberValues: [] } } },
}),
}));
// Note: We're NOT mocking CodeMirror here - using the real component
// This provides integration testing with the actual CodeMirror editor
const SAMPLE_KEY_TYPING = 'http.';
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
describe('QuerySearch (Integration with Real CodeMirror)', () => {
let getKeySuggestionsSpy: MockInstance;
let getValueSuggestionsSpy: MockInstance;
beforeEach(() => {
vi.useRealTimers();
getKeySuggestionsSpy = vi
.spyOn(getKeySuggestionsModule, 'getKeySuggestions')
.mockResolvedValue({
data: {
data: { keys: {} as Record<string, unknown[]> },
},
} as Awaited<ReturnType<typeof getKeySuggestionsModule.getKeySuggestions>>);
getValueSuggestionsSpy = vi
.spyOn(getValueSuggestionsModule, 'getValueSuggestions')
.mockResolvedValue({
data: {
data: { values: { stringValues: [], numberValues: [] } },
},
} as unknown as Awaited<
ReturnType<typeof getValueSuggestionsModule.getValueSuggestions>
>);
});
afterEach(() => {
getKeySuggestionsSpy.mockRestore();
getValueSuggestionsSpy.mockRestore();
});
it('renders with placeholder', () => {
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
onChange={vi.fn() as MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
// CodeMirror renders a contenteditable div, so we check for the container
const editorContainer = document.querySelector('.query-where-clause-editor');
expect(editorContainer).toBeInTheDocument();
});
it('fetches key suggestions when typing a key (debounced)', async () => {
// Use real timers for CodeMirror integration tests
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions
>;
mockedGetKeys.mockClear();
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
onChange={vi.fn() as MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
// Find the CodeMirror editor contenteditable element
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
// Focus and type into the editor
await userEvent.click(editor);
await userEvent.type(editor, SAMPLE_KEY_TYPING);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeys).toHaveBeenCalled(), {
await waitFor(() => expect(getKeySuggestionsSpy).toHaveBeenCalled(), {
timeout: 2000,
});
});
it('fetches value suggestions when editing value context', async () => {
// Use real timers for CodeMirror integration tests
const mockedGetValues = getValueSuggestions as jest.MockedFunction<
typeof getValueSuggestions
>;
mockedGetValues.mockClear();
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
onChange={vi.fn() as MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
@@ -196,51 +196,42 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
await userEvent.click(editor);
await userEvent.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
await waitFor(() => expect(getValueSuggestionsSpy).toHaveBeenCalled(), {
timeout: 2000,
});
});
it('fetches key suggestions on mount for LOGS', async () => {
// Use real timers for CodeMirror integration tests
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions
>;
mockedGetKeysOnMount.mockClear();
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
onChange={vi.fn() as MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
await waitFor(() => expect(getKeySuggestionsSpy).toHaveBeenCalled(), {
timeout: 2000,
});
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.mock.calls.length - 1
const lastArgs = getKeySuggestionsSpy.mock.calls[
getKeySuggestionsSpy.mock.calls.length - 1
]?.[0] as { signal: unknown; searchText: string };
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
});
it('calls provided onRun on Mod-Enter', async () => {
const onRun = jest.fn() as jest.MockedFunction<(q: string) => void>;
const onRun = vi.fn() as MockedFunction<(q: string) => void>;
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
onChange={vi.fn() as MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
onRun={onRun}
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
@@ -250,7 +241,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
await userEvent.click(editor);
await userEvent.type(editor, SAMPLE_STATUS_QUERY);
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
fireEvent.keyDown(editor, {
key: 'Enter',
@@ -274,21 +264,18 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
onChange={vi.fn() as MockedFunction<(v: string) => void>}
queryData={queryDataWithExpression}
dataSource={DataSource.LOGS}
/>,
);
// Wait for CodeMirror to initialize and the expression to be set
await waitFor(
() => {
// CodeMirror stores content in .cm-content, check the text content
const editorContent = document.querySelector(
CM_EDITOR_SELECTOR,
) as HTMLElement;
expect(editorContent).toBeInTheDocument();
// CodeMirror may render the text in multiple ways, check if it contains our expression
const textContent = editorContent.textContent || '';
expect(textContent).toContain('http.status_code');
expect(textContent).toContain('service.name');
@@ -298,13 +285,11 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
});
it('handles queryData.filter.expression changes without triggering onChange', async () => {
// Spy on CodeMirror's EditorView.dispatch, which is invoked when updateEditorValue
// applies a programmatic change to the editor.
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
const dispatchSpy = vi.spyOn(EditorView.prototype, 'dispatch');
const initialExpression = "service.name = 'frontend'";
const updatedExpression = "service.name = 'backend'";
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
const onChange = vi.fn() as MockedFunction<(v: string) => void>;
const initialQueryData = {
...initialQueriesMap.logs.builder.queryData[0],
@@ -321,7 +306,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for CodeMirror to initialize with the initial expression
await waitFor(
() => {
const editorContent = document.querySelector(
@@ -334,13 +318,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
{ timeout: 3000 },
);
// Ensure the editor is explicitly blurred (not focused)
// Blur the actual CodeMirror editor container so that QuerySearch's onBlur handler runs.
// Note: In jsdom + CodeMirror we can't reliably assert the DOM text content changes when
// the expression is updated programmatically, but we can assert that:
// 1) The component continues to render, and
// 2) No onChange is fired for programmatic updates.
const updatedQueryData = {
...initialQueryData,
filter: {
@@ -348,7 +325,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
},
};
// Re-render with updated queryData.filter.expression
rerender(
<QuerySearch
onChange={onChange}
@@ -357,7 +333,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// updateEditorValue should have resulted in a dispatch call + onChange should not have been called
await waitFor(() => {
expect(dispatchSpy).toHaveBeenCalled();
expect(onChange).not.toHaveBeenCalled();
@@ -367,11 +342,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
});
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions
>;
mockedGetKeys.mockClear();
const queryData = {
...initialQueriesMap.metrics.builder.queryData[0],
aggregateAttribute: {
@@ -383,18 +353,15 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
render(
<QuerySearch
onChange={jest.fn()}
onChange={vi.fn()}
queryData={queryData}
dataSource={DataSource.METRICS}
showFilterSuggestionsWithoutMetric
/>,
);
await waitFor(
() => {
expect(mockedGetKeys).toHaveBeenCalled();
},
{ timeout: 2000 },
);
await waitFor(() => expect(getKeySuggestionsSpy).toHaveBeenCalled(), {
timeout: 2000,
});
});
});

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { jest } from '@jest/globals';
import { fireEvent, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MockedFunction } from 'vitest';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
@@ -19,47 +20,45 @@ import {
QueryFunctionsTypes,
} from 'types/common/queryBuilder';
import '@testing-library/jest-dom';
import { QueryBuilderV2 } from '../../QueryBuilderV2';
// Local mocks for domain-specific heavy child components
jest.mock(
'../QueryAggregation/QueryAggregation',
() =>
function QueryAggregation() {
return <div>QueryAggregation</div>;
},
);
jest.mock(
'../MerticsAggregateSection/MetricsAggregateSection',
() =>
function MetricsAggregateSection() {
return <div>MetricsAggregateSection</div>;
},
);
// Mock hooks
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('hooks/queryBuilder/useQueryBuilderOperations');
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const mockedUseQueryOperations = jest.mocked(
// Local mocks for domain-specific heavy child components
vi.mock('../QueryAggregation/QueryAggregation', () => ({
default: function QueryAggregation() {
return <div>QueryAggregation</div>;
},
}));
vi.mock('../MerticsAggregateSection/MetricsAggregateSection', () => ({
default: function MetricsAggregateSection() {
return <div>MetricsAggregateSection</div>;
},
}));
// Mock hooks
vi.mock('hooks/queryBuilder/useQueryBuilder');
vi.mock('hooks/queryBuilder/useQueryBuilderOperations');
const mockedUseQueryBuilder = vi.mocked(useQueryBuilder);
const mockedUseQueryOperations = vi.mocked(
useQueryOperations,
) as jest.MockedFunction<UseQueryOperations>;
) as MockedFunction<UseQueryOperations>;
describe('QueryBuilderV2 + QueryV2 - base render', () => {
let handleRunQueryMock: jest.MockedFunction<() => void>;
let handleQueryFunctionsUpdatesMock: jest.MockedFunction<() => void>;
let handleRunQueryMock: MockedFunction<() => void>;
let handleQueryFunctionsUpdatesMock: MockedFunction<() => void>;
let baseQBContext: QueryBuilderContextType;
beforeEach(() => {
const mockCloneQuery = jest.fn() as jest.MockedFunction<
const mockCloneQuery = vi.fn() as MockedFunction<
(type: string, q: IBuilderQuery) => void
>;
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
handleQueryFunctionsUpdatesMock = jest.fn() as jest.MockedFunction<
() => void
>;
handleRunQueryMock = vi.fn() as MockedFunction<() => void>;
handleQueryFunctionsUpdatesMock = vi.fn() as MockedFunction<() => void>;
const baseQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
@@ -103,35 +102,35 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: jest.fn(),
setLastUsedQuery: vi.fn(),
supersetQuery: currentQueryObj,
setSupersetQuery: jest.fn(),
setSupersetQuery: vi.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
handleSetQueryData: jest.fn(),
handleSetTraceOperatorData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeAllQueryBuilderEntities: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
removeTraceOperator: jest.fn(),
addTraceOperator: jest.fn(),
handleSetQueryData: vi.fn(),
handleSetTraceOperatorData: vi.fn(),
handleSetFormulaData: vi.fn(),
handleSetQueryItemData: vi.fn(),
handleSetConfig: vi.fn(),
removeQueryBuilderEntityByIndex: vi.fn(),
removeAllQueryBuilderEntities: vi.fn(),
removeQueryTypeItemByIndex: vi.fn(),
addNewBuilderQuery: vi.fn(),
addNewFormula: vi.fn(),
removeTraceOperator: vi.fn(),
addTraceOperator: vi.fn(),
cloneQuery: mockCloneQuery,
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
addNewQueryItem: vi.fn(),
redirectWithQueryBuilderData: vi.fn(),
handleRunQuery: handleRunQueryMock,
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
resetQuery: vi.fn(),
handleOnUnitsChange: vi.fn(),
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
initQueryBuilderData: vi.fn(),
isStagedQueryUpdated: vi.fn(() => false),
isDefaultQuery: vi.fn(() => false),
} as unknown as QueryBuilderContextType;
baseQBContext = baseContext;
@@ -143,21 +142,21 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
operators: [],
spaceAggregationOptions: [],
listOfAdditionalFilters: [],
handleChangeOperator: jest.fn(),
handleSpaceAggregationChange: jest.fn(),
handleChangeAggregatorAttribute: jest.fn(),
handleChangeDataSource: jest.fn(),
handleDeleteQuery: jest.fn(),
handleChangeOperator: vi.fn(),
handleSpaceAggregationChange: vi.fn(),
handleChangeAggregatorAttribute: vi.fn(),
handleChangeDataSource: vi.fn(),
handleDeleteQuery: vi.fn(),
handleChangeQueryData:
jest.fn() as unknown as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: jest.fn(),
vi.fn() as unknown as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: vi.fn(),
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
listOfAdditionalFormulaFilters: [],
});
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('renders limit input when dataSource is logs', () => {

View File

@@ -1,3 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
fireEvent,
@@ -12,10 +14,16 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import QueryAddOns from '../QueryV2/QueryAddOns/QueryAddOns';
// Mocks: only what is required for this component to render and for us to assert handler calls
const mockHandleChangeQueryData = jest.fn();
const mockHandleSetQueryData = jest.fn();
const mockHandleChangeQueryData = vi.fn();
const mockHandleSetQueryData = vi.fn();
jest.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
vi.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
useQueryOperations: (): {
handleChangeQueryData: typeof mockHandleChangeQueryData;
} => ({
@@ -23,7 +31,7 @@ jest.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
vi.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
handleSetQueryData: typeof mockHandleSetQueryData;
} => ({
@@ -31,7 +39,7 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
}),
}));
jest.mock('container/QueryBuilder/filters/GroupByFilter/GroupByFilter', () => ({
vi.mock('container/QueryBuilder/filters/GroupByFilter/GroupByFilter', () => ({
GroupByFilter: ({ onChange }: any): JSX.Element => (
<button
data-testid="groupby"
@@ -42,7 +50,7 @@ jest.mock('container/QueryBuilder/filters/GroupByFilter/GroupByFilter', () => ({
),
}));
jest.mock('container/QueryBuilder/filters/OrderByFilter/OrderByFilter', () => ({
vi.mock('container/QueryBuilder/filters/OrderByFilter/OrderByFilter', () => ({
OrderByFilter: ({ onChange }: any): JSX.Element => (
<button
data-testid="orderby"
@@ -53,7 +61,7 @@ jest.mock('container/QueryBuilder/filters/OrderByFilter/OrderByFilter', () => ({
),
}));
jest.mock('../QueryV2/QueryAddOns/HavingFilter/HavingFilter', () => ({
vi.mock('../QueryV2/QueryAddOns/HavingFilter/HavingFilter', () => ({
__esModule: true,
default: ({ onChange, onClose }: any): JSX.Element => (
<div>
@@ -87,7 +95,7 @@ function baseQuery(overrides: Partial<any> = {}): any {
describe('QueryAddOns', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('VALUE panel: no sections auto-open when query has no active add-ons', () => {

View File

@@ -1,4 +1,5 @@
import { jest } from '@jest/globals';
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import type { MockedFunction } from 'vitest';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { render, screen, userEvent } from 'tests/test-utils';
@@ -19,45 +20,44 @@ import {
} from '../QueryV2/previousQuery.utils';
// Local mocks for domain-specific heavy child components
jest.mock(
'../QueryV2/QueryAggregation/QueryAggregation',
() =>
function QueryAggregation(): JSX.Element {
return <div>QueryAggregation</div>;
},
);
jest.mock(
'../QueryV2/MerticsAggregateSection/MetricsAggregateSection',
() =>
function MetricsAggregateSection(): JSX.Element {
return <div>MetricsAggregateSection</div>;
},
);
vi.mock('../QueryV2/QueryAggregation/QueryAggregation', () => ({
default: function QueryAggregation(): JSX.Element {
return <div>QueryAggregation</div>;
},
}));
vi.mock('../QueryV2/MerticsAggregateSection/MetricsAggregateSection', () => ({
default: function MetricsAggregateSection(): JSX.Element {
return <div>MetricsAggregateSection</div>;
},
}));
// Mock networked children to avoid axios during unit tests
jest.mock(
'../QueryV2/QuerySearch/QuerySearch',
() =>
function QuerySearch(): JSX.Element {
return <div>QuerySearch</div>;
},
);
jest.mock('container/QueryBuilder/filters', () => ({
vi.mock('../QueryV2/QuerySearch/QuerySearch', () => ({
default: function QuerySearch(): JSX.Element {
return <div>QuerySearch</div>;
},
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
vi.mock('container/QueryBuilder/filters', () => ({
AggregatorFilter: (): JSX.Element => <div />,
MetricNameSelector: (): JSX.Element => <div />,
}));
// Mock hooks
jest.mock('hooks/queryBuilder/useQueryBuilder');
vi.mock('hooks/queryBuilder/useQueryBuilder');
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const mockedUseQueryBuilder = vi.mocked(useQueryBuilder);
describe('MetricsSelect - signal source switching (standalone)', () => {
let handleSetQueryDataMock: jest.MockedFunction<
let handleSetQueryDataMock: MockedFunction<
(index: number, q: IBuilderQuery) => void
>;
beforeEach(() => {
clearPreviousQuery();
handleSetQueryDataMock = jest.fn() as unknown as jest.MockedFunction<
handleSetQueryDataMock = vi.fn() as unknown as MockedFunction<
(index: number, q: IBuilderQuery) => void
>;
@@ -109,40 +109,40 @@ describe('MetricsSelect - signal source switching (standalone)', () => {
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: jest.fn(),
setLastUsedQuery: vi.fn(),
supersetQuery: currentQueryObj,
setSupersetQuery: jest.fn(),
setSupersetQuery: vi.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
handleSetQueryData: handleSetQueryDataMock,
handleSetTraceOperatorData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeAllQueryBuilderEntities: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
removeTraceOperator: jest.fn(),
addTraceOperator: jest.fn(),
cloneQuery: jest.fn(),
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: jest.fn(),
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
handleSetTraceOperatorData: vi.fn(),
handleSetFormulaData: vi.fn(),
handleSetQueryItemData: vi.fn(),
handleSetConfig: vi.fn(),
removeQueryBuilderEntityByIndex: vi.fn(),
removeAllQueryBuilderEntities: vi.fn(),
removeQueryTypeItemByIndex: vi.fn(),
addNewBuilderQuery: vi.fn(),
addNewFormula: vi.fn(),
removeTraceOperator: vi.fn(),
addTraceOperator: vi.fn(),
cloneQuery: vi.fn(),
addNewQueryItem: vi.fn(),
redirectWithQueryBuilderData: vi.fn(),
handleRunQuery: vi.fn(),
resetQuery: vi.fn(),
handleOnUnitsChange: vi.fn(),
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
initQueryBuilderData: vi.fn(),
isStagedQueryUpdated: vi.fn(() => false),
isDefaultQuery: vi.fn(() => false),
});
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
clearPreviousQuery();
});
@@ -216,13 +216,13 @@ describe('MetricsSelect - signal source switching (standalone)', () => {
});
describe('DataSource change - Logs to Traces', () => {
let handleSetQueryDataMock: jest.MockedFunction<
let handleSetQueryDataMock: MockedFunction<
(index: number, q: IBuilderQuery) => void
>;
beforeEach(() => {
clearPreviousQuery();
handleSetQueryDataMock = jest.fn() as unknown as jest.MockedFunction<
handleSetQueryDataMock = vi.fn() as unknown as MockedFunction<
(i: number, q: IBuilderQuery) => void
>;
@@ -266,40 +266,40 @@ describe('DataSource change - Logs to Traces', () => {
currentQuery: logsCurrentQuery,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: jest.fn(),
setLastUsedQuery: vi.fn(),
supersetQuery: logsCurrentQuery,
setSupersetQuery: jest.fn(),
setSupersetQuery: vi.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
handleSetQueryData: handleSetQueryDataMock,
handleSetTraceOperatorData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeAllQueryBuilderEntities: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
removeTraceOperator: jest.fn(),
addTraceOperator: jest.fn(),
cloneQuery: jest.fn(),
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: jest.fn(),
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
handleSetTraceOperatorData: vi.fn(),
handleSetFormulaData: vi.fn(),
handleSetQueryItemData: vi.fn(),
handleSetConfig: vi.fn(),
removeQueryBuilderEntityByIndex: vi.fn(),
removeAllQueryBuilderEntities: vi.fn(),
removeQueryTypeItemByIndex: vi.fn(),
addNewBuilderQuery: vi.fn(),
addNewFormula: vi.fn(),
removeTraceOperator: vi.fn(),
addTraceOperator: vi.fn(),
cloneQuery: vi.fn(),
addNewQueryItem: vi.fn(),
redirectWithQueryBuilderData: vi.fn(),
handleRunQuery: vi.fn(),
resetQuery: vi.fn(),
handleOnUnitsChange: vi.fn(),
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
initQueryBuilderData: vi.fn(),
isStagedQueryUpdated: vi.fn(() => false),
isDefaultQuery: vi.fn(() => false),
});
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
clearPreviousQuery();
});

View File

@@ -1,3 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import '@testing-library/jest-dom';
@@ -43,7 +45,7 @@ describe('previousQuery.utils', () => {
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('getQueryKey normalizes non-meter signal to empty string', () => {
@@ -150,11 +152,9 @@ describe('previousQuery.utils', () => {
});
it('write errors (e.g., quota) are caught and do not throw', () => {
const spy = jest
.spyOn(window.sessionStorage.__proto__, 'setItem')
.mockImplementation(() => {
throw new Error('quota exceeded');
});
const spy = vi.spyOn(sessionStorage, 'setItem').mockImplementation(() => {
throw new Error('quota exceeded');
});
const key = getQueryKey({
queryName: 'A',

View File

@@ -1,3 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import {
BaseAutocompleteData,
@@ -17,7 +19,7 @@ import {
describe('convertFiltersToExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should handle empty, null, and undefined inputs', () => {
@@ -983,7 +985,7 @@ describe('convertAggregationToExpression', () => {
describe('removeKeysFromExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('Backward compatibility (removeOnlyVariableExpressions = false)', () => {
@@ -1203,7 +1205,7 @@ describe('removeKeysFromExpression', () => {
describe('formatValueForExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('Variable values', () => {

View File

@@ -1,3 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { UseQueryResult } from 'react-query';
import userEvent from '@testing-library/user-event';
import { FiltersType, QuickFiltersSource } from 'components/QuickFilters/types';
@@ -14,19 +15,25 @@ import { DataSource } from 'types/common/queryBuilder';
import CheckboxFilter from './Checkbox';
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
// Mock the query builder hook
jest.mock('hooks/queryBuilder/useQueryBuilder');
const mockUseQueryBuilder = jest.mocked(useQueryBuilder);
vi.mock('hooks/queryBuilder/useQueryBuilder');
const mockUseQueryBuilder = vi.mocked(useQueryBuilder);
// Mock the aggregate values hook
jest.mock('hooks/queryBuilder/useGetAggregateValues');
vi.mock('hooks/queryBuilder/useGetAggregateValues');
const mockUseGetAggregateValues = jest.mocked(useGetAggregateValues);
const mockUseGetAggregateValues = vi.mocked(useGetAggregateValues);
// Mock the key value suggestions hook
jest.mock('hooks/querySuggestions/useGetQueryKeyValueSuggestions');
vi.mock('hooks/querySuggestions/useGetQueryKeyValueSuggestions');
const mockUseGetQueryKeyValueSuggestions = jest.mocked(
const mockUseGetQueryKeyValueSuggestions = vi.mocked(
useGetQueryKeyValueSuggestions,
);
@@ -90,13 +97,13 @@ const createMockQueryBuilderData = (hasActiveFilters = false): any => ({
],
},
},
redirectWithQueryBuilderData: jest.fn(),
redirectWithQueryBuilderData: vi.fn(),
});
describe('CheckboxFilter - User Flows', () => {
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
vi.clearAllMocks();
// Default mock implementations for useGetAggregateValues
mockUseGetAggregateValues.mockReturnValue({
@@ -106,7 +113,7 @@ describe('CheckboxFilter - User Flows', () => {
},
},
isLoading: false,
refetch: jest.fn(),
refetch: vi.fn(),
} as unknown as UseQueryResult<SuccessResponse<IAttributeValuesResponse>>);
// Default mock implementations for useGetQueryKeyValueSuggestions
@@ -123,7 +130,7 @@ describe('CheckboxFilter - User Flows', () => {
},
},
isLoading: false,
refetch: jest.fn(),
refetch: vi.fn(),
} as any);
// Setup MSW server for API calls
@@ -205,7 +212,7 @@ describe('CheckboxFilter - User Flows', () => {
});
it('should update query filters when a checkbox is clicked', async () => {
const redirectWithQueryBuilderData = jest.fn();
const redirectWithQueryBuilderData = vi.fn();
// Start with no active filters so clicking a checkbox creates one
mockUseQueryBuilder.mockReturnValue({
@@ -246,7 +253,7 @@ describe('CheckboxFilter - User Flows', () => {
});
it('should set an IN filter with only the clicked value when using Only', async () => {
const redirectWithQueryBuilderData = jest.fn();
const redirectWithQueryBuilderData = vi.fn();
// Existing filter: service.name IN ['mq-kafka', 'otel-demo']
mockUseQueryBuilder.mockReturnValue({
@@ -304,7 +311,7 @@ describe('CheckboxFilter - User Flows', () => {
});
it('should clear filters for the attribute when using All', async () => {
const redirectWithQueryBuilderData = jest.fn();
const redirectWithQueryBuilderData = vi.fn();
// Existing filter: service.name IN ['mq-kafka']
mockUseQueryBuilder.mockReturnValue({
@@ -389,7 +396,7 @@ describe('CheckboxFilter - User Flows', () => {
],
},
},
redirectWithQueryBuilderData: jest.fn(),
redirectWithQueryBuilderData: vi.fn(),
} as any);
const mockFilter = createMockFilter({ defaultOpen: false });
@@ -414,7 +421,7 @@ describe('CheckboxFilter - User Flows', () => {
});
it('should extend an existing IN filter when checking an additional value', async () => {
const redirectWithQueryBuilderData = jest.fn();
const redirectWithQueryBuilderData = vi.fn();
// Existing filter: service.name IN 'mq-kafka'
mockUseQueryBuilder.mockReturnValue({

View File

@@ -1,3 +1,15 @@
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import type { Mock, MockedFunction } from 'vitest';
import { ENVIRONMENT } from 'constants/env';
import {
ApiMonitoringParams,
@@ -19,18 +31,18 @@ import QuickFilters from '../QuickFilters';
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
import { QuickFiltersConfig } from './constants';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
vi.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: vi.fn(),
}));
jest.mock('container/ApiMonitoring/queryParams');
vi.mock('container/ApiMonitoring/queryParams');
const handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn();
const putHandler = jest.fn();
const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
const handleFilterVisibilityChange = vi.fn();
const redirectWithQueryBuilderData = vi.fn();
const putHandler = vi.fn();
const mockSetApiMonitoringParams = vi.fn() as MockedFunction<
(newParams: Partial<ApiMonitoringParams>, replace?: boolean) => void
>;
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const mockUseApiMonitoringParams = vi.mocked(useApiMonitoringParams);
const BASE_URL = ENVIRONMENT.baseURL;
const SIGNAL = SignalType.LOGS;
@@ -121,7 +133,7 @@ beforeAll(() => {
afterEach(() => {
server.resetHandlers();
jest.clearAllMocks();
vi.clearAllMocks();
});
afterAll(() => {
@@ -129,7 +141,7 @@ afterAll(() => {
});
beforeEach(() => {
(useQueryBuilder as jest.Mock).mockReturnValue({
(useQueryBuilder as Mock).mockReturnValue({
currentQuery: {
builder: {
queryData: [
@@ -158,10 +170,10 @@ describe('Quick Filters', () => {
});
it('should display and allow selection from query dropdown when multiple queries exist', async () => {
const setLastUsedQuery = jest.fn();
const setLastUsedQuery = vi.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useQueryBuilder as jest.Mock).mockReturnValue({
(useQueryBuilder as Mock).mockReturnValue({
currentQuery: {
builder: {
queryData: [
@@ -213,7 +225,7 @@ describe('Quick Filters', () => {
});
it('should not display query dropdown in ListView', () => {
(useQueryBuilder as jest.Mock).mockReturnValue({
(useQueryBuilder as Mock).mockReturnValue({
currentQuery: {
builder: {
queryData: [
@@ -466,9 +478,9 @@ describe('Quick Filters with custom filters', () => {
it('should render duration slider for duration_nono filter', async () => {
// Use fake timers only in this test (for debounce), and wire them to userEvent
jest.useFakeTimers();
vi.useFakeTimers();
const user = userEvent.setup({
advanceTimers: (ms) => jest.advanceTimersByTime(ms),
advanceTimers: (ms) => vi.advanceTimersByTime(ms),
pointerEventsCheck: 0,
});
@@ -492,7 +504,7 @@ describe('Quick Filters with custom filters', () => {
await user.type(minDuration, '10000');
await user.clear(maxDuration);
await user.type(maxDuration, '20000');
jest.advanceTimersByTime(2000);
vi.advanceTimersByTime(2000);
await waitFor(() => {
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
@@ -521,7 +533,7 @@ describe('Quick Filters with custom filters', () => {
);
});
jest.useRealTimers();
vi.useRealTimers();
});
});

View File

@@ -1,9 +1,10 @@
import { act } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent } from 'tests/test-utils';
import ResizeTable from '../ResizeTable';
jest.mock('react-resizable', () => ({
vi.mock('react-resizable', () => ({
Resizable: ({
children,
onResize,
@@ -28,8 +29,8 @@ jest.mock('react-resizable', () => ({
}));
// Make debounce synchronous so onColumnWidthsChange fires immediately
jest.mock('lodash-es', () => ({
...jest.requireActual('lodash-es'),
vi.mock('lodash-es', async () => ({
...(await vi.importActual<typeof import('lodash-es')>('lodash-es')),
debounce: (fn: (...args: any[]) => any): ((...args: any[]) => any) => fn,
}));
@@ -60,7 +61,7 @@ describe('ResizeTable', () => {
});
it('overrides column widths from columnWidths prop and reports them via onColumnWidthsChange', () => {
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
act(() => {
render(
@@ -80,7 +81,7 @@ describe('ResizeTable', () => {
});
it('reports original column widths via onColumnWidthsChange when columnWidths prop is not provided', () => {
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
act(() => {
render(
@@ -112,7 +113,7 @@ describe('ResizeTable', () => {
});
it('only overrides the column that has a stored width, leaving others at their original width', () => {
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
act(() => {
render(
@@ -132,7 +133,7 @@ describe('ResizeTable', () => {
});
it('does not call onColumnWidthsChange on re-render when widths have not changed', () => {
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
const { rerender } = render(
<ResizeTable
@@ -159,7 +160,7 @@ describe('ResizeTable', () => {
});
it('does not call onColumnWidthsChange when no column has a defined width', () => {
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
render(
<ResizeTable
@@ -178,7 +179,7 @@ describe('ResizeTable', () => {
it('calls onColumnWidthsChange with the new width after a column is resized', async () => {
const user = userEvent.setup();
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
render(
<ResizeTable
@@ -202,7 +203,7 @@ describe('ResizeTable', () => {
it('does not affect other columns when only one column is resized', async () => {
const user = userEvent.setup();
const onColumnWidthsChange = jest.fn();
const onColumnWidthsChange = vi.fn();
render(
<ResizeTable
@@ -225,7 +226,7 @@ describe('ResizeTable', () => {
});
it('wraps column titles in drag handler spans when onDragColumn is provided', () => {
const onDragColumn = jest.fn();
const onDragColumn = vi.fn();
render(
<ResizeTable

View File

@@ -0,0 +1,134 @@
import type { ReactElement } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import RolesSelect from '../RolesSelect';
function renderLabeledRolesSelect(node: ReactElement): void {
render(
<div>
<label htmlFor="roles-select-test">Roles</label>
{node}
</div>,
);
}
vi.mock('api/generated/services/role', async () => {
const actual = await vi.importActual<
typeof import('api/generated/services/role')
>('api/generated/services/role');
return {
...actual,
useListRoles: vi.fn(),
};
});
import { useListRoles } from 'api/generated/services/role';
function mockListRolesSuccess(): void {
(useListRoles as Mock).mockReturnValue({
data: listRolesSuccessResponse,
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
isFetching: false,
isSuccess: true,
status: 'success',
});
}
describe('RolesSelect', () => {
beforeEach(() => {
vi.clearAllMocks();
mockListRolesSuccess();
});
it('lists roles from the API in single mode and reports changes', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onChange = vi.fn();
renderLabeledRolesSelect(
<RolesSelect
id="roles-select-test"
mode="single"
value={listRolesSuccessResponse.data[0]?.id}
onChange={onChange}
/>,
);
const rolesControl = await screen.findByLabelText('Roles');
await user.click(rolesControl);
const editorOption = await screen.findByTitle('signoz-editor');
await user.click(editorOption);
const editorId = listRolesSuccessResponse.data.find(
(r) => r.name === 'signoz-editor',
)?.id;
await waitFor(() => {
expect(onChange).toHaveBeenCalled();
expect(onChange.mock.calls[0][0]).toBe(editorId);
});
});
it('lists roles in multiple mode and reports combined selection', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onChange = vi.fn();
const firstId = listRolesSuccessResponse.data[0]?.id as string;
const secondId = listRolesSuccessResponse.data[1]?.id as string;
renderLabeledRolesSelect(
<RolesSelect
id="roles-select-test"
mode="multiple"
value={[firstId]}
onChange={onChange}
/>,
);
const rolesControl = await screen.findByLabelText('Roles');
await user.click(rolesControl);
const secondOption = await screen.findByTitle(
listRolesSuccessResponse.data[1]?.name ?? '',
);
await user.click(secondOption);
await waitFor(() => {
expect(onChange).toHaveBeenCalled();
expect(onChange.mock.calls[0][0]).toStrictEqual([firstId, secondId]);
});
});
it('uses injected roles without fetching when roles prop is set', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const injected = listRolesSuccessResponse.data.slice(0, 2);
renderLabeledRolesSelect(
<RolesSelect
id="roles-select-test"
mode="single"
roles={injected}
value={injected[0]?.id}
onChange={vi.fn()}
/>,
);
expect(useListRoles).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({ enabled: false }),
}),
);
const rolesControl = await screen.findByLabelText('Roles');
await user.click(rolesControl);
await expect(
screen.findByTitle(injected[1]?.name ?? ''),
).resolves.toBeInTheDocument();
});
});

View File

@@ -1,5 +1,6 @@
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from 'tests/test-utils';
import RouteTab from './index';
@@ -75,7 +76,7 @@ describe('RouteTab component', () => {
});
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = jest.fn();
const onChangeHandler = vi.fn();
const history = createMemoryHistory();
render(
<Router history={history}>

Some files were not shown because too many files have changed in this diff Show More