Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FC } from 'react';

import { DialGhostButton, DialNeutralButton, DialPrimaryButton, Step, StepStatus } from '@epam/ai-dial-ui-kit';
import { IconArrowNarrowLeft, IconArrowNarrowRight } from '@tabler/icons-react';
import classNames from 'classnames';

import { ButtonsI18nKey } from '@/src/constants/i18n';
import { BASE_BUTTON_ICON_PROPS } from '@/src/constants/main-layout';
Expand Down Expand Up @@ -31,7 +32,12 @@ const StepperModalButtons: FC<Props> = ({ steps, currentStep, onChangeStep, onFi
};

return (
<div className="flex flex-row items-center justify-between gap-2 px-6 py-4">
<div
className={classNames(
'flex flex-row items-center gap-2 px-6 py-4',
currentStep?.id !== steps[0]?.id ? 'justify-between' : 'justify-end',
)}
>
{currentStep?.id !== steps[0]?.id && (
<DialGhostButton
label={t(ButtonsI18nKey.Back)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ const MethodInfo: FC<Props> = ({ testSuite, onChangeTestSuite, selectedAppType }
}, [onChangeTestSuite, currentSuite]);

const disableConfirm = useMemo(() => {
return !currentSuite.endpointRef?.method || !currentSuite.endpointRef?.relativeUrl;
}, [currentSuite.endpointRef?.method, currentSuite.endpointRef?.relativeUrl]);
return !currentSuite.endpointRef?.method || !currentSuite.endpointRef?.relativeUrlPattern;
}, [currentSuite.endpointRef?.method, currentSuite.endpointRef?.relativeUrlPattern]);

return (testSuite?.endpointRef && !!Object.keys(testSuite?.endpointRef).length) || testSuite ? (
<div className="flex flex-col gap-4 p-4 h-full w-full relative">
Expand All @@ -89,7 +89,7 @@ const MethodInfo: FC<Props> = ({ testSuite, onChangeTestSuite, selectedAppType }
{testSuite?.endpointRef.method}
</span>
)}
<span className="truncate text-primary ml-1">{testSuite?.endpointRef?.relativeUrl}</span>
<span className="truncate text-primary ml-1">{testSuite?.endpointRef?.relativeUrlPattern}</span>
</div>
<div className="flex flex-row gap-4 items-center">
<ViewSelector view={view} changeView={setView} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const MethodItem: FC<Props> = ({ index, item, isActive, onClick }) => {
<span className="tiny bg-layer-3 rounded p-1 border border-primary whitespace-nowrap max-w-[200px] overflow-hidden">
{item.method}
</span>
<span className="truncate text-primary dial-small ml-1">{item.relativeUrl}</span>
<span className="truncate text-primary dial-small ml-1">{item.relativeUrlPattern}</span>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const Methods: FC<Props> = ({ testSuite, selectedApplication, onChange, isCreate
...prev,
endpointRef: {
method: methods[index].method,
relativeUrl: methods[index].relativeUrl,
relativeUrlPattern: methods[index].relativeUrlPattern,
},
}));
}
Expand All @@ -64,12 +64,14 @@ const Methods: FC<Props> = ({ testSuite, selectedApplication, onChange, isCreate
const methods = generateMethodPathCombinations(data?.routes);
setMethods(methods);
const index = methods.findIndex(
(m) => m.method === testSuite.endpointRef?.method && m.relativeUrl === testSuite.endpointRef?.relativeUrl,
(m) =>
m.method === testSuite.endpointRef?.method &&
m.relativeUrlPattern === testSuite.endpointRef?.relativeUrlPattern,
);
setActiveMethodIndex(index === -1 ? 0 : index);
});
}
}, [fullApplication, selectedApplication, testSuite.endpointRef?.method, testSuite.endpointRef?.relativeUrl]);
}, [fullApplication, selectedApplication, testSuite.endpointRef?.method, testSuite.endpointRef?.relativeUrlPattern]);

