Compare commits

..

110 Commits

Author SHA1 Message Date
Naman Verma
301d0103b0 Merge branch 'nv/delete-v2-dashboard' into nv/patch-dashboard 2026-05-05 11:59:12 +05:30
Naman Verma
dc99772ee4 Merge branch 'nv/other-dashboard-v2-update-methods' into nv/delete-v2-dashboard 2026-05-05 11:58:45 +05:30
Naman Verma
80849ebfeb Merge branch 'nv/v2-dashboard-update' into nv/other-dashboard-v2-update-methods 2026-05-05 11:58:22 +05:30
Naman Verma
2c0c7240a4 Merge branch 'nv/v2-dashboard-get' into nv/v2-dashboard-update 2026-05-05 11:56:39 +05:30
Naman Verma
28cb0a8be7 Merge branch 'nv/v2-dashboard-create' into nv/v2-dashboard-get 2026-05-05 11:54:54 +05:30
Naman Verma
54832cad34 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-05 11:54:38 +05:30
Naman Verma
a45178d709 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-05 11:54:21 +05:30
Naman Verma
c4224ecf72 Merge branch 'main' into nv/dashboardv2 2026-05-05 11:53:56 +05:30
Naman Verma
14927c89d3 feat: patch dashboard api 2026-05-05 09:22:25 +05:30
Pandey
8409a9798d fix(authdomain): nest config response, rename Updateable→Updatable, return Identifiable on create (#11176)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(authdomain): nest config response, rename Updateable→Updatable, return Identifiable on create

Three small API-shape corrections on auth_domain:

- GettableAuthDomain previously embedded AuthDomainConfig, which
  flattened sso_enabled / saml_config / oidc_config / google_auth_config /
  role_mapping at the response root and made the response shape
  diverge from the request shape (PostableAuthDomain has them under
  `config`). Move it under a named `Config` field with a `config`
  json tag so request and response carry the same nested object.
- UpdateableAuthDomain → UpdatableAuthDomain (typo fix; aligns with
  UpdatableUser already in the codebase).
- CreateAuthDomain previously returned the full GettableAuthDomain;
  the only field clients actually need from the create response is
  the new ID. Switch to Identifiable so the contract states what the
  endpoint guarantees and clients re-Read for the full domain when
  needed.

Frontend schema and OpenAPI spec regenerated.

* fix(authdomain-frontend): adapt to nested config + Identifiable create response

Regenerate the orval client (`yarn generate:api`) and update the
auth-domain UI for the API shape changes from the previous commit:

- `record.ssoType`, `.ssoEnabled`, `.googleAuthConfig`, `.oidcConfig`,
  `.samlConfig`, `.roleMapping` are now nested under `record.config.*`
  in `AuthtypesGettableAuthDomainDTO` — update SSOEnforcementToggle,
  CreateEdit form initial-values, the list page's Configure button,
  and the auth-domain test mocks.
- `mockCreateSuccessResponse` now returns `{ id }` (Identifiable)
  instead of the full domain.

`yarn generate:api` ran clean: lint OK, tsgo OK.

* fix(authdomain): align CreateAuthDomain success code with handler + adjust integration test

The Create handler returns http.StatusCreated but the OpenAPI
annotation said StatusOK. Sync the annotation to 201, regenerate the
spec + frontend client.

The callbackauthn integration test (01_domain.py) still read
`domain["ssoType"]` off the GET response — now nested under
`domain["config"]["ssoType"]` after the previous shape change. Update
the assertion.
2026-05-04 20:44:41 +00:00
Vinicius Lourenço
de6e4890ae feat(query-search-v2): add initial expression support & store to manage (#11062)
* feat(query-search-v2): add initial expression support & store to manage

* fix(qbv2): format issue
2026-05-04 16:22:52 +00:00
Naman Verma
55487dde3a Merge branch 'nv/other-dashboard-v2-update-methods' into nv/delete-v2-dashboard 2026-05-04 19:27:40 +05:30
Naman Verma
fc5717af51 Merge branch 'nv/v2-dashboard-update' into nv/other-dashboard-v2-update-methods 2026-05-04 19:27:26 +05:30
Naman Verma
8bf650192e Merge branch 'nv/v2-dashboard-get' into nv/v2-dashboard-update 2026-05-04 19:27:14 +05:30
Naman Verma
f8fb7e5f8d Merge branch 'nv/v2-dashboard-create' into nv/v2-dashboard-get 2026-05-04 19:27:02 +05:30
Naman Verma
ff578f7d92 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-04 19:26:49 +05:30
Naman Verma
cd630b1152 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-04 19:26:36 +05:30
Naman Verma
bd0842ac17 fix: query-less panels not allowed 2026-05-04 19:25:49 +05:30
Vinicius Lourenço
20dd264ac1 feat(infra-monitoring): use new table component (#11122)
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(infra-monitoring): use new table component

* test(k8s-base-list): try fix issue with flaky test

* fix(table): tweaks in the layout

* fix(pr-comments): usage of const and move disable lint to line

* fix(css): format of css file

* test(k8s-base-list): flaky test

* test(k8s-base-list): second try to fix flaky test

* fix(table): have different ids for expanded table

* fix(k8s-base-list): third attempt to de-flaky test

* refactor(table): tiny adjustments on table

* fix(k8s-empty-state): better title size
2026-05-04 13:16:23 +00:00
Vishal Sharma
8a7793794d feat(global): add ai assistant url to global config (#11171) 2026-05-04 13:06:33 +00:00
Pandey
680bcd08c3 fix(types): correct OpenAPI schema for AuthDomainConfig and PostableChannel (#11164)
* fix(authtypes): embed values and expose AuthDomainConfig oneOf

GettableAuthDomain now embeds StorableAuthDomain and AuthDomainConfig
by value so the response flattens correctly. AuthDomainConfig also
implements jsonschema.OneOfExposer over the SAML/Google/OIDC variants.

* fix(alertmanagertypes): expose PostableChannel JSONSchema

PostableChannel now implements jsonschema.Exposer, requiring name
and a oneOf branch per *_configs field so the OpenAPI request body
for POST /channels matches the runtime contract enforced in
NewChannelFromReceiver. Switched the route's Request type from
Receiver to PostableChannel and regenerated the OpenAPI spec.

* fix(alertmanagertypes): use components/schemas prefix in PostableChannel refs

The standalone reflector inside JSONSchema defaulted to #/definitions/
prefix, producing dangling refs to ConfigDiscordConfig etc. that broke
the generated frontend client. Pass DefinitionsPrefix("#/components/schemas/")
so refs point to existing OpenAPI components, and regenerate the frontend
Orval client.

* feat(authdomain): add GET /api/v1/domains/{id} endpoint

Returns a single GettableAuthDomain scoped to the caller's organization,
backed by the existing module.GetByOrgIDAndID. Adds Get to the Handler
interface, wires the route under AdminAccess, and regenerates the
OpenAPI spec and frontend Orval client.

* feat(authtypes): expose AuthNProvider enum in OpenAPI schema

AuthNProvider now implements jsonschema.Enum, narrowing the generated
TypeScript type from string to a typed enum. Updated callers in the
auth-domain settings UI and mocks to use AuthtypesAuthNProviderDTO,
and added an early-return guard in the create/edit submit handler so
TS can narrow the union before passing it as ssoType.

* chore(types): document oneOf/discriminator mismatch on PostableChannel and AuthDomainConfig

Both types emit a oneOf in the OpenAPI spec but neither shape supports an
OpenAPI discriminator: PostableChannel implies the variant by which *_configs
field is present, and AuthDomainConfig keeps the variant payload in a
sibling field instead of being the payload itself. Leave a TODO pointing at
ruletypes.RuleThresholdData as the envelope pattern to migrate to.

* fix(ruletypes): handle string driver values in Schedule.Scan and Recurrence.Scan

The Scan methods only handled []byte and silently no-op'd on anything
else. SQLite's TEXT columns come back as string from the driver, so
every GET of a planned_maintenance returned a zero-valued Schedule
(empty timezone, 0001-01-01 startTime/endTime, no recurrence) — even
though Create + Update wrote the values correctly.

Switch on src type, accept []byte, string, and nil; error on anything
else. Aligns Schedule with the existing pattern; in Recurrence fixes
the receiver — Unmarshal was being passed src (the interface{} arg)
rather than r.
2026-05-04 18:00:43 +05:30
Naman Verma
b3e3dd13b4 Merge branch 'nv/other-dashboard-v2-update-methods' into nv/delete-v2-dashboard 2026-05-04 17:48:42 +05:30
Naman Verma
710d5531f3 Merge branch 'nv/v2-dashboard-update' into nv/other-dashboard-v2-update-methods 2026-05-04 17:45:15 +05:30
Naman Verma
e37e427079 fix: merge fixes 2026-05-04 17:40:46 +05:30
Naman Verma
1e99ab4659 Merge branch 'nv/v2-dashboard-get' into nv/v2-dashboard-update 2026-05-04 17:40:26 +05:30
Naman Verma
3353cda021 Merge branch 'nv/v2-dashboard-create' into nv/v2-dashboard-get 2026-05-04 17:35:33 +05:30
Naman Verma
f5a71037bf Merge branch 'nv/v2-dashboard-create' into nv/v2-dashboard-get 2026-05-04 17:33:03 +05:30
Naman Verma
97b85c386a fix: no v2 package and its consequences 2026-05-04 17:27:58 +05:30
Naman Verma
00bdf50c1c fix: no v2 package and its consequences 2026-05-04 17:26:12 +05:30
Naman Verma
5dec4ec580 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-04 17:18:39 +05:30
Naman Verma
325767c240 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-04 17:17:32 +05:30
Naman Verma
5fed2a4585 chore: no v2 subpackage 2026-05-04 17:16:39 +05:30
Vinicius Lourenço
5cf0e0fbb9 Reapply "feat(global-time-store): add support to context, url persistence, store persistence, drift handle (#11081)" (#11152) (#11157)
This reverts commit 8b13f004ed.
2026-05-04 11:04:26 +00:00
Naman Verma
664337ae0f Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-05-04 16:19:29 +05:30
Naman Verma
a0ea276681 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-05-04 16:18:03 +05:30
Naman Verma
2dc8699f08 fix: wrap errors 2026-05-04 14:55:38 +05:30
Naman Verma
ed81ed8ab5 fix: no need for copying textboxvariablespec 2026-05-04 14:44:42 +05:30
Naman Verma
48c9da19df fix: return 500 err if spec is nil for composite kind w/ code comment 2026-05-04 14:34:16 +05:30
Naman Verma
eb9663d518 fix: remove extra (un)marshal cycle 2026-05-04 14:18:37 +05:30
Naman Verma
a56a862338 fix: add allowed values in err messages 2026-05-04 14:16:22 +05:30
Naman Verma
021f33f65e Merge branch 'main' into nv/dashboardv2 2026-05-04 12:52:31 +05:30
Naman Verma
ca96c71146 feat: delete dashboard v2 API and hard delete cron job 2026-05-03 15:01:43 +05:30
Naman Verma
de2909d1d1 feat: lock, unlock, create public, update public v2 dashboard APIs 2026-05-03 14:38:24 +05:30
Naman Verma
f311fcabf7 feat: v2 dashboard update API 2026-04-29 18:39:48 +05:30
Naman Verma
a37c07f881 feat: v2 dashboard GET API 2026-04-29 15:12:47 +05:30
Naman Verma
4d9386f418 fix: merge conflicts 2026-04-29 14:36:39 +05:30
Naman Verma
737473521d Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-04-29 14:33:25 +05:30
Naman Verma
1863db8ba8 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-29 14:32:14 +05:30
Naman Verma
661af09a13 Merge branch 'main' into nv/dashboardv2 2026-04-29 14:31:59 +05:30
Naman Verma
6024fa2b91 fix: remove extra spec from builder query marshalling 2026-04-29 14:31:16 +05:30
Naman Verma
8996a96387 chore: use existing mapper 2026-04-29 14:09:34 +05:30
Naman Verma
d6db5c2aab test: integration test fixes 2026-04-29 12:56:14 +05:30
Naman Verma
709590ea1b test: integration tests for create API 2026-04-29 12:23:12 +05:30
Naman Verma
1add46b4c5 fix: module should also validate postable dashboard 2026-04-28 20:05:38 +05:30
Naman Verma
8401261e20 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-04-28 20:04:33 +05:30
Naman Verma
0ff34a7274 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 20:04:08 +05:30
Naman Verma
44e3bd9608 chore: separate method for validation 2026-04-28 20:03:48 +05:30
Naman Verma
c3944d779e fix: more dashboard request validations 2026-04-28 19:59:11 +05:30
Naman Verma
f5ec783a53 fix: go lint fix 2026-04-28 19:33:28 +05:30
Naman Verma
35b729c425 Merge branch 'nv/tags-for-dashboard-create' into nv/v2-dashboard-create 2026-04-28 19:30:42 +05:30
Naman Verma
4f43c3d803 fix: use existing tag's casing if new tag is a prefix of an existing tag 2026-04-28 19:30:07 +05:30
Naman Verma
5dbde6c64d fix: only return name of a tag in dashboard response 2026-04-28 19:13:03 +05:30
Naman Verma
fb6fdd54ec feat: v2 create dashboard API 2026-04-28 15:05:29 +05:30
Naman Verma
64b8ba62da Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 15:04:11 +05:30
Naman Verma
7c66df408b Merge branch 'main' into nv/dashboardv2 2026-04-28 15:04:03 +05:30
Naman Verma
54049de391 chore: follow proper unmarshal json method structure 2026-04-28 15:02:49 +05:30
Naman Verma
a82f4237c8 Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 09:52:23 +05:30
Naman Verma
89606b6238 Merge branch 'main' into nv/dashboardv2 2026-04-28 09:52:13 +05:30
Naman Verma
db5ce958eb Merge branch 'nv/dashboardv2' into nv/tags-for-dashboard-create 2026-04-28 09:49:01 +05:30
Naman Verma
c8d3a9a54b feat: enum for entity type that other modules can register 2026-04-28 09:47:24 +05:30
Naman Verma
637870b1fc feat: define tags module for v2 dashboard creation 2026-04-27 22:14:47 +05:30
Naman Verma
d46a7e24c9 Merge branch 'main' into nv/dashboardv2 2026-04-27 22:12:12 +05:30
Naman Verma
2a451e1c31 test: test for drift detection mechanics 2026-04-27 18:57:41 +05:30
Naman Verma
60b6d1d890 chore: better method name extractKindAndSpec 2026-04-27 18:42:31 +05:30
Naman Verma
36f755b232 chore: cleanup comments 2026-04-27 18:39:41 +05:30
Naman Verma
c1b3e3683a chore: code movement 2026-04-27 18:37:57 +05:30
Naman Verma
4c68544b1a chore: go lint fix (godot) 2026-04-27 18:37:05 +05:30
Naman Verma
90d9ab95f9 chore: code movement 2026-04-27 18:35:18 +05:30
Naman Verma
065e712e0c chore: code movement 2026-04-27 18:33:48 +05:30
Naman Verma
50db309ecd chore: code movement 2026-04-27 18:32:41 +05:30
Naman Verma
261bc552b0 chore: cleanup testing code 2026-04-27 18:24:52 +05:30
Naman Verma
bab720e98b Merge branch 'main' into nv/dashboardv2 2026-04-27 18:21:09 +05:30
Naman Verma
71fef6636b chore: better method name 2026-04-27 18:18:14 +05:30
Naman Verma
fc3cdecbbb chore: cleaner comment 2026-04-27 18:15:21 +05:30
Naman Verma
860fcfa641 chore: cleaner comment 2026-04-27 18:14:27 +05:30
Naman Verma
a090e3a4aa chore: cleaner comment 2026-04-27 18:14:02 +05:30
Naman Verma
6cf73e2ade chore: better comment to explain what restrictKindToLiteral does 2026-04-27 18:13:34 +05:30
Naman Verma
bbcb6a45d6 chore: renames and code rearrangement 2026-04-27 17:53:54 +05:30
Naman Verma
d13934febc fix: remove textbox plugin from openapi spec 2026-04-27 17:29:36 +05:30
Naman Verma
d5a7b7523d fix: strict decode variable spec as well 2026-04-27 17:27:51 +05:30
Naman Verma
5b8984f131 Merge branch 'main' into nv/dashboardv2 2026-04-27 17:18:44 +05:30
Naman Verma
6ddc5f1f12 chore: better error messages 2026-04-27 17:18:11 +05:30
Naman Verma
055968bfad fix: dot at the end of a comment 2026-04-27 17:07:58 +05:30
Naman Verma
1bf0f38ed9 fix: js lint errors 2026-04-27 17:07:38 +05:30
Naman Verma
842125e20a chore: too many comments 2026-04-27 16:50:41 +05:30
Naman Verma
6dab35caf8 chore: better file name 2026-04-27 16:43:42 +05:30
Naman Verma
047e9e2001 chore: better file names 2026-04-27 16:42:31 +05:30
Naman Verma
45eaa7db58 test: add tests for spec wrappers 2026-04-27 16:36:27 +05:30
Naman Verma
8a3d894eba chore: comment cleanup 2026-04-27 16:32:29 +05:30
Naman Verma
5239060b53 chore: move plugin maps to correct file 2026-04-27 16:30:33 +05:30
Naman Verma
42c6f507ac test: more descriptive test file name 2026-04-27 15:42:54 +05:30
Naman Verma
1b695a0b80 chore: separate file for perses replicas 2026-04-27 15:42:21 +05:30
Naman Verma
438cfab155 chore: comment clean up 2026-04-27 15:39:46 +05:30
Naman Verma
69f7617e01 Merge branch 'main' into nv/dashboardv2 2026-04-27 15:36:58 +05:30
Naman Verma
4420a7e1fc test: much bigger json for data column 2026-04-24 22:16:03 +05:30
Naman Verma
b4bc68c5c5 test: data column in perf tests should match real data 2026-04-24 17:17:37 +05:30
Naman Verma
eb9eb317cc test: perf test script for both sql flavours 2026-04-23 17:14:33 +05:30
Naman Verma
0b1eb16a42 test: fixes in dashboard perf testing data generator 2026-04-23 15:42:58 +05:30
Naman Verma
05a4d12183 test: script to generate test dashboard data in a sql db 2026-04-23 14:19:58 +05:30
Naman Verma
bbaf64c4f0 feat: openapi spec generation 2026-04-21 13:41:06 +05:30
618 changed files with 22910 additions and 13296 deletions

View File

@@ -27,6 +27,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
@@ -100,8 +101,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, tagModule)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -42,6 +42,7 @@ import (
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
@@ -144,8 +145,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, dashboardModule), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -13,6 +13,8 @@ global:
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
# mcp_url: <unset>
# the url of the SigNoz AI Assistant server. when unset, the AI Assistant is hidden in the frontend.
# ai_assistant_url: <unset>
##################### Version #####################
version:

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,10 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@@ -30,9 +32,9 @@ type module struct {
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
func NewModule(store dashboardtypes.Store, sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
pkgDashboardModule := pkgimpldashboard.NewModule(store, sqlstore, settings, analytics, orgGetter, queryParser, tagModule)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -197,6 +199,72 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, postable)
}
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updateable)
}
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
}
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) CreatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, postable dashboardtypes.PostablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
if _, err := module.licensing.GetActive(ctx, orgID); err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existing, err := module.pkgDashboardModule.GetV2(ctx, orgID, id)
if err != nil {
return nil, err
}
if existing.PublicConfig != nil {
return nil, errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", id)
}
publicDashboard := dashboardtypes.NewPublicDashboard(postable.TimeRangeEnabled, postable.DefaultTimeRange, id)
if err := module.store.CreatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard)); err != nil {
return nil, err
}
existing.PublicConfig = publicDashboard
return existing, nil
}
func (module *module) DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, deletedBy string) error {
return module.pkgDashboardModule.DeleteV2(ctx, orgID, id, deletedBy)
}
func (module *module) UpdatePublicV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable dashboardtypes.UpdatablePublicDashboard) (*dashboardtypes.DashboardV2, error) {
if _, err := module.licensing.GetActive(ctx, orgID); err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existing, err := module.pkgDashboardModule.GetV2(ctx, orgID, id)
if err != nil {
return nil, err
}
if existing.PublicConfig == nil {
return nil, errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodePublicDashboardNotFound, "dashboard with id %s isn't public", id)
}
existing.PublicConfig.Update(updatable.TimeRangeEnabled, updatable.DefaultTimeRange)
if err := module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(existing.PublicConfig)); err != nil {
return nil, err
}
return existing, nil
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}

