From a45d4f245bc14f372aa202efc4ba207e429e468a Mon Sep 17 00:00:00 2001 From: "Zhendi.Wang" Date: Wed, 25 Dec 2024 15:56:32 +0800 Subject: [PATCH] unit tests for new evaluation and evaluation detail page (#866) --- .../evaluations/EvaluationDetail.spec.js | 85 ++++++++++ .../evaluations/NewEvaluation.spec.js | 158 ++++++++++++++++++ .../components/evaluations/NewEvaluation.vue | 1 + makefile | 26 +-- 4 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/__tests__/evaluations/EvaluationDetail.spec.js create mode 100644 frontend/src/components/__tests__/evaluations/NewEvaluation.spec.js diff --git a/frontend/src/components/__tests__/evaluations/EvaluationDetail.spec.js b/frontend/src/components/__tests__/evaluations/EvaluationDetail.spec.js new file mode 100644 index 000000000..0da85894b --- /dev/null +++ b/frontend/src/components/__tests__/evaluations/EvaluationDetail.spec.js @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import EvaluationDetail from '../../evaluations/EvaluationDetail.vue' + +const createResponse = (data, errorMsg = null) => ({ + data: { value: { data } }, + error: { value: errorMsg ? { msg: errorMsg } : null } +}) + +const mockApiResponses = { + '/tags?scope=dataset&category=evaluation': createResponse([ + { name: 'tag1', show_name: 'Tag 1' } + ]), + '/evaluations/1': createResponse({ + task_name: 'task1', + submit_time: '2021-10-01', + repo_ids: ['repo1'], + task_desc: 'desc1', + datasets: [{ repo_id: 'namespace1/repo1', tags: [{ name: 'tag1' }] }], + download_url: 'test.zip' + }) +} + +vi.mock('../../../packs/useFetchApi', () => ({ + default: (url) => ({ + json: () => Promise.resolve(mockApiResponses[url] || createResponse([])) + }) +})) + +describe('EvaluationDetail', () => { + let wrapper + + beforeEach(() => { + wrapper = mount(EvaluationDetail, { + props: { + evaluationId: '1' + } + }) + }) + + it('renders evaluation details correctly', async () => { + const dateElement = wrapper.find('.text-gray-700.text-base') + expect(wrapper.find('.text-2xl').text()).toBe('task1') + expect(dateElement.text()).toBe('2021-10-01') + expect(wrapper.text()).includes('desc1') + }) + + it('displays download button when download_url is present', async () => { + const downloadBtn = wrapper.find('a.btn-primary') + expect(downloadBtn.exists()).toBe(true) + expect(downloadBtn.attributes('href')).toBe('test.zip') + }) + + it('groups datasets by categories correctly', async () => { + const groupedDatasets = wrapper.vm.groupedDatasets + expect(groupedDatasets).toHaveLength(1) + expect(groupedDatasets[0].name).toBe('tag1') + expect(groupedDatasets[0].datasets[0]).toBe('repo1') + }) + + it('formats table data correctly', async () => { + wrapper.vm.evaluationResult = { + summary: { + column: [{ key: 'score', customizeRender: true }], + data: [ + { + dataset: 'dataset1', + metric: 'metric1', + score: '0.95' + } + ] + } + } + wrapper.vm.scoresKey = 'score' + await wrapper.vm.$nextTick() + + const tableData = wrapper.vm.getTableData('all') + expect(tableData).toHaveLength(1) + expect(tableData[0]).toEqual({ + dataset: 'dataset1', + metric: 'metric1', + score: '0.95' + }) + }) +}) diff --git a/frontend/src/components/__tests__/evaluations/NewEvaluation.spec.js b/frontend/src/components/__tests__/evaluations/NewEvaluation.spec.js new file mode 100644 index 000000000..6b3c2955f --- /dev/null +++ b/frontend/src/components/__tests__/evaluations/NewEvaluation.spec.js @@ -0,0 +1,158 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import NewEvaluation from '../../evaluations/NewEvaluation.vue' + +const MOCK_USERNAME = 'testuser' +const MOCK_MODEL_PATH = `${MOCK_USERNAME}/testmodel` +const MOCK_DATASET_PATH = `${MOCK_USERNAME}/testdataset` +const MOCK_CLUSTER_ID = 'cluster1' +const MOCK_EVALUATION_PATH = `${MOCK_USERNAME}/testevaluation` + +const MOCK_RESOURCE = { + id: 1, + order_detail_id: 1, + name: 'gpu-resource', + type: 'gpu', + is_available: true, + pay_mode: 'minute', + price: 1000 +} + +const MOCK_TRANSFORMED_RESOURCE = { + label: 'all.minutePay', + options: [ + { + id: 1, + order_detail_id: 1, + name: 'gpu-resource', + type: 'gpu', + is_available: true, + pay_mode: 'minute', + price: 1000, + label: 'gpu-resource 10all.hourUnit' + } + ] +} + +const createResponse = (data, errorMsg = null) => ({ + data: { value: { data } }, + error: { value: errorMsg ? { msg: errorMsg } : null } +}) + +const mockApiResponses = { + [`/runtime_framework/models?search=test&deploy_type=4`]: createResponse([ + { path: MOCK_MODEL_PATH } + ]), + '/cluster': createResponse([ + { cluster_id: MOCK_CLUSTER_ID, region: 'region1' } + ]), + [`/space_resources?cluster_id=${MOCK_CLUSTER_ID}`]: createResponse([ + MOCK_RESOURCE + ]), + [`/models/${MOCK_MODEL_PATH}/runtime_framework?deploy_type=4`]: + createResponse([{ id: 1, frame_name: 'pytorch' }]), + '/tags?scope=dataset&category=evaluation': createResponse([ + { name: 'tag1', show_name: 'Tag 1' } + ]), + '/datasets?tag_category=runtime_framework&tag_name=pytorch&tag_category=evaluation&tag_name=tag1': + createResponse([{ path: MOCK_DATASET_PATH }]) +} + +vi.mock('../../../stores/UserStore', () => ({ + default: () => ({ username: MOCK_USERNAME }) +})) + +vi.mock('../../../packs/useFetchApi', () => ({ + default: (url) => ({ + json: () => Promise.resolve(mockApiResponses[url] || createResponse([])), + post: () => ({ + json: () => + Promise.resolve(createResponse({ path: MOCK_EVALUATION_PATH })) + }) + }) +})) + +describe('NewEvaluation', () => { + let wrapper + + beforeEach(() => { + vi.clearAllMocks() + wrapper = mount(NewEvaluation) + }) + + describe('Initial State', () => { + it('mounts and initializes with shared resource type', () => { + expect(wrapper.exists()).toBe(true) + expect(wrapper.vm.dataForm.evaluation_resource_type).toBe('shared') + }) + + it('loads initial data', async () => { + await wrapper.vm.$nextTick() + expect(wrapper.vm.evaluationClusters.length).toBeGreaterThan(0) + }) + }) + + describe('Form Validation', () => { + const validateForm = () => { + return new Promise((resolve) => { + wrapper.vm.$refs.dataFormRef.validate((valid) => resolve(valid)) + }) + } + + it('fails validation with empty required fields', async () => { + expect(await validateForm()).toBe(false) + }) + + it('fails validation with invalid model_id format', async () => { + wrapper.vm.dataForm.model_id = 'invalid/format/' + await wrapper.vm.$nextTick() + expect(await validateForm()).toBe(false) + }) + }) + + describe('Resource Type UI', () => { + const getClusterSelect = () => wrapper.find('[data-test="cluster-select"]') + + it('shows/hides dedicated resource fields based on type selection', async () => { + wrapper.vm.dataForm.evaluation_resource_type = 'dedicated' + await wrapper.vm.$nextTick() + expect(getClusterSelect().isVisible()).toBe(true) + + wrapper.vm.dataForm.evaluation_resource_type = 'shared' + await wrapper.vm.$nextTick() + expect(getClusterSelect().isVisible()).toBe(false) + }) + }) + + describe('API Interactions', () => { + it('fetches and formats models', async () => { + await wrapper.vm.fetchModels('test') + expect(wrapper.vm.models).toEqual([ + { key: MOCK_MODEL_PATH, value: MOCK_MODEL_PATH } + ]) + }) + + it('fetches resources for selected cluster', async () => { + wrapper.vm.dataForm.evaluation_cluster = MOCK_CLUSTER_ID + await wrapper.vm.fetchResources() + expect(wrapper.vm.evaluationResources).toEqual([ + MOCK_TRANSFORMED_RESOURCE + ]) + }) + }) + + describe('Form Submission', () => { + it('submits form and redirects on success', async () => { + wrapper.vm.dataForm = { + name: 'Test Evaluation', + model_id: MOCK_MODEL_PATH, + evaluation_framework: 1, + evaluation_dataset: [MOCK_DATASET_PATH], + evaluation_resource_type: 'shared' + } + + await wrapper.vm.handleSubmit() + expect(window.location.href).toBe('/resource-console') + }) + }) +}) diff --git a/frontend/src/components/evaluations/NewEvaluation.vue b/frontend/src/components/evaluations/NewEvaluation.vue index d5cb87141..37cdc8713 100644 --- a/frontend/src/components/evaluations/NewEvaluation.vue +++ b/frontend/src/components/evaluations/NewEvaluation.vue @@ -159,6 +159,7 @@ :label="t('evaluation.new.cluster')" class="w-full" prop="evaluation_cluster" + data-test="cluster-select" v-show="dataForm.evaluation_resource_type === 'dedicated'" >