Compare commits

...

4 Commits

Author SHA1 Message Date
Nityananda Gohain
dbacd34ec2 Merge branch 'main' into hotfix/spanmapper 2026-07-13 11:45:02 +05:30
nityanandagohain
0cbf3e8ee2 fix: change group_id to groupId in response 2026-07-13 11:43:45 +05:30
Vinicius Lourenço
d1a06a91bf test(service-account): mock motion lib & change drawer open/close detection (#12077)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* test(service-account): mock motion lib & change drawer open/close detection

* test(serviceaccount): remove getByRole & use wait for instead of waitForElementToBeRemoved
2026-07-13 06:03:15 +00:00
nityanandagohain
1f057f041b fix: set correct opapi response model for span mapper list 2026-07-13 11:23:33 +05:30
15 changed files with 212 additions and 124 deletions

View File

@@ -7998,6 +7998,15 @@ components:
required:
- items
type: object
SpantypesGettableSpanMappers:
properties:
items:
items:
$ref: '#/components/schemas/SpantypesSpanMapper'
type: array
required:
- items
type: object
SpantypesGettableTraceAggregations:
properties:
aggregations:
@@ -8150,7 +8159,7 @@ components:
type: boolean
fieldContext:
$ref: '#/components/schemas/SpantypesFieldContext'
group_id:
groupId:
type: string
id:
type: string
@@ -8163,7 +8172,7 @@ components:
type: string
required:
- id
- group_id
- groupId
- name
- fieldContext
- config
@@ -13742,7 +13751,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
status:
type: string
required:

View File

@@ -0,0 +1,88 @@
// oxlint-disable-next-line no-restricted-imports
import * as React from 'react';
// In jsdom, AnimatePresence from motion/react keeps children in DOM during exit
// animations (awaiting rAF-driven completion that never fully runs in jsdom).
// This mock makes AnimatePresence render children immediately and makes motion.*
// elements render as their plain HTML equivalents without animation side-effects.
//
// IMPORTANT: motion component references are cached so React sees a stable
// component identity across re-renders and does not enter an infinite remount loop.
const MOTION_PROPS_TO_STRIP = new Set([
'initial',
'animate',
'exit',
'variants',
'transition',
'whileHover',
'whileTap',
'whileFocus',
'whileInView',
'layout',
'layoutId',
'onAnimationStart',
'onAnimationComplete',
]);
const cache = new Map<string, React.ComponentType>();
function getMotionComponent(tag: string): React.ComponentType {
if (!cache.has(tag)) {
const Component = React.forwardRef<HTMLElement, Record<string, unknown>>(
(props, ref) => {
const domProps: Record<string, unknown> = {};
for (const [k, v] of Object.entries(props)) {
if (!MOTION_PROPS_TO_STRIP.has(k)) {
domProps[k] = v;
}
}
return React.createElement(tag, { ...domProps, ref });
},
);
Component.displayName = `motion.${tag}`;
cache.set(tag, Component as unknown as React.ComponentType);
}
return cache.get(tag) as React.ComponentType;
}
const motionHandler: ProxyHandler<Record<string, React.ComponentType>> = {
get(_target, prop: string) {
return getMotionComponent(prop);
},
};
export const AnimatePresence: React.FC<{
children?: React.ReactNode;
mode?: string;
}> = ({ children }) => React.createElement(React.Fragment, null, children);
export const motion = new Proxy(
{} as Record<string, React.ComponentType>,
motionHandler,
);
export const useAnimation = (): Record<string, unknown> => ({
start: (): unknown => Promise.resolve(),
stop: (): unknown => undefined,
set: (): unknown => undefined,
});
export const useMotionValue = (
initial: unknown,
): { get: () => unknown; set: () => void } => ({
get: (): unknown => initial,
set: (): unknown => undefined,
});
export const useTransform = (): { get: () => number } => ({
get: (): number => 0,
});
export const useSpring = (v: unknown): unknown => v;
export const useScroll = (): { scrollY: { get: () => number } } => ({
scrollY: { get: (): number => 0 },
});
export default { motion, AnimatePresence };

View File

@@ -20,6 +20,7 @@ const config: Config.InitialOptions = {
'\\.module\\.mjs$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^motion/react$': '<rootDir>/__mocks__/motionMock.tsx',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,

View File

@@ -9194,6 +9194,76 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
groupId: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesGettableSpanMappersDTO {
/**
* @type array
*/
items: SpantypesSpanMapperDTO[];
}
export enum SpantypesSpanAggregationTypeDTO {
span_count = 'span_count',
execution_time_percentage = 'execution_time_percentage',
@@ -9440,30 +9510,6 @@ export interface SpantypesPostableFlamegraphDTO {
selectedSpanId?: string;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesPostableSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
@@ -9512,45 +9558,6 @@ export interface SpantypesPostableWaterfallDTO {
uncollapsedSpans?: string[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
group_id: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesUpdatableSpanMapperDTO {
config?: SpantypesSpanMapperConfigDTO;
/**
@@ -10852,7 +10859,7 @@ export type ListSpanMappersPathParameters = {
groupId: string;
};
export type ListSpanMappers200 = {
data: SpantypesGettableSpanMapperGroupsDTO;
data: SpantypesGettableSpanMappersDTO;
/**
* @type string
*/

View File

@@ -92,6 +92,7 @@ function CreateServiceAccountModal(): JSX.Element {
width="narrow"
className="create-sa-modal"
disableOutsideClick={isErrorModalVisible}
testId="create-service-account-modal"
>
<div className="create-sa-modal__content">
<form

View File

@@ -1,13 +1,7 @@
import { toast } from '@signozhq/ui/sonner';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
@@ -89,7 +83,7 @@ describe('CreateServiceAccountModal', () => {
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
screen.queryByTestId('create-service-account-modal'),
).not.toBeInTheDocument();
});
});
@@ -129,7 +123,7 @@ describe('CreateServiceAccountModal', () => {
});
expect(
screen.getByRole('dialog', { name: /New Service Account/i }),
screen.getByTestId('create-service-account-modal'),
).toBeInTheDocument();
});
@@ -137,15 +131,14 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const dialog = await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await screen.findByTestId('create-service-account-modal');
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitForElementToBeRemoved(dialog);
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
await waitFor(() => {
expect(
screen.queryByTestId('create-service-account-modal'),
).not.toBeInTheDocument();
});
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -151,6 +151,7 @@ function AddKeyModal(): JSX.Element {
className="add-key-modal"
showCloseButton
disableOutsideClick={isErrorModalVisible}
testId="add-key-modal"
>
{phase === Phase.FORM && (
<KeyFormPhase

View File

@@ -91,6 +91,7 @@ function DeleteAccountModal(): JSX.Element {
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
data-testid="confirm-delete-btn"
>
<Trash2 size={12} />
Delete
@@ -111,6 +112,7 @@ function DeleteAccountModal(): JSX.Element {
className="alert-dialog sa-delete-dialog"
showCloseButton={false}
disableOutsideClick={isErrorModalVisible}
testId="delete-service-account-modal"
footer={footer}
>
{content}

View File

@@ -175,6 +175,7 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
}
showCloseButton={!isRevokeConfirmOpen}
disableOutsideClick={isErrorModalVisible}
testId="edit-key-modal"
footer={
isRevokeConfirmOpen ? (
<RevokeKeyFooter

View File

@@ -2,13 +2,7 @@ import { toast } from '@signozhq/ui/sonner';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import AddKeyModal from '../AddKeyModal';
@@ -97,7 +91,7 @@ describe('AddKeyModal', () => {
await screen.findByText('snz_abc123xyz456secret');
expect(screen.getByText(/Store the key securely/i)).toBeInTheDocument();
await screen.findByRole('dialog', { name: /Key Created Successfully/i });
expect(screen.getByTestId('add-key-modal')).toBeInTheDocument();
});
it('copy button writes key to clipboard and shows toast.success', async () => {
@@ -133,9 +127,11 @@ describe('AddKeyModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const dialog = await screen.findByRole('dialog', { name: /Add a New Key/i });
await screen.findByTestId('add-key-modal');
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitForElementToBeRemoved(dialog);
await waitFor(() => {
expect(screen.queryByTestId('add-key-modal')).not.toBeInTheDocument();
});
});
});

View File

@@ -73,9 +73,7 @@ describe('EditKeyModal (URL-controlled)', () => {
it('renders nothing when edit-key param is absent', () => {
renderModal(null, { account: 'sa-1' });
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
});
it('renders key data from prop when edit-key param is set', async () => {
@@ -102,9 +100,7 @@ describe('EditKeyModal (URL-controlled)', () => {
});
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
});
});
@@ -131,9 +127,7 @@ describe('EditKeyModal (URL-controlled)', () => {
expect(latestUrlUpdate.queryString).not.toContain('edit-key=');
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
});
});
@@ -145,9 +139,7 @@ describe('EditKeyModal (URL-controlled)', () => {
await user.click(screen.getByRole('button', { name: /Revoke Key/i }));
// Same dialog, now showing revoke confirmation
await expect(
screen.findByRole('dialog', { name: /Revoke Original Key Name/i }),
).resolves.toBeInTheDocument();
expect(screen.getByTestId('edit-key-modal')).toBeInTheDocument();
expect(
screen.getByText(/Revoking this key will permanently invalidate it/i),
).toBeInTheDocument();
@@ -170,9 +162,7 @@ describe('EditKeyModal (URL-controlled)', () => {
});
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
});
});
});

View File

@@ -222,21 +222,20 @@ describe('ServiceAccountDrawer', () => {
screen.getByRole('button', { name: /Delete Service Account/i }),
);
const dialog = await screen.findByRole('dialog', {
name: /Delete service account CI Bot/i,
});
expect(dialog).toBeInTheDocument();
await screen.findByTestId('delete-service-account-modal');
expect(
screen.getByTestId('delete-service-account-modal'),
).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /^Delete$/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await user.click(screen.getByTestId('confirm-delete-btn'));
await waitFor(() => {
expect(deleteSpy).toHaveBeenCalled();
});
await waitFor(() => {
expect(screen.queryByDisplayValue('CI Bot')).not.toBeInTheDocument();
});
await waitFor(
() => {
expect(deleteSpy).toHaveBeenCalled();
expect(screen.queryByDisplayValue('CI Bot')).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('deleted account shows read-only name, no Save button, no Delete button', async () => {

View File

@@ -208,7 +208,7 @@ describe('ServiceAccountsSettings (integration)', () => {
fireEvent.click(screen.getByRole('button', { name: /New Service Account/i }));
await screen.findByRole('dialog', { name: /New Service Account/i });
await screen.findByTestId('create-service-account-modal');
expect(screen.getByPlaceholderText('Enter a name')).toBeInTheDocument();
});

View File

@@ -98,7 +98,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Description: "Returns all mappers belonging to a mapping group.",
Request: nil,
RequestContentType: "",
Response: new(spantypes.GettableSpanMapperGroups),
Response: new(spantypes.GettableSpanMappers),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},

View File

@@ -54,7 +54,7 @@ type SpanMapper struct {
types.UserAuditable
ID valuer.UUID `json:"id" required:"true"`
GroupID valuer.UUID `json:"group_id" required:"true"`
GroupID valuer.UUID `json:"groupId" required:"true"`
Name string `json:"name" required:"true"`
FieldContext FieldContext `json:"fieldContext" required:"true"`
Config SpanMapperConfig `json:"config" required:"true"`