return (
<div className="w-full flex flex-row h-full gap-2">
Expand All @@ -86,7 +88,7 @@ const Methods: FC<Props> = ({ testSuite, selectedApplication, onChange, isCreate
{!!methods.length && <span className="dial-tiny text-secondary block">{t(TestSuitesI18nKey.Other)}</span>}
{methods.map((method, index) => (
<MethodItem
key={(method?.relativeUrl || '') + method.method}
key={(method?.relativeUrlPattern || '') + method.method}
item={method}
index={index + 1}
isActive={activeMethodIndex === index + 1}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,92 +1,152 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
import { describe, test, expect, vi, beforeEach } from 'vitest';
import Methods from '../Methods';
import { CHAT_COMPLETION_METHOD } from '../../constants/chat-completion-method';

const mockGetDeployment = vi.fn();
const mockGenerateMethodPathCombinations = vi.fn();

vi.mock('@/src/locales/client', () => ({
useI18n: () => (k: string) => k,
}));

vi.mock('@/src/components/TestSuites/utils/method', () => ({
generateMethodPathCombinations: vi.fn(() => [
{ method: 'GET', relativeUrl: '/api' },
{ method: 'POST', relativeUrl: '/data' },
]),
generateMethodPathCombinations: (...args: any[]) => mockGenerateMethodPathCombinations(...args),
}));

vi.mock('@/src/app/[lang]/test-suites/actions', () => ({
getDeployment: vi.fn(() => Promise.resolve({ deploymentId: 'd', $type: 't' })),
getDeployment: (...args: any[]) => mockGetDeployment(...args),
}));

vi.mock('@epam/ai-dial-ui-kit', () => ({
DialCollapsibleSidebar: ({ children }: any) => <div data-testid="sidebar">{children}</div>,
DialCollapsibleSidebar: ({ children }: any) => <div role="complementary">{children}</div>,
}));

vi.mock('../MethodItem', () => ({
__esModule: true,
default: ({ item, index, onClick }: any) => (
<button data-testid={`method-${index}`} onClick={() => onClick(index)}>
{item?.method ?? `CHAT-${index}`}
</button>
default: ({ item, index, onClick, isActive }: any) => (
<div className={isActive ? 'active-method' : 'inactive-method'} data-index={index}>
<button onClick={() => onClick(index)}>
{item?.method} {item?.relativeUrlPattern || ''}
</button>
</div>
),
}));

vi.mock('../MethodInfo', () => ({
__esModule: true,
default: ({ testSuite }: any) => <div data-testid="method-info">{JSON.stringify(testSuite?.endpointRef)}</div>,
default: ({ testSuite }: any) => (
<div role="region" aria-label="method-info">
{testSuite?.endpointRef?.method}
</div>
),
}));

describe('Methods component', () => {
const onChange = vi.fn();
const baseTestSuite: any = { endpointRef: {} };
const selectedApplication: any = { deploymentId: 'd', $type: 't', routes: { r1: {} } };
const mockDeployment = {
deploymentId: 'test-deployment',
$type: 'application',
routes: {
'route-1': { path: '/api/users', methods: ['GET', 'POST'] },
'route-2': { path: '/api/data', methods: ['GET'] },
},
};

const mockMethods = [
{ method: 'GET', relativeUrlPattern: '/api/users' },
{ method: 'POST', relativeUrlPattern: '/api/users' },
{ method: 'GET', relativeUrlPattern: '/api/data' },
];

beforeEach(() => {
onChange.mockClear();
mockGetDeployment.mockClear();
mockGenerateMethodPathCombinations.mockClear();
mockGetDeployment.mockResolvedValue(mockDeployment);
mockGenerateMethodPathCombinations.mockReturnValue(mockMethods);
});

test('renders chat-completion item and generated methods', async () => {
render(<Methods testSuite={baseTestSuite} selectedApplication={selectedApplication} onChange={onChange} />);
test('renders chat-completion method as first item', async () => {
const testSuite: any = { endpointRef: {} };
const selectedApplication: any = { deploymentId: 'test-deployment', $type: 'application' };

render(<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />);

await waitFor(() => {
expect(screen.getByTestId('method-0')).toBeInTheDocument();
expect(screen.getByTestId('method-1')).toBeInTheDocument();
expect(screen.getByTestId('method-2')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /POST.*\/api\/users/ })).toBeInTheDocument();
});
});

test('clicking generated method calls onChange updater with correct endpointRef', async () => {
render(<Methods testSuite={baseTestSuite} selectedApplication={selectedApplication} onChange={onChange} />);
test('fetches deployment and generates methods on mount', async () => {
const testSuite: any = { endpointRef: {} };
const selectedApplication: any = { deploymentId: 'test-deployment', $type: 'application' };

render(<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />);

await waitFor(() => {
expect(screen.getByTestId('method-1')).toBeInTheDocument();
expect(mockGetDeployment).toHaveBeenCalledWith('test-deployment', 'application');
expect(mockGenerateMethodPathCombinations).toHaveBeenCalledWith(mockDeployment.routes);
});
});

const genMethodButton = screen.getByTestId('method-1');
fireEvent.click(genMethodButton);
test('renders all generated methods after chat-completion', async () => {
const testSuite: any = { endpointRef: {} };
const selectedApplication: any = { deploymentId: 'test-deployment', $type: 'application' };

render(<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />);

await waitFor(() => {
expect(screen.getByRole('button', { name: 'GET /api/users' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'POST /api/users' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'GET /api/data' })).toBeInTheDocument();
});
});

expect(onChange).toHaveBeenCalledTimes(1);
test('sets active method based on existing endpointRef on mount', async () => {
const testSuite: any = {
endpointRef: { method: 'POST', relativeUrlPattern: '/api/users' },
};
const selectedApplication: any = { deploymentId: 'test-deployment', $type: 'application' };

const updater = onChange.mock.calls[0][0];
const newState = updater(baseTestSuite);
render(<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />);

expect(newState.endpointRef).toEqual({ method: 'POST', relativeUrl: '/data' });
await waitFor(() => {
expect(screen.getByRole('button', { name: 'POST /api/users' })).toBeInTheDocument();
});

const activeButton = screen.getByRole('button', { name: 'POST /api/users' });
});

test('clicking chat-completion item sets endpointRef to CHAT_COMPLETION_METHOD', async () => {
render(<Methods testSuite={baseTestSuite} selectedApplication={selectedApplication} onChange={onChange} />);
test('defaults to chat-completion when endpointRef does not match any method', async () => {
const testSuite: any = {
endpointRef: { method: 'DELETE', relativeUrlPattern: '/not-found' },
};
const selectedApplication: any = { deploymentId: 'test-deployment', $type: 'application' };

render(<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />);

await waitFor(() => {
expect(screen.getByTestId('method-0')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /POST.*\/api\/users/ })).toBeInTheDocument();
});

const chatButton = screen.getByTestId('method-0');
fireEvent.click(chatButton);
const chatButton = screen.getByRole('button', { name: /POST.*\/api\/users/ });
});

expect(onChange).toHaveBeenCalledTimes(1);
test('does not fetch deployment if already loaded', async () => {
const testSuite: any = { endpointRef: {} };
const selectedApplication: any = { deploymentId: 'test-deployment', $type: 'application' };

const { rerender } = render(
<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />,
);

await waitFor(() => {
expect(mockGetDeployment).toHaveBeenCalledTimes(1);
});

const updater = onChange.mock.calls[0][0];
const newState = updater(baseTestSuite);
rerender(<Methods testSuite={testSuite} selectedApplication={selectedApplication} onChange={onChange} />);

expect(newState.endpointRef).toBeDefined();
expect(mockGetDeployment).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const ImportFileModal: FC<Props> = ({ selectedTestSuiteId, isModalOpen, onClose,

const [isLoading, setIsLoading] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [testCases, setTestCases] = useState<TestCase[] | null>(null);
const [testCases, setTestCases] = useState<object[] | null>(null);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([]);

const onChangeFile = (files: File[]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
export interface ImportPreview {
autoDetectedSchema: AutoDetectedSchema[];
detectedColumns: ColumnMapping[];
sampleRows: RowMapping[];
totalRows: number;
warnings: ValidationWarning[];
}

export interface AutoDetectedSchema {
name: string;
type: string;
required: boolean;
}

export interface ColumnMapping {
fieldName: string;
headerName: string;
Expand All @@ -15,8 +22,7 @@ export interface ColumnMapping {
export interface RowMapping {
enabled: boolean;
valid: boolean;
facts: Record<string, string | number>;
parameters: Record<string, unknown>;
data: Record<string, string | number>;
validationWarnings: RowValidationWarning[];
testCaseName: string;
}
Expand All @@ -25,7 +31,7 @@ export interface RowValidationWarning {
code: string;
message: string;
path: string;
source: string;
fieldName: string;
}

export interface ValidationWarning {
Expand Down
Loading
Loading