View File

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

View File

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

View File

@@ -15,13 +15,14 @@
"lint:generated": "oxlint ./src/api/generated --fix",
"lint:fix": "oxlint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs && patch-package",
"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",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:changedsince": "vitest run --changed HEAD~1 --coverage",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
},
@@ -170,7 +171,7 @@
"@commitlint/config-conventional": "^20.4.2",
"@faker-js/faker": "9.3.0",
"@jest/globals": "30.2.0",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
@@ -198,11 +199,9 @@
"@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",
@@ -217,7 +216,6 @@
"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,15 +232,15 @@
"ts-jest": "29.4.6",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",
"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",
"vitest": "4.1.5"
"vite-tsconfig-paths": "6.1.1"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"oxlint --fix --quiet",
"oxlint --fix",
"oxfmt --write",
"sh -c tsgo --noEmit"
],

View File

@@ -1,46 +0,0 @@
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,4 +1,3 @@
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';
@@ -23,13 +22,13 @@ import { ROLES, USER_ROLES } from 'types/roles';
import PrivateRoute from '../Private';
// Mock localStorage APIs
const mockLocalStorage = vi.hoisted((): Record<string, string> => ({}));
vi.mock('api/browser/localstorage/get', () => ({
const mockLocalStorage: Record<string, string> = {};
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: (key: string): string | null => mockLocalStorage[key] || null,
}));
vi.mock('api/browser/localstorage/set', () => ({
jest.mock('api/browser/localstorage/set', () => ({
__esModule: true,
default: (key: string, value: string): void => {
mockLocalStorage[key] = value;
@@ -37,29 +36,27 @@ vi.mock('api/browser/localstorage/set', () => ({
}));
// Mock useGetTenantLicense hook
const mockTenantLicense = vi.hoisted(() => ({ isCloudUser: true }));
vi.mock('hooks/useGetTenantLicense', () => ({
let mockIsCloudUser = true;
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: (): {
isCloudUser: boolean;
isEnterpriseSelfHostedUser: boolean;
isCommunityUser: boolean;
isCommunityEnterpriseUser: boolean;
} => ({
isCloudUser: mockTenantLicense.isCloudUser,
isEnterpriseSelfHostedUser: !mockTenantLicense.isCloudUser,
isCloudUser: mockIsCloudUser,
isEnterpriseSelfHostedUser: !mockIsCloudUser,
isCommunityUser: false,
isCommunityEnterpriseUser: false,
}),
}));
// Mock react-query for users fetch
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 },
let mockUsersData: { email: string }[] = [];
jest.mock('api/generated/services/users', () => ({
...jest.requireActual('api/generated/services/users'),
useListUsers: jest.fn(() => ({
data: { data: mockUsersData },
isFetching: false,
})),
}));
@@ -179,13 +176,13 @@ function createMockAppContext(
orgPreferencesFetchError: null,
changelog: null,
showChangelogModal: false,
activeLicenseRefetch: vi.fn(),
updateUser: vi.fn(),
updateOrgPreferences: vi.fn(),
updateUserPreferenceInContext: vi.fn(),
updateOrg: vi.fn(),
updateChangelog: vi.fn(),
toggleChangelogModal: vi.fn(),
activeLicenseRefetch: jest.fn(),
updateUser: jest.fn(),
updateOrgPreferences: jest.fn(),
updateUserPreferenceInContext: jest.fn(),
updateOrg: jest.fn(),
updateChangelog: jest.fn(),
toggleChangelogModal: jest.fn(),
versionData: { version: '1.0.0', ee: 'Y', setupCompleted: true },
hasEditPermission: true,
...overrides,
@@ -205,7 +202,7 @@ function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
isCloudUser = true,
} = options;
mockTenantLicense.isCloudUser = isCloudUser;
mockIsCloudUser = isCloudUser;
const contextValue = createMockAppContext(appContext);
@@ -248,11 +245,11 @@ function assertRendersChildren(): void {
describe('PrivateRoute', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
queryClient.clear();
Object.keys(mockLocalStorage).forEach((key) => delete mockLocalStorage[key]);
mockTenantLicense.isCloudUser = true;
mockUsers.data = [];
mockIsCloudUser = true;
mockUsersData = [];
});
describe('Old Routes Handling', () => {
@@ -1017,7 +1014,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
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1056,7 +1053,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
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1127,7 +1124,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
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.BILLING,
@@ -1152,7 +1149,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is blocked and accessing settings', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.SETTINGS,
@@ -1176,7 +1173,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is suspended (DEFAULTED)', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1203,7 +1200,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is access restricted (TERMINATED)', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1230,7 +1227,7 @@ describe('PrivateRoute', () => {
});
it('should not redirect to onboarding when workspace is access restricted (EXPIRED)', async () => {
mockUsers.data = [{ email: 'test@example.com' }];
mockUsersData = [{ email: 'test@example.com' }];
renderPrivateRoute({
initialRoute: ROUTES.HOME,

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,11 @@ import type {
import type {
AuthtypesPostableAuthDomainDTO,
AuthtypesUpdateableAuthDomainDTO,
CreateAuthDomain200,
AuthtypesUpdatableAuthDomainDTO,
CreateAuthDomain201,
DeleteAuthDomainPathParameters,
GetAuthDomain200,
GetAuthDomainPathParameters,
ListAuthDomains200,
RenderErrorResponseDTO,
UpdateAuthDomainPathParameters,
@@ -124,7 +126,7 @@ export const createAuthDomain = (
authtypesPostableAuthDomainDTO: BodyType<AuthtypesPostableAuthDomainDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAuthDomain200>({
return GeneratedAPIInstance<CreateAuthDomain201>({
url: `/api/v1/domains`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -277,19 +279,122 @@ export const useDeleteAuthDomain = <
return useMutation(mutationOptions);
};
/**
* This endpoint returns an auth domain by ID
* @summary Get auth domain by ID
*/
export const getAuthDomain = (
{ id }: GetAuthDomainPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetAuthDomain200>({
url: `/api/v1/domains/${id}`,
method: 'GET',
signal,
});
};
export const getGetAuthDomainQueryKey = ({
id,
}: GetAuthDomainPathParameters) => {
return [`/api/v1/domains/${id}`] as const;
};
export const getGetAuthDomainQueryOptions = <
TData = Awaited<ReturnType<typeof getAuthDomain>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetAuthDomainPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAuthDomainQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAuthDomain>>> = ({
signal,
}) => getAuthDomain({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAuthDomainQueryResult = NonNullable<
Awaited<ReturnType<typeof getAuthDomain>>
>;
export type GetAuthDomainQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get auth domain by ID
*/
export function useGetAuthDomain<
TData = Awaited<ReturnType<typeof getAuthDomain>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetAuthDomainPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAuthDomainQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get auth domain by ID
*/
export const invalidateGetAuthDomain = async (
queryClient: QueryClient,
{ id }: GetAuthDomainPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAuthDomainQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an auth domain
* @summary Update auth domain
*/
export const updateAuthDomain = (
{ id }: UpdateAuthDomainPathParameters,
authtypesUpdateableAuthDomainDTO: BodyType<AuthtypesUpdateableAuthDomainDTO>,
authtypesUpdatableAuthDomainDTO: BodyType<AuthtypesUpdatableAuthDomainDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/domains/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: authtypesUpdateableAuthDomainDTO,
data: authtypesUpdatableAuthDomainDTO,
});
};
@@ -302,7 +407,7 @@ export const getUpdateAuthDomainMutationOptions = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
>;
@@ -311,7 +416,7 @@ export const getUpdateAuthDomainMutationOptions = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
> => {
@@ -328,7 +433,7 @@ export const getUpdateAuthDomainMutationOptions = <
Awaited<ReturnType<typeof updateAuthDomain>>,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -343,7 +448,7 @@ export type UpdateAuthDomainMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAuthDomain>>
>;
export type UpdateAuthDomainMutationBody =
BodyType<AuthtypesUpdateableAuthDomainDTO>;
BodyType<AuthtypesUpdatableAuthDomainDTO>;
export type UpdateAuthDomainMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -358,7 +463,7 @@ export const useUpdateAuthDomain = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
>;
@@ -367,7 +472,7 @@ export const useUpdateAuthDomain = <
TError,
{
pathParams: UpdateAuthDomainPathParameters;
data: BodyType<AuthtypesUpdateableAuthDomainDTO>;
data: BodyType<AuthtypesUpdatableAuthDomainDTO>;
},
TContext
> => {

View File

@@ -18,6 +18,7 @@ import type {
} from 'react-query';
import type {
AlertmanagertypesPostableChannelDTO,
ConfigReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
@@ -122,14 +123,14 @@ export const invalidateListChannels = async (
* @summary Create notification channel
*/
export const createChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
alertmanagertypesPostableChannelDTO: BodyType<AlertmanagertypesPostableChannelDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateChannel201>({
url: `/api/v1/channels`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
data: alertmanagertypesPostableChannelDTO,
signal,
});
};
@@ -141,13 +142,13 @@ export const getCreateChannelMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
> => {
const mutationKey = ['createChannel'];
@@ -161,7 +162,7 @@ export const getCreateChannelMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
{ data: BodyType<AlertmanagertypesPostableChannelDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -174,7 +175,8 @@ export const getCreateChannelMutationOptions = <
export type CreateChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof createChannel>>
>;
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type CreateChannelMutationBody =
BodyType<AlertmanagertypesPostableChannelDTO>;
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -187,13 +189,13 @@ export const useCreateChannel = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
{ data: BodyType<AlertmanagertypesPostableChannelDTO> },
TContext
> => {
const mutationOptions = getCreateChannelMutationOptions(options);

View File

@@ -18,8 +18,10 @@ import type {
} from 'react-query';
import type {
CreateDashboardV2201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeletePublicDashboardPathParameters,
@@ -634,3 +636,88 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* This endpoint creates a v2-shape dashboard with structured metadata, a typed data tree, and resolved tags.
* @summary Create dashboard (v2)
*/
export const createDashboardV2 = (
dashboardtypesPostableDashboardV2DTO: BodyType<DashboardtypesPostableDashboardV2DTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardV2201>({
url: `/api/v2/dashboards`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardV2DTO,
signal,
});
};
export const getCreateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
const mutationKey = ['createDashboardV2'];
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 createDashboardV2>>,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardV2(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardV2>>
>;
export type CreateDashboardV2MutationBody =
BodyType<DashboardtypesPostableDashboardV2DTO>;
export type CreateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard (v2)
*/
export const useCreateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
const mutationOptions = getCreateDashboardV2MutationOptions(options);
return useMutation(mutationOptions);
};

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,6 @@ 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
@@ -130,10 +129,13 @@ export const interceptorRejected = async (
afterLogin(response.data.accessToken, response.data.refreshToken, true);
try {
const reResponse = await retryRequestAfterAuth(
value.config,
response.data.accessToken,
);
const reResponse = await axios({
...value.config,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessToken}`,
},
});
return await Promise.resolve(reResponse);
} catch (error) {

View File

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

View File

@@ -1,17 +0,0 @@
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,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,

View File

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

View File

@@ -1,16 +1,15 @@
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';
vi.mock('../../../api/browser/localstorage/get', () => ({
jest.mock('../../../api/browser/localstorage/get', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
// Access the mocked function
const mockGet = vi.mocked(getLocal);
const mockGet = getLocal as unknown as jest.Mock;
describe('AppLoading', () => {
const SIGNOZ_TEXT = 'SigNoz';
@@ -19,12 +18,12 @@ describe('AppLoading', () => {
const CONTAINER_SELECTOR = '.app-loading-container';
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('should render loading screen with dark theme by default', () => {
// Mock localStorage to return dark theme (or undefined for default)
mockGet.mockReturnValue(null);
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
@@ -41,17 +40,14 @@ describe('AppLoading', () => {
it('should have proper structure and content', () => {
// Mock localStorage to return dark theme
mockGet.mockReturnValue(null);
mockGet.mockReturnValue(undefined);
render(<AppLoading />);
// Check for brand logo
const logo = screen.getByAltText(SIGNOZ_TEXT);
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute(
'src',
expect.stringContaining('data:image/svg+xml'),
);
expect(logo).toHaveAttribute('src', 'test-file-stub');
// 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,25 +37,27 @@ const mockChangelog: ChangelogSchema = {
};
// Mock react-markdown to just render children as plain text
vi.mock('react-markdown', () => ({
default: function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
}));
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
// mock useAppContext
vi.mock('providers/App/App', () => ({
useAppContext: vi.fn(() => ({
updateUserPreferenceInContext: vi.fn(),
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({
updateUserPreferenceInContext: jest.fn(),
userPreferences: [
{
name: 'last_seen_changelog_version',
name: USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
value: 'v1.0.0',
},
],
})),
}));
function renderChangelog(onClose: () => void = vi.fn()): void {
function renderChangelog(onClose: () => void = jest.fn()): void {
render(
<MockQueryClientProvider>
<ChangelogModal changelog={mockChangelog} onClose={onClose} />
@@ -76,14 +78,14 @@ describe('ChangelogModal', () => {
});
it('calls onClose when Skip for now is clicked', () => {
const onClose = vi.fn();
const onClose = jest.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 = vi.fn();
window.open = jest.fn();
renderChangelog();
fireEvent.click(screen.getByText('Update my workspace'));
expect(window.open).toHaveBeenCalledWith(
@@ -98,7 +100,7 @@ describe('ChangelogModal', () => {
const scrollBtn = screen.getByTestId('scroll-more-btn');
const contentDiv = screen.getByTestId('changelog-content');
if (contentDiv) {
contentDiv.scrollTo = vi.fn();
contentDiv.scrollTo = jest.fn();
}
fireEvent.click(scrollBtn);
if (contentDiv) {

View File

@@ -1,7 +1,6 @@
/* 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,
@@ -10,11 +9,13 @@ import {
import ChangelogRenderer from '../components/ChangelogRenderer';
// Mock react-markdown to just render children as plain text
vi.mock('react-markdown', () => ({
default: function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
}));
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
const mockChangelog: ChangelogSchema = {
id: 1,

View File

@@ -1,40 +1,16 @@
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 } = vi.hoisted(() => ({
mockCopyToClipboard: vi.fn(),
}));
const mockCopyToClipboard = jest.fn();
vi.mock('react-use', () => ({
jest.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();
@@ -57,7 +33,7 @@ describe('CodeBlock', () => {
});
it('copies code and triggers callback', async () => {
const onCopy = vi.fn();
const onCopy = jest.fn();
render(<CodeBlock code="SELECT * FROM logs;" onCopy={onCopy} />);
fireEvent.click(screen.getByRole('button', { name: /copy code/i }));

View File

@@ -1,49 +1,28 @@
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 } from 'tests/test-utils';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
vi.mock('@signozhq/icons', () => ({
X: ({ size: _size }: any): JSX.Element => <span aria-hidden="true" />,
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));
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 mockToast = jest.mocked(toast);
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 () => ({
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
__esModule: true,
...(await vi.importActual('providers/ErrorModalProvider')),
useErrorModal: vi.fn(() => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
@@ -61,7 +40,7 @@ function renderModal(): ReturnType<typeof render> {
describe('CreateServiceAccountModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
server.use(
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: {} })),
@@ -147,16 +126,12 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByRole('dialog', {
const dialog = await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(dialog);
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -1,37 +1,36 @@
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';
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useLocation: vi.fn().mockReturnValue({
useLocation: jest.fn().mockReturnValue({
pathname: '/test-path',
}),
};
});
vi.mock('react-redux', async () => ({
...(await vi.importActual('react-redux')),
useDispatch: vi.fn(() => vi.fn()),
useSelector: vi.fn(() => ({
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(() => jest.fn()),
useSelector: jest.fn(() => ({
minTime: 0,
maxTime: Date.now(),
})),
}));
vi.mock('providers/Timezone', async () => {
const actual = await vi.importActual('providers/Timezone');
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');
return {
...actual,
useTimezone: vi.fn().mockReturnValue({
useTimezone: jest.fn().mockReturnValue({
timezone: {
value: 'UTC',
offset: '+00:00',
@@ -46,30 +45,6 @@ vi.mock('providers/Timezone', async () => {
};
});
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;
@@ -148,8 +123,8 @@ describe('CustomTimePicker', () => {
});
it('applies valid shorthand on Enter', () => {
const onValid = vi.fn();
const onError = vi.fn();
const onValid = jest.fn();
const onError = jest.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
@@ -166,9 +141,9 @@ describe('CustomTimePicker', () => {
});
it('sets error and updates custom time status for invalid shorthand exceeding max allowed window', () => {
const onValid = vi.fn();
const onError = vi.fn();
const onCustomTimeStatusUpdate = vi.fn();
const onValid = jest.fn();
const onError = jest.fn();
const onCustomTimeStatusUpdate = jest.fn();
render(
<Wrapper
@@ -191,8 +166,8 @@ describe('CustomTimePicker', () => {
});
it('treats close after change like pressing Enter (blur + chevron)', () => {
const onValid = vi.fn();
const onError = vi.fn();
const onValid = jest.fn();
const onError = jest.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
@@ -216,8 +191,8 @@ describe('CustomTimePicker', () => {
});
it('applies epoch start/end range on Enter via onCustomDateHandler', () => {
const onCustomDateHandler = vi.fn();
const onError = vi.fn();
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
render(
<Wrapper onCustomDateHandler={onCustomDateHandler} onError={onError} />,
@@ -238,9 +213,9 @@ describe('CustomTimePicker', () => {
});
it('uses validateTimeRange result for generic formatted ranges (valid case)', () => {
const validateTimeRangeSpy = vi.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = vi.fn();
const onError = vi.fn();
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: true,
@@ -269,9 +244,9 @@ describe('CustomTimePicker', () => {
});
it('uses validateTimeRange result for generic formatted ranges (invalid case)', () => {
const validateTimeRangeSpy = vi.spyOn(timeUtils, 'validateTimeRange');
const onValid = vi.fn();
const onError = vi.fn();
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onValid = jest.fn();
const onError = jest.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: false,

View File

@@ -2,33 +2,23 @@ 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,
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(),
}));
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();
interface MockAppState {
globalTime: Pick<GlobalReducer, 'minTime' | 'maxTime'>;
}
vi.mock('react-redux', () => ({
useDispatch: (): Mock => mockDispatch,
jest.mock('react-redux', () => ({
useDispatch: (): jest.Mock => mockDispatch,
useSelector: (selector: (state: MockAppState) => unknown): unknown => {
const mockState: MockAppState = {
globalTime: {
@@ -40,8 +30,8 @@ vi.mock('react-redux', () => ({
},
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: Mock } => ({
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
@@ -53,7 +43,7 @@ interface MockUrlQuery {
toString: () => string;
}
vi.mock('hooks/useUrlQuery', () => ({
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): MockUrlQuery => ({
delete: mockUrlQueryDelete,
@@ -63,46 +53,26 @@ vi.mock('hooks/useUrlQuery', () => ({
}),
}));
vi.mock('providers/Timezone', () => ({
jest.mock('providers/Timezone', () => ({
useTimezone: (): { timezone: { value: string; offset: string } } => ({
timezone: { value: 'UTC', offset: 'UTC' },
}),
}));
vi.mock('react-router-dom', () => ({
jest.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: vi.fn(),
onError: vi.fn(),
onSelect: jest.fn(),
onError: jest.fn(),
selectedValue: '15m',
selectedTime: '15m',
onValidCustomDateChange: vi.fn(),
onValidCustomDateChange: jest.fn(),
open: false,
setOpen: vi.fn(),
setOpen: jest.fn(),
items: [
{ value: '15m', label: 'Last 15 minutes' },
{ value: '1h', label: 'Last 1 hour' },
@@ -113,12 +83,12 @@ const defaultProps = {
describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(Date, 'now').mockReturnValue(NOW_MS);
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
});
afterEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
it('should render zoom out button when showLiveLogs is false', () => {

View File

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

View File

@@ -1,28 +1,27 @@
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: vi.fn().mockImplementation((query) => ({
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: 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()]),
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
}));
describe('DraggableTableRow Snapshot test', () => {

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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,16 +1,14 @@
import { describe, expect, it, vi } from 'vitest';
import { dragHandler, dropHandler } from '../utils';
vi.mock('react-dnd', () => ({
useDrop: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), vi.fn()]),
useDrag: vi.fn().mockImplementation(() => [vi.fn(), vi.fn(), 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()]),
}));
describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return true', () => {
const monitor = {
isOver: vi.fn().mockReturnValueOnce(true),
isOver: jest.fn().mockReturnValueOnce(true),
} as never;
const dropDataTruthy = dropHandler(monitor);
@@ -19,7 +17,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dropHandler return false', () => {
const monitor = {
isOver: vi.fn().mockReturnValueOnce(false),
isOver: jest.fn().mockReturnValueOnce(false),
} as never;
const dropDataFalsy = dropHandler(monitor);
@@ -28,7 +26,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dragHandler return true', () => {
const monitor = {
isDragging: vi.fn().mockReturnValueOnce(true),
isDragging: jest.fn().mockReturnValueOnce(true),
} as never;
const dragDataTruthy = dragHandler(monitor);
@@ -37,7 +35,7 @@ describe('Utils testing of DraggableTableRow component', () => {
it('Should dragHandler return false', () => {
const monitor = {
isDragging: vi.fn().mockReturnValueOnce(false),
isDragging: jest.fn().mockReturnValueOnce(false),
} as never;
const dragDataFalsy = dragHandler(monitor);

View File

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

View File

@@ -1,39 +1,10 @@
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';
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>
),
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: jest.fn(),
}));
describe('Editor', () => {
@@ -63,7 +34,7 @@ describe('Editor', () => {
});
it('renders with dark mode theme', () => {
(useIsDarkMode as Mock).mockImplementation(() => true);
(useIsDarkMode as jest.Mock).mockImplementation(() => true);
const { container } = render(<Editor value="dark mode text" />);
@@ -71,7 +42,7 @@ describe('Editor', () => {
});
it('renders with light mode theme', () => {
(useIsDarkMode as Mock).mockImplementation(() => false);
(useIsDarkMode as jest.Mock).mockImplementation(() => false);
const { container } = render(<Editor value="light mode text" />);

View File

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

View File

@@ -1,26 +1,19 @@
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 = vi.hoisted(() => ({
const mockVersionData = {
payload: {
ee: 'Y',
version: '1.0.0',
},
}));
vi.mock('react-query', async () => ({
...(await vi.importActual('react-query')),
};
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueryClient: (): { getQueryData: () => typeof mockVersionData } => ({
getQueryData: vi.fn(() => mockVersionData),
}),
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
getQueryData: jest.fn(() => mockVersionData),
}),
}));
const mockError: APIError = new APIError({
@@ -38,7 +31,7 @@ const mockError: APIError = new APIError({
});
describe('ErrorModal Component', () => {
it('should render the modal when open is true', () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the error message is displayed
expect(screen.getByText('An error occurred')).toBeInTheDocument();
@@ -48,14 +41,14 @@ describe('ErrorModal Component', () => {
});
it('should not render the modal when open is false', () => {
render(<ErrorModal error={mockError} open={false} onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open={false} onClose={jest.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 = vi.fn();
const onCloseMock = jest.fn();
render(<ErrorModal error={mockError} open onClose={onCloseMock} />);
// Click the close button
@@ -68,14 +61,14 @@ describe('ErrorModal Component', () => {
});
it('should display version data if available', async () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.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={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the messages count badge is displayed
expect(screen.getByText('MESSAGES')).toBeInTheDocument();
@@ -89,7 +82,7 @@ describe('ErrorModal Component', () => {
});
it('should render the open docs button when URL is provided', async () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
// Check if the open docs button is displayed
const openDocsButton = screen.getByTestId('error-docs-button');
@@ -102,7 +95,7 @@ describe('ErrorModal Component', () => {
});
it('should not display scroll for more if there are less than 10 messages', () => {
render(<ErrorModal error={mockError} open onClose={vi.fn()} />);
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
expect(screen.queryByText('Scroll for more')).not.toBeInTheDocument();
});
@@ -120,7 +113,7 @@ describe('ErrorModal Component', () => {
},
});
render(<ErrorModal error={longError} open onClose={vi.fn()} />);
render(<ErrorModal error={longError} open onClose={jest.fn()} />);
// Check if the scroll hint is displayed
expect(screen.getByText('Scroll for more')).toBeInTheDocument();
@@ -132,7 +125,7 @@ it('should render the trigger component if provided', () => {
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={vi.fn()}
onClose={jest.fn()}
/>,
);
@@ -146,7 +139,7 @@ it('should open the modal when the trigger component is clicked', async () => {
<ErrorModal
error={mockError}
triggerComponent={mockTrigger}
onClose={vi.fn()}
onClose={jest.fn()}
/>,
);
@@ -160,14 +153,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={vi.fn()} />);
render(<ErrorModal error={mockError} onClose={jest.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 = vi.fn();
const onCloseMock = jest.fn();
render(<ErrorModal error={mockError} onClose={onCloseMock} />);
// Click the trigger component
@@ -186,7 +179,9 @@ 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');
expect(modal[0]).toHaveClass('ant-zoom-leave');
const style = window.getComputedStyle(modal[0]);
expect(style.display).toBe('none');
});
});

View File

@@ -3,63 +3,57 @@ 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 = vi.hoisted(() => vi.fn());
const historyReplace = jest.fn();
vi.mock('react-router-dom', async () => {
const actual =
await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
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: 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('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: vi.fn(() => 'mockedPanelType'),
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
vi.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: vi.fn(() => ({
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: jest.fn(() => ({
data: { data: { data: viewMockData } },
isLoading: false,
error: null,
isRefetching: false,
refetch: vi.fn(),
refetch: jest.fn(),
})),
}));
vi.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: vi.fn(() => ({
mutateAsync: vi.fn(),
jest.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
vi.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: vi.fn(() => ({
mutateAsync: vi.fn(),
jest.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
// Mock usePreferenceSync
vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
@@ -72,8 +66,8 @@ vi.mock('providers/preferences/sync/usePreferenceSync', () => ({
},
loading: false,
error: null,
updateColumns: vi.fn(),
updateFormatting: vi.fn(),
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));

View File

@@ -2,22 +2,21 @@ 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';
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
vi.mock('antd', async () => ({
...(await vi.importActual('antd')),
useForm: vi.fn().mockReturnValue({
onFinish: vi.fn(),
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),
}));
@@ -30,7 +29,7 @@ describe('MenuItemGenerator', () => {
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={vi.fn()}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
@@ -48,7 +47,7 @@ describe('MenuItemGenerator', () => {
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={vi.fn()}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>

View File

@@ -1,16 +1,14 @@
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';
vi.mock('react-router-dom', async () => ({
...(await vi.importActual<typeof import('react-router-dom')>(
'react-router-dom',
)),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/services/`,
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
@@ -22,13 +20,13 @@ const queryClient = new QueryClient({
},
});
vi.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: vi.fn(() => 'mockedPanelType'),
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
vi.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: vi.fn(() => ({
mutateAsync: vi.fn(),
jest.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
@@ -38,8 +36,8 @@ describe('SaveViewWithName', () => {
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={vi.fn()}
refetchAllView={vi.fn()}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
@@ -52,8 +50,8 @@ describe('SaveViewWithName', () => {
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={vi.fn()}
refetchAllView={vi.fn()}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);

View File

@@ -7,7 +7,6 @@ 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,5 +1,3 @@
import { describe, expect, it } from 'vitest';
import dayjs from 'dayjs';
import { convertTimeRange, TIME_UNITS } from '../xAxisConfig';

View File

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

View File

@@ -1,5 +1,4 @@
import { ReactElement } from 'react';
import { describe, expect, it, vi } from 'vitest';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
@@ -13,12 +12,6 @@ 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,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import AnnouncementsModal from '../AnnouncementsModal';

View File

@@ -1,6 +1,4 @@
// 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';
@@ -11,38 +9,39 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
vi.mock('api/common/logEvent', () => ({
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: vi.fn(() => Promise.resolve()),
default: jest.fn(() => Promise.resolve()),
}));
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
vi.mock('@signozhq/ui', () => ({
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: vi.fn(),
error: vi.fn(),
success: jest.fn(),
error: jest.fn(),
},
}));
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: vi.fn(),
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
}));
vi.mock('container/Integrations/utils', () => ({
handleContactSupport: vi.fn(),
jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));
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 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 mockOnClose = vi.fn();
const mockOnClose = jest.fn();
const mockLocation = {
pathname: '/test-path',
@@ -50,7 +49,7 @@ const mockLocation = {
describe('FeedbackModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,

View File

@@ -1,47 +1,23 @@
// 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, waitFor } from '@testing-library/react';
import { render, screen } 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';
vi.mock('api/common/logEvent', () => ({
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
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', () => ({
jest.mock('../FeedbackModal', () => ({
__esModule: true,
default: ({ onClose }: { onClose: () => void }): JSX.Element => (
<div data-testid="feedback-modal">
@@ -52,27 +28,27 @@ vi.mock('../FeedbackModal', () => ({
),
}));
vi.mock('../ShareURLModal', () => ({
jest.mock('../ShareURLModal', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="share-modal">Share URL Modal</div>
),
}));
vi.mock('../AnnouncementsModal', () => ({
jest.mock('../AnnouncementsModal', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="announcements-modal">Announcements Modal</div>
),
}));
vi.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: vi.fn(),
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
}));
const mockLogEvent = logEvent as Mock;
const mockUseLocation = useLocation as Mock;
const mockUseGetTenantLicense = useGetTenantLicense as Mock;
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const defaultProps = {
enableAnnouncements: true,
@@ -86,7 +62,7 @@ const mockLocation = {
describe('HeaderRightSection', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockUseLocation.mockReturnValue(mockLocation);
// Default to licensed user (Enterprise or Cloud)
mockUseGetTenantLicense.mockReturnValue({
@@ -201,9 +177,7 @@ describe('HeaderRightSection', () => {
// Close feedback modal
const closeFeedbackButton = screen.getByText('Close Feedback');
await user.click(closeFeedbackButton);
await waitFor(() => {
expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument();
});
expect(screen.queryByTestId('feedback-modal')).not.toBeInTheDocument();
});
it('should close other modals when opening feedback modal', async () => {
@@ -223,9 +197,7 @@ describe('HeaderRightSection', () => {
await user.click(feedbackButton!);
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('share-modal')).not.toBeInTheDocument();
});
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
import { describe, expect, it, beforeEach, vi } from 'vitest';
import type { Mock } from 'vitest';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
@@ -12,39 +12,35 @@ import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
const hoistedReduxMocks = vi.hoisted(() => ({
useSelectorMock: vi.fn(),
}));
vi.mock('api/common/logEvent', () => ({
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
matchPath: vi.fn(),
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
matchPath: jest.fn(),
}));
vi.mock('hooks/useUrlQuery', () => ({
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-redux', async () => ({
...(await vi.importActual<typeof import('react-redux')>('react-redux')),
useSelector: hoistedReduxMocks.useSelectorMock,
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));
vi.mock('lib/getMinMax', () => ({
jest.mock('lib/getMinMax', () => ({
__esModule: true,
default: vi.fn(),
default: jest.fn(),
}));
vi.mock('react-use', async () => ({
...(await vi.importActual('react-use')),
useCopyToClipboard: vi.fn(),
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useCopyToClipboard: jest.fn(),
}));
// Mock window.location
@@ -57,29 +53,29 @@ Object.defineProperty(window, 'location', {
writable: true,
});
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 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 mockUrlQuery = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
toString: vi.fn(() => 'param=value'),
get: jest.fn(),
set: jest.fn(),
delete: jest.fn(),
toString: jest.fn(() => 'param=value'),
};
const mockHandleCopyToClipboard = vi.fn();
const mockHandleCopyToClipboard = jest.fn();
const TEST_PATH = '/test-path';
const ENABLE_ABSOLUTE_TIME_TEXT = 'Enable absolute time';
describe('ShareURLModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockUseLocation.mockReturnValue({
pathname: TEST_PATH,

View File

@@ -1,6 +1,3 @@
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';
@@ -15,114 +12,38 @@ const makeApiError = (message: string, code = StatusCodes.CONFLICT): APIError =>
error: { code: 'already_exists', message, url: '', errors: [] },
});
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(),
}),
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(),
},
}));
vi.mock('providers/ErrorModalProvider', async () => ({
const showErrorModal = jest.fn();
jest.mock('providers/ErrorModalProvider', () => ({
__esModule: true,
...(await vi.importActual<typeof import('providers/ErrorModalProvider')>(
'providers/ErrorModalProvider',
)),
useErrorModal: vi.fn(() => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
}));
const mockSendInvite = vi.mocked(sendInvite);
const mockInviteUsers = vi.mocked(inviteUsers);
const mockSendInvite = jest.mocked(sendInvite);
const mockInviteUsers = jest.mocked(inviteUsers);
const defaultProps = {
open: true,
onClose: vi.fn(),
onComplete: vi.fn(),
onClose: jest.fn(),
onComplete: jest.fn(),
};
describe('InviteMembersModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
showErrorModal.mockClear();
mockSendInvite.mockResolvedValue({
httpStatusCode: 200,
@@ -217,7 +138,7 @@ describe('InviteMembersModal', () => {
it('uses sendInvite (single) when only one row is filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = vi.fn();
const onComplete = jest.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);
@@ -322,7 +243,7 @@ describe('InviteMembersModal', () => {
it('uses inviteUsers (bulk) when multiple rows are filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = vi.fn();
const onComplete = jest.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);

View File

@@ -1,5 +1,4 @@
import { ComponentType, lazy as reactLazy, Suspense } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import React, { ComponentType, Suspense } from 'react';
import {
render,
screen,
@@ -8,16 +7,6 @@ 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>;
@@ -33,10 +22,6 @@ const loadSampleComponent = (): Promise<{
});
describe('Loadable', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('should render the lazily loaded component', async () => {
const LoadableSampleComponent = Loadable(loadSampleComponent);
@@ -53,9 +38,12 @@ describe('Loadable', () => {
});
it('should call lazy with the provided import path', () => {
const reactLazySpy = jest.spyOn(React, 'lazy');
Loadable(loadSampleComponent);
expect(vi.mocked(reactLazy)).toHaveBeenCalledTimes(1);
expect(vi.mocked(reactLazy)).toHaveBeenCalledWith(expect.any(Function));
expect(reactLazySpy).toHaveBeenCalledTimes(1);
expect(reactLazySpy).toHaveBeenCalledWith(expect.any(Function));
reactLazySpy.mockRestore();
});
});

View File

@@ -1,6 +1,5 @@
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,5 +1,3 @@
import { describe, expect, it } from 'vitest';
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';

View File

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

View File

@@ -1,27 +1,8 @@
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',
@@ -53,13 +34,13 @@ const defaultProps = {
currentPage: 1,
pageSize: 20,
searchQuery: '',
onPageChange: vi.fn(),
onRowClick: vi.fn(),
onPageChange: jest.fn(),
onRowClick: jest.fn(),
};
describe('MembersTable', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('renders member rows with name, email, and ACTIVE status', () => {
@@ -84,7 +65,9 @@ describe('MembersTable', () => {
});
it('calls onRowClick with the member data when a row is clicked', async () => {
const onRowClick = vi.fn() as MockedFunction<(member: MemberRow) => void>;
const onRowClick = jest.fn() as jest.MockedFunction<
(member: MemberRow) => void
>;
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
@@ -104,7 +87,7 @@ describe('MembersTable', () => {
});
it('renders DELETED badge and calls onRowClick when a deleted member row is clicked', async () => {
const onRowClick = vi.fn();
const onRowClick = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const deletedMember: MemberRow = {
id: 'user-del',

View File

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

View File

@@ -1,8 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// 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;
}
exports[`MessageTip > custom action 1`] = `
<div
class="ant-alert ant-alert-info ant-alert-with-description sc-aXZVg bzzGSj css-dev-only-do-not-override-2i2tap"
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-2i2tap"
data-show="true"
role="alert"
>

View File

@@ -1,14 +1,11 @@
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 = vi.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Helper function to render with VirtuosoMockContext
const renderWithVirtuoso = (
@@ -20,18 +17,10 @@ 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.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(() => Promise.resolve()),
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
},
});
@@ -62,18 +51,12 @@ const mockGroupedOptions = [
describe('CustomMultiSelect - Comprehensive Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnChange: MockedFunction<
NonNullable<CustomMultiSelectProps['onChange']>
>;
let mockOnChange: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnChange = vi.fn();
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
mockOnChange = jest.fn();
jest.clearAllMocks();
});
// ===== 1. CUSTOM VALUES SUPPORT =====
@@ -822,7 +805,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 = vi.fn();
const mockDropdownChange = jest.fn();
renderWithVirtuoso(
<CustomMultiSelect
@@ -849,7 +832,8 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Dropdown should be hidden (not completely removed from DOM)
const dropdown = document.querySelector('.ant-select-dropdown');
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(dropdown).toHaveStyle('pointer-events: none');
});
});
@@ -940,7 +924,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(searchInput).toHaveValue('');
});
});
@@ -1173,7 +1157,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
// The dropdown should be hidden with the hidden class
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});
@@ -1284,7 +1268,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = vi.fn().mockImplementation(
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), 100);
@@ -1507,7 +1491,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expectDropdownToBeClosingOrHidden(dropdown);
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,20 @@
import { describe, expect, it } from 'vitest';
import { render } from 'tests/test-utils';
// 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 NotFound from './index';
describe('Not Found page test', () => {
it('should render Not Found page without errors', () => {
const { asFragment } = render(<NotFound />);
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<NotFound />
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -1,31 +1,125 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
<div
class="sc-gEvEer jnIQEo"
.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"
>
<img
alt="not-found"
src="/src/assets/Images/notFound404.png"
src="test-file-stub"
style="max-height: 480px; max-width: 480px;"
/>
<div
class="sc-fqkvVR dmgRTJ"
class="c1"
>
<p
class="sc-eqUAAy keriGu"
class="c2"
>
Ah, seems like we reached a dead end!
</p>
<p
class="sc-eqUAAy keriGu"
class="c2"
>
Page Not Found
</p>
</div>
<a
class="sc-aXZVg hSWmhs"
class="c3"
href="/home"
tabindex="0"
>

View File

@@ -1,15 +1,7 @@
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.
@@ -49,7 +41,7 @@ function queryTooltipInner(): HTMLElement | null {
describe('OverflowInputToolTip', () => {
beforeEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});
it('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {

View File

@@ -0,0 +1,126 @@
import { ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import { useStore } from 'zustand';
import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from '../utils';
import { QuerySearchV2Context } from './context';
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
import { createExpressionStore } from './QuerySearchV2.store';
export interface QuerySearchV2ProviderProps {
queryParamKey: string;
initialExpression?: string;
/**
* @default false
*/
persistOnUnmount?: boolean;
children: ReactNode;
}
/**
* Provider component that creates a scoped zustand store and exposes
* expression state to children via context.
*/
export function QuerySearchV2Provider({
initialExpression = '',
persistOnUnmount = false,
queryParamKey,
children,
}: QuerySearchV2ProviderProps): JSX.Element {
const storeRef = useRef(createExpressionStore());
const store = storeRef.current;
const [urlExpression, setUrlExpression] = useQueryState(
queryParamKey,
parseAsString,
);
const committedExpression = useStore(store, (s) => s.committedExpression);
const setInputExpression = useStore(store, (s) => s.setInputExpression);
const commitExpression = useStore(store, (s) => s.commitExpression);
const initializeFromUrl = useStore(store, (s) => s.initializeFromUrl);
const resetExpression = useStore(store, (s) => s.resetExpression);
const isInitialized = useRef(false);
useEffect(() => {
if (!isInitialized.current && urlExpression) {
const cleanedExpression = getUserExpressionFromCombined(
initialExpression,
urlExpression,
);
initializeFromUrl(cleanedExpression);
isInitialized.current = true;
}
}, [urlExpression, initialExpression, initializeFromUrl]);
useEffect(() => {
if (isInitialized.current || !urlExpression) {
setUrlExpression(committedExpression || null);
}
}, [committedExpression, setUrlExpression, urlExpression]);
useEffect(() => {
return (): void => {
if (!persistOnUnmount) {
setUrlExpression(null);
resetExpression();
}
};
}, [persistOnUnmount, setUrlExpression, resetExpression]);
const handleChange = useCallback(
(expression: string): void => {
const userOnly = getUserExpressionFromCombined(
initialExpression,
expression,
);
setInputExpression(userOnly);
},
[initialExpression, setInputExpression],
);
const handleRun = useCallback(
(expression: string): void => {
const userOnly = getUserExpressionFromCombined(
initialExpression,
expression,
);
commitExpression(userOnly);
},
[initialExpression, commitExpression],
);
const combinedExpression = useMemo(
() => combineInitialAndUserExpression(initialExpression, committedExpression),
[initialExpression, committedExpression],
);
const contextValue = useMemo<QuerySearchV2ContextValue>(
() => ({
expression: combinedExpression,
userExpression: committedExpression,
initialExpression,
querySearchProps: {
initialExpression: initialExpression.trim() ? initialExpression : undefined,
onChange: handleChange,
onRun: handleRun,
},
}),
[
combinedExpression,
committedExpression,
initialExpression,
handleChange,
handleRun,
],
);
return (
<QuerySearchV2Context.Provider value={contextValue}>
{children}
</QuerySearchV2Context.Provider>
);
}

View File

@@ -0,0 +1,60 @@
import { createStore, StoreApi } from 'zustand';
export type QuerySearchV2Store = {
/**
* User-typed expression (local state, updates on typing)
*/
inputExpression: string;
/**
* Committed expression (synced to URL, updates on submit)
*/
committedExpression: string;
setInputExpression: (expression: string) => void;
commitExpression: (expression: string) => void;
resetExpression: () => void;
initializeFromUrl: (urlExpression: string) => void;
};
export interface QuerySearchProps {
initialExpression: string | undefined;
onChange: (expression: string) => void;
onRun: (expression: string) => void;
}
export interface QuerySearchV2ContextValue {
/**
* Combined expression: "initialExpression AND (userExpression)"
*/
expression: string;
userExpression: string;
initialExpression: string;
querySearchProps: QuerySearchProps;
}
export function createExpressionStore(): StoreApi<QuerySearchV2Store> {
return createStore<QuerySearchV2Store>((set) => ({
inputExpression: '',
committedExpression: '',
setInputExpression: (expression: string): void => {
set({ inputExpression: expression });
},
commitExpression: (expression: string): void => {
set({
inputExpression: expression,
committedExpression: expression,
});
},
resetExpression: (): void => {
set({
inputExpression: '',
committedExpression: '',
});
},
initializeFromUrl: (urlExpression: string): void => {
set({
inputExpression: urlExpression,
committedExpression: urlExpression,
});
},
}));
}

View File

@@ -0,0 +1,95 @@
import { ReactNode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { useQuerySearchV2Context } from '../context';
import {
QuerySearchV2Provider,
QuerySearchV2ProviderProps,
} from '../QuerySearchV2.provider';
const mockSetQueryState = jest.fn();
let mockUrlValue: string | null = null;
jest.mock('nuqs', () => ({
parseAsString: {},
useQueryState: jest.fn(() => [mockUrlValue, mockSetQueryState]),
}));
function createWrapper(
props: Partial<QuerySearchV2ProviderProps> = {},
): ({ children }: { children: ReactNode }) => JSX.Element {
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QuerySearchV2Provider queryParamKey="testExpression" {...props}>
{children}
</QuerySearchV2Provider>
);
};
}
describe('QuerySearchExpressionProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUrlValue = null;
});
it('should provide initial context values', () => {
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper(),
});
expect(result.current.expression).toBe('');
expect(result.current.userExpression).toBe('');
expect(result.current.initialExpression).toBe('');
});
it('should combine initialExpression with userExpression', () => {
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper({ initialExpression: 'k8s.pod.name = "my-pod"' }),
});
expect(result.current.expression).toBe('k8s.pod.name = "my-pod"');
expect(result.current.initialExpression).toBe('k8s.pod.name = "my-pod"');
act(() => {
result.current.querySearchProps.onChange('service = "api"');
});
act(() => {
result.current.querySearchProps.onRun('service = "api"');
});
expect(result.current.expression).toBe(
'k8s.pod.name = "my-pod" AND (service = "api")',
);
expect(result.current.userExpression).toBe('service = "api"');
});
it('should provide querySearchProps with correct callbacks', () => {
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper({ initialExpression: 'initial' }),
});
expect(result.current.querySearchProps.initialExpression).toBe('initial');
expect(typeof result.current.querySearchProps.onChange).toBe('function');
expect(typeof result.current.querySearchProps.onRun).toBe('function');
});
it('should initialize from URL value on mount', () => {
mockUrlValue = 'status = 500';
const { result } = renderHook(() => useQuerySearchV2Context(), {
wrapper: createWrapper(),
});
expect(result.current.userExpression).toBe('status = 500');
expect(result.current.expression).toBe('status = 500');
});
it('should throw error when used outside provider', () => {
expect(() => {
renderHook(() => useQuerySearchV2Context());
}).toThrow(
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
);
});
});

View File

@@ -0,0 +1,61 @@
import { createExpressionStore } from '../QuerySearchV2.store';
describe('createExpressionStore', () => {
it('should create a store with initial state', () => {
const store = createExpressionStore();
const state = store.getState();
expect(state.inputExpression).toBe('');
expect(state.committedExpression).toBe('');
});
it('should update inputExpression via setInputExpression', () => {
const store = createExpressionStore();
store.getState().setInputExpression('service.name = "api"');
expect(store.getState().inputExpression).toBe('service.name = "api"');
expect(store.getState().committedExpression).toBe('');
});
it('should update both expressions via commitExpression', () => {
const store = createExpressionStore();
store.getState().setInputExpression('service.name = "api"');
store.getState().commitExpression('service.name = "api"');
expect(store.getState().inputExpression).toBe('service.name = "api"');
expect(store.getState().committedExpression).toBe('service.name = "api"');
});
it('should reset all state via resetExpression', () => {
const store = createExpressionStore();
store.getState().setInputExpression('service.name = "api"');
store.getState().commitExpression('service.name = "api"');
store.getState().resetExpression();
expect(store.getState().inputExpression).toBe('');
expect(store.getState().committedExpression).toBe('');
});
it('should initialize from URL value', () => {
const store = createExpressionStore();
store.getState().initializeFromUrl('status = 500');
expect(store.getState().inputExpression).toBe('status = 500');
expect(store.getState().committedExpression).toBe('status = 500');
});
it('should create isolated store instances', () => {
const store1 = createExpressionStore();
const store2 = createExpressionStore();
store1.getState().setInputExpression('expr1');
store2.getState().setInputExpression('expr2');
expect(store1.getState().inputExpression).toBe('expr1');
expect(store2.getState().inputExpression).toBe('expr2');
});
});

View File

@@ -0,0 +1,17 @@
// eslint-disable-next-line no-restricted-imports -- React Context required for scoped store pattern
import { createContext, useContext } from 'react';
import type { QuerySearchV2ContextValue } from './QuerySearchV2.store';
export const QuerySearchV2Context =
createContext<QuerySearchV2ContextValue | null>(null);
export function useQuerySearchV2Context(): QuerySearchV2ContextValue {
const context = useContext(QuerySearchV2Context);
if (!context) {
throw new Error(
'useQuerySearchV2Context must be used within a QuerySearchV2Provider',
);
}
return context;
}

View File

@@ -0,0 +1,8 @@
export { useQuerySearchV2Context } from './context';
export type { QuerySearchV2ProviderProps } from './QuerySearchV2.provider';
export { QuerySearchV2Provider } from './QuerySearchV2.provider';
export type {
QuerySearchProps,
QuerySearchV2ContextValue,
QuerySearchV2Store,
} from './QuerySearchV2.store';

View File

@@ -19,6 +19,13 @@
display: flex;
flex-direction: row;
.query-search-initial-scope-label {
position: absolute;
left: 8px;
top: 10px;
z-index: 10;
}
.query-where-clause-editor {
flex: 1;
min-width: 400px;
@@ -53,6 +60,10 @@
}
}
}
&.hasInitialExpression .cm-editor .cm-content {
padding-left: 22px !important;
}
}
.cm-editor {
@@ -68,7 +79,6 @@
border-radius: 2px;
border: 1px solid var(--l1-border);
padding: 0px !important;
background-color: var(--l1-background) !important;
&:focus-within {
border-color: var(--l1-border);

View File

@@ -30,7 +30,7 @@ import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariabl
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
import { Info, TriangleAlert } from 'lucide-react';
import { Filter, Info, TriangleAlert } from 'lucide-react';
import {
IDetailedError,
IQueryContext,
@@ -47,6 +47,7 @@ import { validateQuery } from 'utils/queryValidationUtils';
import { unquote } from 'utils/stringUtils';
import { queryExamples } from './constants';
import { combineInitialAndUserExpression } from './utils';
import './QuerySearch.styles.scss';
@@ -85,6 +86,8 @@ interface QuerySearchProps {
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
showFilterSuggestionsWithoutMetric?: boolean;
/** When set, the editor shows only the user expression; API/filter uses `initial AND (user)`. */
initialExpression?: string;
}
function QuerySearch({
@@ -96,6 +99,7 @@ function QuerySearch({
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
initialExpression,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -112,18 +116,26 @@ function QuerySearch({
const [isFocused, setIsFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
const handleQueryValidation = useCallback((newExpression: string): void => {
try {
const validationResponse = validateQuery(newExpression);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process query',
errors: [error as IDetailedError],
});
}
}, []);
const isScopedFilter = initialExpression !== undefined;
const validateExpressionForEditor = useCallback(
(editorDoc: string): void => {
const toValidate = isScopedFilter
? combineInitialAndUserExpression(initialExpression ?? '', editorDoc)
: editorDoc;
try {
const validationResponse = validateQuery(toValidate);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process query',
errors: [error as IDetailedError],
});
}
},
[initialExpression, isScopedFilter],
);
const getCurrentExpression = useCallback(
(): string => editorRef.current?.state.doc.toString() || '',
@@ -165,6 +177,8 @@ function QuerySearch({
setIsEditorReady(true);
}, []);
const prevQueryDataExpressionRef = useRef<string | undefined>();
useEffect(
() => {
if (!isEditorReady) {
@@ -173,13 +187,22 @@ function QuerySearch({
const newExpression = queryData.filter?.expression || '';
const currentExpression = getCurrentExpression();
const prevExpression = prevQueryDataExpressionRef.current;
// Do not update codemirror editor if the expression is the same
if (newExpression !== currentExpression && !isFocused) {
// Only sync editor when queryData.filter?.expression actually changed from external source
// Not when focus changed (which would reset uncommitted user input)
const queryDataExpressionChanged = prevExpression !== newExpression;
prevQueryDataExpressionRef.current = newExpression;
if (
queryDataExpressionChanged &&
newExpression !== currentExpression &&
!isFocused
) {
updateEditorValue(newExpression, { skipOnChange: true });
if (newExpression) {
handleQueryValidation(newExpression);
}
}
if (!isFocused) {
validateExpressionForEditor(currentExpression);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -284,7 +307,7 @@ function QuerySearch({
}
});
}
setKeySuggestions(Array.from(merged.values()));
setKeySuggestions([...merged.values()]);
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
@@ -337,7 +360,7 @@ function QuerySearch({
// If value contains single quotes, escape them and wrap in single quotes
if (value.includes("'")) {
// Replace single quotes with escaped single quotes
const escapedValue = value.replace(/'/g, "\\'");
const escapedValue = value.replaceAll(/'/g, "\\'");
return `'${escapedValue}'`;
}
@@ -614,7 +637,7 @@ function QuerySearch({
const handleBlur = (): void => {
const currentExpression = getCurrentExpression();
handleQueryValidation(currentExpression);
validateExpressionForEditor(currentExpression);
setIsFocused(false);
};
@@ -632,7 +655,6 @@ function QuerySearch({
);
const handleExampleClick = (exampleQuery: string): void => {
// If there's an existing query, append the example with AND
const currentExpression = getCurrentExpression();
const newExpression = currentExpression
? `${currentExpression} AND ${exampleQuery}`
@@ -897,12 +919,12 @@ function QuerySearch({
// If we have previous pairs, we can prioritize keys that haven't been used yet
if (queryContext.queryPairs && queryContext.queryPairs.length > 0) {
const usedKeys = queryContext.queryPairs.map((pair) => pair.key);
const usedKeys = new Set(queryContext.queryPairs.map((pair) => pair.key));
// Add boost to unused keys to prioritize them
options = options.map((option) => ({
...option,
boost: usedKeys.includes(option.label) ? -10 : 10,
boost: usedKeys.has(option.label) ? -10 : 10,
}));
}
@@ -1317,6 +1339,19 @@ function QuerySearch({
)}
<div className="query-where-clause-editor-container">
{isScopedFilter ? (
<Tooltip title={initialExpression || ''} placement="left">
<div className="query-search-initial-scope-label">
<Filter
size={14}
style={{
opacity: 0.9,
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
}}
/>
</div>
</Tooltip>
) : null}
<Tooltip
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
placement="left"
@@ -1356,6 +1391,7 @@ function QuerySearch({
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,
hasInitialExpression: isScopedFilter,
})}
extensions={[
autocompletion({
@@ -1390,7 +1426,12 @@ function QuerySearch({
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(getCurrentExpression());
const user = getCurrentExpression();
onRun(
isScopedFilter
? combineInitialAndUserExpression(initialExpression ?? '', user)
: user,
);
}
return true;
},
@@ -1555,6 +1596,7 @@ QuerySearch.defaultProps = {
placeholder:
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
showFilterSuggestionsWithoutMetric: false,
initialExpression: undefined,
};
export default QuerySearch;

View File

@@ -0,0 +1,58 @@
import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});

View File

@@ -0,0 +1,40 @@
export function combineInitialAndUserExpression(
initial: string,
user: string,
): string {
const i = initial.trim();
const u = user.trim();
if (!i) {
return u;
}
if (!u) {
return i;
}
return `${i} AND (${u})`;
}
export function getUserExpressionFromCombined(
initial: string,
combined: string | null | undefined,
): string {
const i = initial.trim();
const c = (combined ?? '').trim();
if (!c) {
return '';
}
if (!i) {
return c;
}
if (c === i) {
return '';
}
const wrappedPrefix = `${i} AND (`;
if (c.startsWith(wrappedPrefix) && c.endsWith(')')) {
return c.slice(wrappedPrefix.length, -1);
}
const plainPrefix = `${i} AND `;
if (c.startsWith(plainPrefix)) {
return c.slice(plainPrefix.length);
}
return c;
}

View File

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

View File

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

View File

@@ -1,20 +1,11 @@
import { EditorView } from '@uiw/react-codemirror';
import * as getKeySuggestionsModule from 'api/querySuggestions/getKeySuggestions';
import * as getValueSuggestionsModule from 'api/querySuggestions/getValueSuggestion';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } 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';
@@ -22,6 +13,7 @@ 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,
@@ -34,6 +26,7 @@ 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('');
@@ -41,6 +34,7 @@ beforeAll(() => {
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
@@ -51,6 +45,7 @@ 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;
@@ -59,6 +54,7 @@ beforeAll(() => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
@@ -79,28 +75,25 @@ 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;
});
vi.mock('hooks/useDarkMode', () => ({
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
vi.mock('providers/Dashboard/store/useDashboardStore', () => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
vi.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = vi.fn();
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
return {
__esModule: true,
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
@@ -108,85 +101,92 @@ vi.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={vi.fn() as MockedFunction<(v: string) => void>}
onChange={jest.fn() as jest.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={vi.fn() as MockedFunction<(v: string) => void>}
onChange={jest.fn() as jest.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);
await waitFor(() => expect(getKeySuggestionsSpy).toHaveBeenCalled(), {
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeys).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={vi.fn() as MockedFunction<(v: string) => void>}
onChange={jest.fn() as jest.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,42 +196,51 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
await userEvent.click(editor);
await userEvent.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
await waitFor(() => expect(getValueSuggestionsSpy).toHaveBeenCalled(), {
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetValues).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={vi.fn() as MockedFunction<(v: string) => void>}
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
await waitFor(() => expect(getKeySuggestionsSpy).toHaveBeenCalled(), {
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
timeout: 2000,
});
const lastArgs = getKeySuggestionsSpy.mock.calls[
getKeySuggestionsSpy.mock.calls.length - 1
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.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 = vi.fn() as MockedFunction<(q: string) => void>;
const onRun = jest.fn() as jest.MockedFunction<(q: string) => void>;
render(
<QuerySearch
onChange={vi.fn() as MockedFunction<(v: string) => void>}
onChange={jest.fn() as jest.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();
@@ -241,6 +250,7 @@ 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',
@@ -264,18 +274,21 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
render(
<QuerySearch
onChange={vi.fn() as MockedFunction<(v: string) => void>}
onChange={jest.fn() as jest.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');
@@ -285,11 +298,13 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
});
it('handles queryData.filter.expression changes without triggering onChange', async () => {
const dispatchSpy = vi.spyOn(EditorView.prototype, 'dispatch');
// 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 initialExpression = "service.name = 'frontend'";
const updatedExpression = "service.name = 'backend'";
const onChange = vi.fn() as MockedFunction<(v: string) => void>;
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
const initialQueryData = {
...initialQueriesMap.logs.builder.queryData[0],
@@ -306,6 +321,7 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for CodeMirror to initialize with the initial expression
await waitFor(
() => {
const editorContent = document.querySelector(
@@ -318,6 +334,13 @@ 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: {
@@ -325,6 +348,7 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
},
};
// Re-render with updated queryData.filter.expression
rerender(
<QuerySearch
onChange={onChange}
@@ -333,6 +357,7 @@ 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();
@@ -342,6 +367,11 @@ 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: {
@@ -353,15 +383,18 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
render(
<QuerySearch
onChange={vi.fn()}
onChange={jest.fn()}
queryData={queryData}
dataSource={DataSource.METRICS}
showFilterSuggestionsWithoutMetric
/>,
);
await waitFor(() => expect(getKeySuggestionsSpy).toHaveBeenCalled(), {
timeout: 2000,
});
await waitFor(
() => {
expect(mockedGetKeys).toHaveBeenCalled();
},
{ timeout: 2000 },
);
});
});

View File

@@ -1,7 +1,6 @@
/* 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';
@@ -20,45 +19,47 @@ import {
QueryFunctionsTypes,
} from 'types/common/queryBuilder';
import '@testing-library/jest-dom';
import { QueryBuilderV2 } from '../../QueryBuilderV2';
vi.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: ReturnType<typeof vi.fn> } => ({
safeNavigate: vi.fn(),
}),
}));
// 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>;
},
}));
jest.mock(
'../QueryAggregation/QueryAggregation',
() =>
function QueryAggregation() {
return <div>QueryAggregation</div>;
},
);
jest.mock(
'../MerticsAggregateSection/MetricsAggregateSection',
() =>
function MetricsAggregateSection() {
return <div>MetricsAggregateSection</div>;
},
);
// Mock hooks
vi.mock('hooks/queryBuilder/useQueryBuilder');
vi.mock('hooks/queryBuilder/useQueryBuilderOperations');
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('hooks/queryBuilder/useQueryBuilderOperations');
const mockedUseQueryBuilder = vi.mocked(useQueryBuilder);
const mockedUseQueryOperations = vi.mocked(
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const mockedUseQueryOperations = jest.mocked(
useQueryOperations,
) as MockedFunction<UseQueryOperations>;
) as jest.MockedFunction<UseQueryOperations>;
describe('QueryBuilderV2 + QueryV2 - base render', () => {
let handleRunQueryMock: MockedFunction<() => void>;
let handleQueryFunctionsUpdatesMock: MockedFunction<() => void>;
let handleRunQueryMock: jest.MockedFunction<() => void>;
let handleQueryFunctionsUpdatesMock: jest.MockedFunction<() => void>;
let baseQBContext: QueryBuilderContextType;
beforeEach(() => {
const mockCloneQuery = vi.fn() as MockedFunction<
const mockCloneQuery = jest.fn() as jest.MockedFunction<
(type: string, q: IBuilderQuery) => void
>;
handleRunQueryMock = vi.fn() as MockedFunction<() => void>;
handleQueryFunctionsUpdatesMock = vi.fn() as MockedFunction<() => void>;
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
handleQueryFunctionsUpdatesMock = jest.fn() as jest.MockedFunction<
() => void
>;
const baseQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
@@ -102,35 +103,35 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: vi.fn(),
setLastUsedQuery: jest.fn(),
supersetQuery: currentQueryObj,
setSupersetQuery: vi.fn(),
setSupersetQuery: jest.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
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(),
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(),
cloneQuery: mockCloneQuery,
addNewQueryItem: vi.fn(),
redirectWithQueryBuilderData: vi.fn(),
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: handleRunQueryMock,
resetQuery: vi.fn(),
handleOnUnitsChange: vi.fn(),
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData: vi.fn(),
isStagedQueryUpdated: vi.fn(() => false),
isDefaultQuery: vi.fn(() => false),
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
} as unknown as QueryBuilderContextType;
baseQBContext = baseContext;
@@ -142,21 +143,21 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
operators: [],
spaceAggregationOptions: [],
listOfAdditionalFilters: [],
handleChangeOperator: vi.fn(),
handleSpaceAggregationChange: vi.fn(),
handleChangeAggregatorAttribute: vi.fn(),
handleChangeDataSource: vi.fn(),
handleDeleteQuery: vi.fn(),
handleChangeOperator: jest.fn(),
handleSpaceAggregationChange: jest.fn(),
handleChangeAggregatorAttribute: jest.fn(),
handleChangeDataSource: jest.fn(),
handleDeleteQuery: jest.fn(),
handleChangeQueryData:
vi.fn() as unknown as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: vi.fn(),
jest.fn() as unknown as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: jest.fn(),
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
listOfAdditionalFormulaFilters: [],
});
});
afterEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('renders limit input when dataSource is logs', () => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
export type {
QuerySearchProps,
QuerySearchV2ContextValue,
QuerySearchV2ProviderProps,
} from './QueryV2/QuerySearch/Provider';
export {
QuerySearchV2Provider,
useQuerySearchV2Context,
} from './QueryV2/QuerySearch/Provider';
export { QueryBuilderV2 } from './QueryBuilderV2';
export {
QueryBuilderV2Provider,
useQueryBuilderV2Context,
} from './QueryBuilderV2Context';

View File

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

View File

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

View File

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

View File

@@ -1,134 +0,0 @@
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,6 +1,5 @@
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';
@@ -76,7 +75,7 @@ describe('RouteTab component', () => {
});
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = vi.fn();
const onChangeHandler = jest.fn();
const history = createMemoryHistory();
render(
<Router history={history}>

View File

@@ -1,7 +1,6 @@
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
render,
screen,
@@ -12,28 +11,22 @@ import {
import AddKeyModal from '../AddKeyModal';
const { mockCopyToClipboard, mockCopyState } = vi.hoisted(() => {
const copyFn = vi.fn();
const copyState = { value: undefined, error: undefined };
return {
mockCopyToClipboard: copyFn,
mockCopyState: copyState,
};
});
vi.mock('@signozhq/ui', async () => ({
...(await vi.importActual<typeof import('@signozhq/ui')>('@signozhq/ui')),
toast: { success: vi.fn(), error: vi.fn() },
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));
vi.mock('react-use', () => ({
const mockCopyToClipboard = jest.fn();
const mockCopyState = { value: undefined, error: undefined };
jest.mock('react-use', () => ({
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
mockCopyState,
mockCopyToClipboard,
],
}));
const mockToast = vi.mocked(toast);
const mockToast = jest.mocked(toast);
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys';
@@ -60,7 +53,7 @@ function renderModal(): ReturnType<typeof render> {
describe('AddKeyModal', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockCopyToClipboard.mockClear();
server.use(
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>

View File

@@ -2,25 +2,16 @@ import { toast } from '@signozhq/ui';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import EditKeyModal from '../EditKeyModal';
vi.mock('@signozhq/ui', async () => ({
...(await vi.importActual<typeof import('@signozhq/ui')>('@signozhq/ui')),
toast: { success: vi.fn(), error: vi.fn() },
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));
const mockToast = vi.mocked(toast);
const mockToast = jest.mocked(toast);
const SA_KEY_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys/key-1';
@@ -38,7 +29,7 @@ function renderModal(
account: 'sa-1',
'edit-key': 'key-1',
},
onUrlUpdate?: Mock,
onUrlUpdate?: jest.Mock,
): ReturnType<typeof render> {
return render(
<NuqsTestingAdapter
@@ -53,7 +44,7 @@ function renderModal(
describe('EditKeyModal (URL-controlled)', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
server.use(
rest.put(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
@@ -108,7 +99,7 @@ describe('EditKeyModal (URL-controlled)', () => {
it('cancel clears edit-key param and closes modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onUrlUpdate = vi.fn();
const onUrlUpdate = jest.fn();
renderModal(mockKey, undefined, onUrlUpdate);
await screen.findByDisplayValue('Original Key Name');

View File

@@ -2,40 +2,16 @@ import { toast } from '@signozhq/ui';
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import KeysTab from '../KeysTab';
vi.mock('@signozhq/ui', async () => ({
...(await vi.importActual('@signozhq/ui')),
toast: { success: vi.fn(), error: vi.fn() },
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));
const mockToast = vi.mocked(toast);
const TZ_ORIG = process.env.TZ;
beforeAll(() => {
process.env.TZ = 'UTC';
});
afterAll(() => {
if (TZ_ORIG === undefined) {
delete process.env.TZ;
} else {
process.env.TZ = TZ_ORIG;
}
});
const mockToast = jest.mocked(toast);
const SA_KEY_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys/:fid';
@@ -77,7 +53,7 @@ function renderKeysTab(
describe('KeysTab', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
server.use(
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
@@ -96,7 +72,7 @@ describe('KeysTab', () => {
it('renders empty state when no keys and clicking add sets add-key param', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onUrlUpdate = vi.fn();
const onUrlUpdate = jest.fn();
render(
<NuqsTestingAdapter
searchParams={{ account: 'sa-1' }}
@@ -129,7 +105,7 @@ describe('KeysTab', () => {
it('clicking a row sets the edit-key URL param', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onUrlUpdate = vi.fn();
const onUrlUpdate = jest.fn();
render(
<NuqsTestingAdapter onUrlUpdate={onUrlUpdate}>
@@ -153,7 +129,7 @@ describe('KeysTab', () => {
it('clicking revoke icon sets revoke-key URL param', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onUrlUpdate = vi.fn();
const onUrlUpdate = jest.fn();
render(
<NuqsTestingAdapter onUrlUpdate={onUrlUpdate}>

View File

@@ -2,13 +2,12 @@ import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
vi.mock('@signozhq/ui', async () => ({
...(await vi.importActual('@signozhq/ui')),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
footer,
@@ -24,7 +23,7 @@ vi.mock('@signozhq/ui', async () => ({
{footer}
</div>
) : null,
toast: { success: vi.fn(), error: vi.fn() },
toast: { success: jest.fn(), error: jest.fn() },
}));
const ROLES_ENDPOINT = '*/api/v1/roles';
@@ -55,14 +54,14 @@ function renderDrawer(
): ReturnType<typeof render> {
return render(
<NuqsTestingAdapter searchParams={searchParams} hasMemory>
<ServiceAccountDrawer onSuccess={vi.fn()} />
<ServiceAccountDrawer onSuccess={jest.fn()} />
</NuqsTestingAdapter>,
);
}
describe('ServiceAccountDrawer', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
@@ -113,8 +112,8 @@ describe('ServiceAccountDrawer', () => {
});
it('editing name enables Save; clicking Save sends correct payload and calls onSuccess', async () => {
const onSuccess = vi.fn();
const updateSpy = vi.fn();
const onSuccess = jest.fn();
const updateSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
@@ -149,8 +148,8 @@ describe('ServiceAccountDrawer', () => {
});
it('changing roles enables Save; clicking Save sends role add request without delete', async () => {
const roleSpy = vi.fn();
const deleteSpy = vi.fn();
const roleSpy = jest.fn();
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
@@ -186,7 +185,7 @@ describe('ServiceAccountDrawer', () => {
});
it('"Delete Service Account" opens confirm dialog; confirming sends delete request', async () => {
const deleteSpy = vi.fn();
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
@@ -285,7 +284,7 @@ describe('ServiceAccountDrawer', () => {
describe('ServiceAccountDrawer save-error UX', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),

View File

@@ -1,7 +1,5 @@
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import type { MockedFunction } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent } from 'tests/test-utils';
import ServiceAccountsTable from '../ServiceAccountsTable';
@@ -26,12 +24,12 @@ const mockDisabledAccount: ServiceAccountRow = {
const defaultProps = {
loading: false,
onRowClick: vi.fn(),
onRowClick: jest.fn(),
};
describe('ServiceAccountsTable', () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});
it('renders name, email, role badge, and ACTIVE status badge', () => {
@@ -51,7 +49,7 @@ describe('ServiceAccountsTable', () => {
});
it('calls onRowClick with the correct account when a row is clicked', async () => {
const onRowClick = vi.fn() as MockedFunction<
const onRowClick = jest.fn() as jest.MockedFunction<
(row: ServiceAccountRow) => void
>;
const user = userEvent.setup({ pointerEventsCheck: 0 });

View File

@@ -1,10 +1,11 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import '@testing-library/jest-dom';
import type { CmdAction } from '../ShiftOverlay';
import { ShiftOverlay } from '../ShiftOverlay';
vi.mock('../formatShortcut', () => ({
jest.mock('../formatShortcut', () => ({
formatShortcut: (shortcut: string[]): string => shortcut.join('+'),
}));
@@ -14,7 +15,7 @@ const baseActions: CmdAction[] = [
name: 'Go to Traces',
section: 'navigation',
shortcut: ['Shift', 'T'],
perform: vi.fn(),
perform: jest.fn(),
},
{
id: '2',
@@ -22,20 +23,20 @@ const baseActions: CmdAction[] = [
section: 'navigation',
shortcut: ['Shift', 'M'],
roles: ['ADMIN'], // ✅ now UserRole[]
perform: vi.fn(),
perform: jest.fn(),
},
{
id: '3',
name: 'Create Alert',
section: 'actions',
shortcut: ['A'],
perform: vi.fn(),
perform: jest.fn(),
},
{
id: '4',
name: 'Go to Logs',
section: 'navigation',
perform: vi.fn(),
perform: jest.fn(),
},
];
@@ -57,7 +58,7 @@ describe('ShiftOverlay', () => {
id: 'x',
name: 'Create Alert',
section: 'actions',
perform: vi.fn(),
perform: jest.fn(),
},
]}
userRole="ADMIN"

View File

@@ -1,23 +1,8 @@
import { act, renderHook } from '@testing-library/react';
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from 'vitest';
import { useShiftHoldOverlay } from '../useShiftHoldOverlay';
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
jest.useFakeTimers();
function pressShift(target: EventTarget = window): void {
const event = new KeyboardEvent('keydown', {
@@ -39,7 +24,7 @@ function releaseShift(): void {
describe('useShiftHoldOverlay', () => {
afterEach(() => {
vi.clearAllTimers();
jest.clearAllTimers();
});
it('shows overlay after holding Shift for 600ms', () => {
@@ -47,7 +32,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(true);
@@ -58,9 +43,9 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(300);
jest.advanceTimersByTime(300);
releaseShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(false);
@@ -71,7 +56,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(true);
@@ -90,7 +75,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(false);
@@ -104,7 +89,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift(input);
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(false);
@@ -117,7 +102,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(true);
@@ -134,7 +119,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(true);
@@ -151,7 +136,7 @@ describe('useShiftHoldOverlay', () => {
act(() => {
pressShift();
vi.advanceTimersByTime(600);
jest.advanceTimersByTime(600);
});
expect(result.current).toBe(false);

View File

@@ -1,12 +1,11 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { TimezoneContextType } from 'providers/Timezone';
import { Span } from 'types/api/trace/getTraceV2';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import SpanHoverCard from '../SpanHoverCard';
// Mock timezone provider so SpanHoverCard can use useTimezone without a real context
vi.mock('providers/Timezone', () => ({
jest.mock('providers/Timezone', () => ({
__esModule: true,
useTimezone: (): TimezoneContextType => ({
timezone: {
@@ -21,18 +20,18 @@ vi.mock('providers/Timezone', () => ({
offset: 'UTC',
searchIndex: 'UTC',
},
updateTimezone: vi.fn(),
formatTimezoneAdjustedTimestamp: vi.fn(() => 'mock-date'),
updateTimezone: jest.fn(),
formatTimezoneAdjustedTimestamp: jest.fn(() => 'mock-date'),
isAdaptationEnabled: true,
setIsAdaptationEnabled: vi.fn(),
setIsAdaptationEnabled: jest.fn(),
}),
}));
// Mock dayjs for testing, including timezone helpers used in timezoneUtils
vi.mock('dayjs', () => {
jest.mock('dayjs', () => {
const mockDayjsInstance: any = {};
mockDayjsInstance.format = vi.fn((formatString: string) =>
mockDayjsInstance.format = jest.fn((formatString: string) =>
// Match the DD_MMM_YYYY_HH_MM_SS format: 'DD MMM YYYY, HH:mm:ss'
formatString === 'DD MMM YYYY, HH:mm:ss'
? '15 Mar 2024, 14:23:45'
@@ -40,21 +39,18 @@ vi.mock('dayjs', () => {
);
// Support chaining: dayjs().tz(timezone).format(...) and dayjs().tz(timezone).utcOffset()
mockDayjsInstance.tz = vi.fn(() => mockDayjsInstance);
mockDayjsInstance.utcOffset = vi.fn(() => 0);
mockDayjsInstance.tz = jest.fn(() => mockDayjsInstance);
mockDayjsInstance.utcOffset = jest.fn(() => 0);
const mockDayjs = vi.fn(() => mockDayjsInstance);
const mockDayjs = jest.fn(() => mockDayjsInstance);
Object.assign(mockDayjs, {
extend: vi.fn(),
extend: jest.fn(),
// Support dayjs.tz.guess()
tz: { guess: vi.fn(() => 'UTC') },
tz: { guess: jest.fn(() => 'UTC') },
});
return {
__esModule: true,
default: mockDayjs,
};
return mockDayjs;
});
const HOVER_ELEMENT_ID = 'hover-element';
@@ -103,11 +99,11 @@ const mockTraceMetadata = {
describe('SpanHoverCard', () => {
beforeEach(() => {
vi.useFakeTimers();
jest.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
jest.useRealTimers();
});
it('renders child element correctly', () => {
@@ -138,7 +134,7 @@ describe('SpanHoverCard', () => {
// Advance time by 0.5 seconds
act(() => {
vi.advanceTimersByTime(200);
jest.advanceTimersByTime(200);
});
// Now popover should appear
@@ -157,13 +153,13 @@ describe('SpanHoverCard', () => {
// Quick hover and unhover (less than the 0.2s delay)
fireEvent.mouseEnter(hoverElement);
act(() => {
vi.advanceTimersByTime(100); // Only 0.1 seconds
jest.advanceTimersByTime(100); // Only 0.1 seconds
});
fireEvent.mouseLeave(hoverElement);
// Advance past the full delay
act(() => {
vi.advanceTimersByTime(400);
jest.advanceTimersByTime(400);
});
// Popover should not appear
@@ -182,7 +178,7 @@ describe('SpanHoverCard', () => {
// Hover and wait for popover
fireEvent.mouseEnter(hoverElement);
act(() => {
vi.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
});
// Check that popover shows span operation name in title
@@ -212,7 +208,7 @@ describe('SpanHoverCard', () => {
// Hover and wait for popover
fireEvent.mouseEnter(hoverElement);
act(() => {
vi.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
});
// Verify the DD MMM YYYY, HH:mm:ss format is displayed
@@ -236,7 +232,7 @@ describe('SpanHoverCard', () => {
// Hover and wait for popover
fireEvent.mouseEnter(hoverElement);
act(() => {
vi.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
});
// Check relative time display
@@ -260,7 +256,7 @@ describe('SpanHoverCard', () => {
// Hover and wait for popover
fireEvent.mouseEnter(hoverElement);
act(() => {
vi.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
});
expect(screen.getByText('Events:')).toBeInTheDocument();
@@ -288,7 +284,7 @@ describe('SpanHoverCard', () => {
// Should appear after delay
act(() => {
vi.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
});
expect(screen.getByText('Duration:')).toBeInTheDocument();
});

View File

@@ -47,10 +47,16 @@ function TanStackCustomTableRow<TData>({
const isActive = context?.isRowActive?.(rowData) ?? false;
const extraClass = context?.getRowClassName?.(rowData) ?? '';
const rowStyle = context?.getRowStyle?.(rowData);
const enableAlternatingRowColors =
context?.enableAlternatingRowColors ?? false;
const rowClassName = cx(
tableStyles.tableRow,
isActive && tableStyles.tableRowActive,
enableAlternatingRowColors &&
(item.row.index % 2 === 0
? tableStyles.tableRowEven
: tableStyles.tableRowOdd),
extraClass,
);
@@ -105,6 +111,12 @@ function areTableRowPropsEqual<TData>(
if (prev.context?.columnVisibilityKey !== next.context?.columnVisibilityKey) {
return false;
}
if (
prev.context?.enableAlternatingRowColors !==
next.context?.enableAlternatingRowColors
) {
return false;
}
if (prev.context !== next.context) {
const prevActive = prev.context?.isRowActive?.(prevData) ?? false;

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