diff --git a/packages/editor-ui/src/components/WorkflowEvaluation/tests/MetricsInput.test.ts b/packages/editor-ui/src/components/WorkflowEvaluation/tests/MetricsInput.test.ts new file mode 100644 index 0000000000..d612cd4529 --- /dev/null +++ b/packages/editor-ui/src/components/WorkflowEvaluation/tests/MetricsInput.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createComponentRenderer } from '@/__tests__/render'; +import MetricsInput from '../EditEvaluation/MetricsInput.vue'; +import userEvent from '@testing-library/user-event'; + +const renderComponent = createComponentRenderer(MetricsInput); + +describe('MetricsInput', () => { + let props: { modelValue: string[] }; + + beforeEach(() => { + props = { + modelValue: ['Metric 1', 'Metric 2'], + }; + }); + + it('should render correctly with initial metrics', () => { + const { getAllByPlaceholderText } = renderComponent({ props }); + const inputs = getAllByPlaceholderText('Enter metric name'); + expect(inputs).toHaveLength(2); + expect(inputs[0]).toHaveValue('Metric 1'); + expect(inputs[1]).toHaveValue('Metric 2'); + }); + + it('should update a metric when typing in the input', async () => { + const { getAllByPlaceholderText, emitted } = renderComponent({ + props: { + modelValue: [''], + }, + }); + const inputs = getAllByPlaceholderText('Enter metric name'); + await userEvent.type(inputs[0], 'Updated Metric 1'); + + expect(emitted('update:modelValue')).toBeTruthy(); + expect(emitted('update:modelValue')).toEqual('Updated Metric 1'.split('').map((c) => [[c]])); + }); + + it('should render correctly with no initial metrics', () => { + props.modelValue = []; + const { queryAllByRole, getByText } = renderComponent({ props }); + const inputs = queryAllByRole('textbox'); + expect(inputs).toHaveLength(0); + expect(getByText('New metric')).toBeInTheDocument(); + }); + + it('should handle adding multiple metrics', async () => { + const { getByText, emitted } = renderComponent({ props }); + const addButton = getByText('New metric'); + + addButton.click(); + addButton.click(); + addButton.click(); + + expect(emitted('update:modelValue')).toHaveProperty('length', 3); + }); +}); diff --git a/packages/editor-ui/src/components/WorkflowEvaluation/tests/TagsInput.test.ts b/packages/editor-ui/src/components/WorkflowEvaluation/tests/TagsInput.test.ts new file mode 100644 index 0000000000..168eef5ab0 --- /dev/null +++ b/packages/editor-ui/src/components/WorkflowEvaluation/tests/TagsInput.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import TagsInput from '../EditEvaluation/TagsInput.vue'; +import { createTestingPinia } from '@pinia/testing'; +import type { ITag } from '@/Interface'; + +describe('TagsInput', () => { + const mockTags: ITag[] = [ + { id: '1', name: 'Tag 1' }, + { id: '2', name: 'Tag 2' }, + { id: '3', name: 'Tag 3' }, + ]; + + const mockTagsById: Record = { + '1': mockTags[0], + '2': mockTags[1], + '3': mockTags[2], + }; + + const defaultProps = { + allTags: mockTags, + tagsById: mockTagsById, + isLoading: false, + startEditing: vi.fn(), + saveChanges: vi.fn(), + cancelEditing: vi.fn(), + }; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders correctly with default props', () => { + const wrapper = mount(TagsInput, { + props: defaultProps, + global: { + plugins: [createTestingPinia()], + }, + }); + expect(wrapper.exists()).toBe(true); + }); + + it('computes tag name correctly', async () => { + const wrapper = mount(TagsInput, { + props: { + ...defaultProps, + modelValue: { + isEditing: false, + appliedTagIds: ['1'], + }, + }, + global: { + plugins: [createTestingPinia()], + }, + }); + + const vm = wrapper.vm; + expect(vm.getTagName('1')).toBe('Tag 1'); + expect(vm.getTagName('4')).toBe(''); + }); + + it.only('updates tags correctly', async () => { + const wrapper = mount(TagsInput, { + props: { + ...defaultProps, + 'onUpdate:modelValue': async (e) => await wrapper.setProps({ modelValue: e }), + }, + global: { + plugins: [createTestingPinia()], + }, + }); + + await wrapper.find('[data-test-id=workflow-tags-dropdown]').setValue('test'); + + expect(wrapper.emitted('update:modelValue')).toBeTruthy(); + expect(wrapper.emitted('update:modelValue')![0][0]).toEqual({ + isEditing: false, + appliedTagIds: ['2'], + }); + }); + + it('clears tags when empty array is passed', async () => { + const wrapper = mount(TagsInput, { + props: defaultProps, + global: { + plugins: [createTestingPinia()], + }, + }); + + await wrapper.vm.updateTags([]); + expect(wrapper.emitted('update:modelValue')).toBeTruthy(); + expect(wrapper.emitted('update:modelValue')![0][0]).toEqual({ + isEditing: false, + appliedTagIds: [], + }); + }); + + it('handles editing state correctly', async () => { + const wrapper = mount(TagsInput, { + props: { + ...defaultProps, + modelValue: { + isEditing: true, + appliedTagIds: ['1'], + }, + }, + global: { + plugins: [createTestingPinia()], + }, + }); + + expect(wrapper.vm.modelValue.isEditing).toBe(true); + }); +}); diff --git a/packages/editor-ui/src/views/WorkflowEvaluation/tests/EvaluationEditView.test.ts b/packages/editor-ui/src/views/WorkflowEvaluation/tests/EvaluationEditView.test.ts new file mode 100644 index 0000000000..350e9d7d5b --- /dev/null +++ b/packages/editor-ui/src/views/WorkflowEvaluation/tests/EvaluationEditView.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { createPinia, setActivePinia } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { createComponentRenderer } from '@/__tests__/render'; +import EvaluationEditView from '@/views/WorkflowEvaluation/EvaluationEditView.vue'; +import { useRoute, useRouter } from 'vue-router'; +import { useToast } from '@/composables/useToast'; +import { useEvaluationForm } from '@/components/WorkflowEvaluation/composables/useEvaluationForm'; +import { useAnnotationTagsStore } from '@/stores/tags.store'; +import { ref, nextTick } from 'vue'; +import { VIEWS } from '@/constants'; + +vi.mock('vue-router'); +vi.mock('@/composables/useToast'); +vi.mock('@/components/WorkflowEvaluation/composables/useEvaluationForm'); +vi.mock('@/stores/tags.store'); + +describe('EvaluationEditView', () => { + const renderComponent = createComponentRenderer(EvaluationEditView); + + beforeEach(() => { + setActivePinia(createPinia()); + + vi.mocked(useRoute).mockReturnValue({ + params: {}, + path: '/test-path', + name: 'test-route', + } as ReturnType); + + vi.mocked(useRouter).mockReturnValue({ + push: vi.fn(), + resolve: vi.fn().mockReturnValue({ href: '/test-href' }), + } as unknown as ReturnType); + + vi.mocked(useToast).mockReturnValue({ + showMessage: vi.fn(), + showError: vi.fn(), + } as unknown as ReturnType); + vi.mocked(useEvaluationForm).mockReturnValue({ + state: ref({ + name: { value: '', isEditing: false, tempValue: '' }, + description: '', + tags: { appliedTagIds: [], isEditing: false }, + evaluationWorkflow: { id: '1', name: 'Test Workflow' }, + metrics: [], + }), + fieldsIssues: ref([]), + isSaving: ref(false), + loadTestData: vi.fn(), + saveTest: vi.fn(), + startEditing: vi.fn(), + saveChanges: vi.fn(), + cancelEditing: vi.fn(), + handleKeydown: vi.fn(), + } as unknown as ReturnType); + vi.mocked(useAnnotationTagsStore).mockReturnValue({ + isLoading: ref(false), + allTags: ref([]), + tagsById: ref({}), + fetchAll: vi.fn(), + } as unknown as ReturnType); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should load test data when testId is provided', async () => { + vi.mocked(useRoute).mockReturnValue({ + params: { testId: '1' }, + path: '/test-path', + name: 'test-route', + } as unknown as ReturnType); + const loadTestDataMock = vi.fn(); + vi.mocked(useEvaluationForm).mockReturnValue({ + ...vi.mocked(useEvaluationForm)(), + loadTestData: loadTestDataMock, + } as unknown as ReturnType); + + renderComponent({ + pinia: createTestingPinia(), + }); + + await nextTick(); + expect(loadTestDataMock).toHaveBeenCalledWith('1'); + }); + + it('should not load test data when testId is not provided', async () => { + const loadTestDataMock = vi.fn(); + vi.mocked(useEvaluationForm).mockReturnValue({ + ...vi.mocked(useEvaluationForm)(), + loadTestData: loadTestDataMock, + } as unknown as ReturnType); + + renderComponent({ + pinia: createTestingPinia(), + }); + + await nextTick(); + expect(loadTestDataMock).not.toHaveBeenCalled(); + }); + + it('should save test and show success message on successful save', async () => { + const saveTestMock = vi.fn().mockResolvedValue({}); + const showMessageMock = vi.fn(); + const routerPushMock = vi.fn(); + const routerResolveMock = vi.fn().mockReturnValue({ href: '/test-href' }); + vi.mocked(useEvaluationForm).mockReturnValue({ + ...vi.mocked(useEvaluationForm)(), + saveTest: saveTestMock, + } as unknown as ReturnType); + vi.mocked(useToast).mockReturnValue({ showMessage: showMessageMock } as unknown as ReturnType< + typeof useToast + >); + vi.mocked(useRouter).mockReturnValue({ + push: routerPushMock, + resolve: routerResolveMock, + } as unknown as ReturnType); + + const { getByTestId } = renderComponent({ + pinia: createTestingPinia(), + }); + await nextTick(); + const saveButton = getByTestId('run-test-button'); + saveButton.click(); + await nextTick(); + + expect(saveTestMock).toHaveBeenCalled(); + expect(showMessageMock).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + expect(routerPushMock).toHaveBeenCalledWith({ name: VIEWS.WORKFLOW_EVALUATION }); + }); + + it('should show error message on failed save', async () => { + const saveTestMock = vi.fn().mockRejectedValue(new Error('Save failed')); + const showErrorMock = vi.fn(); + vi.mocked(useEvaluationForm).mockReturnValue({ + ...vi.mocked(useEvaluationForm)(), + saveTest: saveTestMock, + } as unknown as ReturnType); + vi.mocked(useToast).mockReturnValue({ showError: showErrorMock } as unknown as ReturnType< + typeof useToast + >); + + const { getByTestId } = renderComponent({ + pinia: createTestingPinia(), + }); + await nextTick(); + const saveButton = getByTestId('run-test-button'); + saveButton.click(); + await nextTick(); + expect(saveTestMock).toHaveBeenCalled(); + expect(showErrorMock).toHaveBeenCalled(); + }); + + it('should display "Update Test" button when editing existing test', async () => { + vi.mocked(useRoute).mockReturnValue({ + params: { testId: '1' }, + path: '/test-path', + name: 'test-route', + } as unknown as ReturnType); + const { getByTestId } = renderComponent({ + pinia: createTestingPinia(), + }); + await nextTick(); + const updateButton = getByTestId('run-test-button'); + expect(updateButton.textContent).toContain('Update test'); + }); + + it('should display "Run Test" button when creating new test', async () => { + const { getByTestId } = renderComponent({ + pinia: createTestingPinia(), + }); + await nextTick(); + const saveButton = getByTestId('run-test-button'); + expect(saveButton).toBeTruthy(); + }); + + it('should apply "has-issues" class to inputs with issues', async () => { + vi.mocked(useEvaluationForm).mockReturnValue({ + ...vi.mocked(useEvaluationForm)(), + fieldsIssues: ref([{ field: 'name' }, { field: 'tags' }]), + } as unknown as ReturnType); + + const { container } = renderComponent({ + pinia: createTestingPinia(), + }); + await nextTick(); + expect(container.querySelector('.has-issues')).toBeTruthy(); + }); + + it('should fetch all tags on mount', async () => { + const fetchAllMock = vi.fn(); + vi.mocked(useAnnotationTagsStore).mockReturnValue({ + ...vi.mocked(useAnnotationTagsStore)(), + fetchAll: fetchAllMock, + } as unknown as ReturnType); + + renderComponent({ + pinia: createTestingPinia(), + }); + + await nextTick(); + expect(fetchAllMock).toHaveBeenCalled(); + }); +});