From c11a74abdaa4099e20dd5994d6bc0eb8e58885a9 Mon Sep 17 00:00:00 2001 From: Jared <39230796+jialudev@users.noreply.github.com> Date: Thu, 26 Dec 2024 16:42:48 +0800 Subject: [PATCH] add unit tests for Dataset (#874) * test: add unit tests for Dataset --- frontend/setupTests.js | 28 ++- .../datasets/DatasetRelationsCard.spec.js | 59 ++++++ .../datasets/DatasetSettings.spec.js | 150 ++++++++++++++ .../__tests__/shared/RepoCards.spec.js | 190 ++++++++++++++++++ frontend/src/components/shared/RepoCards.vue | 4 +- 5 files changed, 424 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/__tests__/datasets/DatasetRelationsCard.spec.js create mode 100644 frontend/src/components/__tests__/datasets/DatasetSettings.spec.js create mode 100644 frontend/src/components/__tests__/shared/RepoCards.spec.js diff --git a/frontend/setupTests.js b/frontend/setupTests.js index ac798a5a3..b1bcaeba8 100644 --- a/frontend/setupTests.js +++ b/frontend/setupTests.js @@ -1,13 +1,18 @@ import { config } from '@vue/test-utils'; import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; -import CsgButton from '@/components/shared/CsgButton.vue'; +import CsgButton from '@/components/shared/CsgButton.vue'; import SvgIcon from '@/components/shared/SvgIcon.vue'; +import FlashMessage from '@/components/shared/FlashMessage.vue'; import * as ElementPlusIconsVue from '@element-plus/icons-vue'; import { createI18n } from 'vue-i18n'; import { createPinia } from 'pinia' import en from '@/locales/en.js' import zh from '@/locales/zh.js' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import '@/assets/stylesheets/element-plus/_variables.css' +import '@/assets/stylesheets/markdown.css' +import '@/style.css' const pinia = createPinia(); const i18n = createI18n({ @@ -19,16 +24,29 @@ const i18n = createI18n({ } }); -config.global.plugins = [ElementPlus, i18n, pinia]; - -// register global components config.global.components = { SvgIcon, CsgButton, + FlashMessage, ...ElementPlusIconsVue }; -// gllbal mock +config.global.plugins = [ + [ElementPlus, { + locale: zhCn, + }], + i18n, + pinia +]; + +const DEFAULT_TAGS = [] +const CSGHUB_SERVER = 'http://localhost:8080' + +config.global.provide = { + defaultTags: DEFAULT_TAGS, + csghubServer: CSGHUB_SERVER, + nameRule: /^(?=.{2,64}$)(?!.*[-_.]{2})[a-zA-Z][a-zA-Z0-9_.-]*[a-zA-Z0-9]+$/ +}; // Mock window.location const mockLocation = { diff --git a/frontend/src/components/__tests__/datasets/DatasetRelationsCard.spec.js b/frontend/src/components/__tests__/datasets/DatasetRelationsCard.spec.js new file mode 100644 index 000000000..380ec7d2a --- /dev/null +++ b/frontend/src/components/__tests__/datasets/DatasetRelationsCard.spec.js @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { mount } from "@vue/test-utils"; +import DatasetRelationsCard from "@/components/datasets/DatasetRelationsCard.vue"; +import RepoItem from "@/components/shared/RepoItem.vue"; + +const createWrapper = (props) => { + return mount(DatasetRelationsCard, { + props: { + namespacePath: 'test/namespace', + datasets: [], + ...props + } + }); +}; + +describe("DatasetRelationsCard", () => { + it("mounts correctly", () => { + const wrapper = createWrapper(); + expect(wrapper.vm).toBeDefined(); + }); + + it("renders correctly with props", () => { + const datasets = [{ + id: 1, + name: 'Dataset 1', + path: 'user/dataset-1', + updated_at: '2024-03-20 10:00:00', + downloads: 100 + }, { + id: 2, + name: 'Dataset 2', + path: 'user/dataset-2', + updated_at: '2024-03-20 10:00:00', + downloads: 200 + }]; + + const wrapper = createWrapper({ datasets }); + + // Test title and count display + expect(wrapper.find('h3').text()).toContain('Associated Datasets2'); + expect(wrapper.find('h3 .text-gray-500').text().trim()).toBe('2'); + + // Test RepoItem components rendering + const repoItems = wrapper.findAllComponents(RepoItem); + expect(repoItems.length).toBe(datasets.length); + + // Verify RepoItem props + const firstRepoItem = repoItems[0]; + expect(firstRepoItem.props('repo')).toEqual(datasets[0]); + expect(firstRepoItem.props('repoType')).toBe('dataset'); + expect(firstRepoItem.props('cardType')).toBe('relations'); + }); + + it("renders correctly with empty datasets", () => { + const wrapper = createWrapper({ datasets: [] }); + expect(wrapper.findAllComponents(RepoItem).length).toBe(0); + expect(wrapper.find('h3 .text-gray-500').text().trim()).toBe('0'); + }); +}); diff --git a/frontend/src/components/__tests__/datasets/DatasetSettings.spec.js b/frontend/src/components/__tests__/datasets/DatasetSettings.spec.js new file mode 100644 index 000000000..b4892d034 --- /dev/null +++ b/frontend/src/components/__tests__/datasets/DatasetSettings.spec.js @@ -0,0 +1,150 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { mount } from "@vue/test-utils"; +import DatasetSettings from "@/components/datasets/DatasetSettings.vue"; +import { createPinia, setActivePinia } from 'pinia'; + +// Mock the API response +vi.mock('../../../packs/useFetchApi', () => ({ + default: (url) => ({ + post: () => ({ + json: () => Promise.resolve({ + data: { value: { msg: 'Success' } }, + error: { value: null } + }) + }), + put: () => ({ + json: () => Promise.resolve({ + data: { value: { msg: 'Success' } }, + error: { value: null } + }) + }), + delete: () => ({ + json: () => Promise.resolve({ + data: { value: null }, + error: { value: null } + }) + }), + get: () => ({ + json: () => Promise.resolve({ + data: { + value: { + data: { + content: btoa('test content'), + sha: 'test-sha' + } + } + }, + error: { value: null } + }) + }), + json: () => { + if (url === '/tags') { + return Promise.resolve({ + data: { + value: { + data: [ + { name: 'tag1', category: 'industry', scope: 'dataset', show_name: 'Tag 1' }, + { name: 'tag2', category: 'industry', scope: 'dataset', show_name: 'Tag 2' } + ] + } + }, + error: { value: null } + }) + } + return Promise.resolve({ + data: { value: null }, + error: { value: null } + }) + } + }) +})); + +const createWrapper = (props = {}) => { + return mount(DatasetSettings, { + props: { + path: "test/dataset", + datasetNickname: "Test Dataset", + datasetDesc: "Test Description", + default_branch: "main", + tagList: [], + tags: { + task_tags: [], + other_tags: [], + industry_tags: [] + }, + ...props + }, + global: { + plugins: [createPinia()], + mocks: { + $t: (key) => key + } + } + }); +}; + +describe("DatasetSettings", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it("mounts correctly", () => { + const wrapper = createWrapper(); + expect(wrapper.vm).toBeDefined(); + }); + + it("displays dataset path correctly", () => { + const wrapper = createWrapper(); + expect(wrapper.find('.bg-gray-50').text()).toBe("test/dataset"); + }); + + it("updates dataset nickname", async () => { + const wrapper = createWrapper(); + await wrapper.find('input').setValue('New Dataset Name'); + await wrapper.findAll('button').find(btn => btn.text() === 'all.update').trigger('click'); + expect(wrapper.vm.theDatasetNickname).toBe('New Dataset Name'); + }); + + it("updates dataset description", async () => { + const wrapper = createWrapper(); + const textarea = wrapper.find('textarea'); + await textarea.setValue('New Description'); + const updateButtons = wrapper.findAll('button'); + await updateButtons[1].trigger('click'); + expect(wrapper.vm.theDatasetDesc).toBe('New Description'); + }); + + it("handles tag selection correctly", async () => { + const wrapper = createWrapper({ + tagList: [{ name: "tag1", show_name: "Tag 1" }] + }); + await wrapper.vm.selectTag({ name: "tag1", show_name: "Tag 1" }); + expect(wrapper.vm.selectedTags).toHaveLength(1); + }); + + it("removes tag when close icon is clicked", async () => { + const wrapper = createWrapper(); + await wrapper.setData({ + selectedTags: [{ name: "tag1", show_name: "Tag 1" }] + }); + await wrapper.vm.removeTag("tag1"); + expect(wrapper.vm.selectedTags).toHaveLength(0); + }); + + it.skip("handles visibility change", async () => { + const wrapper = createWrapper(); + const select = wrapper.find('.el-select'); + await select.trigger('click'); + const options = wrapper.findAll('.el-option'); + await options[0].trigger('click'); + expect(wrapper.vm.visibilityName).toBe('Private'); + }); + + it("validates delete dataset input", async () => { + const wrapper = createWrapper(); + const deleteInput = wrapper.findAll('input').at(-1); + await deleteInput.setValue('test/dataset'); + const deleteButton = wrapper.find('#confirmDelete'); + expect(deleteButton.classes()).toContain('bg-error-600'); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/__tests__/shared/RepoCards.spec.js b/frontend/src/components/__tests__/shared/RepoCards.spec.js new file mode 100644 index 000000000..ab90599a4 --- /dev/null +++ b/frontend/src/components/__tests__/shared/RepoCards.spec.js @@ -0,0 +1,190 @@ +import { describe, it, expect, vi } from "vitest"; +import { mount } from "@vue/test-utils"; +import RepoCards from "@/components/shared/RepoCards.vue"; +import RepoItem from "@/components/shared/RepoItem.vue"; +import TagSidebar from "@/components/tags/TagSidebar.vue"; +import CsgPagination from "@/components/shared/CsgPagination.vue"; +import { ElInput, ElSelect } from 'element-plus'; + +// Mock getQueryParams +vi.mock('@/components/shared/RepoCards.vue', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getQueryParams: () => ({ + tag: 'test-tag', + tagType: 'test-type' + }) + }; +}); + +// Mock fetch API +vi.mock('@/packs/useFetchApi', () => ({ + default: vi.fn((url) => ({ + json: () => { + if (url === '/tags') { + return Promise.resolve({ + data: { + value: { + data: [ + { + category: 'task', + scope: 'model', + built_in: true, + group: 'computer_vision', + name: 'computer_vision', + show_name: 'Computer Vision' + }, + { + category: 'framework', + scope: 'model', + built_in: true, + name: 'pytorch', + show_name: 'PyTorch' + }, + { + category: 'language', + scope: 'model', + built_in: true, + name: 'python', + show_name: 'Python' + }, + { + category: 'license', + scope: 'model', + built_in: true, + name: 'MIT', + show_name: 'MIT License' + } + ] + } + } + }); + } + return Promise.resolve({ + data: { + value: { + data: [], + total: 0 + } + } + }); + } + })) +})); + +// Mock tags API response +vi.mock('@/packs/useFetchApi', () => ({ + default: vi.fn((url) => ({ + json: () => { + if (url === '/tags') { + return Promise.resolve({ + data: { + value: { + data: [ + { + category: 'task', + scope: 'model', + built_in: true, + group: 'computer_vision', + name: 'computer_vision', + show_name: 'Computer Vision' + }, + { + category: 'framework', + scope: 'model', + built_in: true, + name: 'pytorch', + show_name: 'PyTorch' + }, + { + category: 'language', + scope: 'model', + built_in: true, + name: 'english', + show_name: 'English' + }, + { + category: 'license', + scope: 'model', + built_in: true, + name: 'MIT', + show_name: 'MIT License' + } + ] + } + } + }); + } + return Promise.resolve({ + data: { + value: { + data: [], + total: 0 + } + } + }); + } + })) +})); + + +const createWrapper = (props = {}) => { + return mount(RepoCards, { + props: { + repoType: 'model', + ...props + }, + global: { + components: { + RepoItem, + TagSidebar, + CsgPagination + }, + stubs: { + 'svg-icon': true, + 'el-message': true + } + } + }); +}; + +window.location.href = 'http://localhost:3000/models'; + +describe("RepoCards", () => { + it("mounts correctly", () => { + + const wrapper = createWrapper(); + expect(wrapper.vm).toBeDefined(); + }); + + it("renders correct components based on repoType", () => { + const wrapper = createWrapper({ repoType: 'model' }); + expect(wrapper.findComponent(TagSidebar).exists()).toBe(true); + expect(wrapper.findComponent(ElInput).exists()).toBe(true); + expect(wrapper.findComponent(ElSelect).exists()).toBe(true); + }); + + it("calculates correct perPage based on repoType", () => { + const modelWrapper = createWrapper({ repoType: 'model' }); + expect(modelWrapper.vm.perPage).toBe(16); + + const spaceWrapper = createWrapper({ repoType: 'space' }); + expect(spaceWrapper.vm.perPage).toBe(9); + }); + + it("handles tag selection correctly", async () => { + const wrapper = createWrapper(); + await wrapper.vm.resetTags('vision', 'pytorch', 'python', 'MIT'); + + expect(wrapper.vm.taskTag).toBe('vision'); + expect(wrapper.vm.frameworkTag).toBe('pytorch'); + expect(wrapper.vm.languageTag).toBe('python'); + expect(wrapper.vm.licenseTag).toBe('MIT'); + }); + + it("loads repos on mount for space type", () => { + const wrapper = createWrapper({ repoType: 'space' }); + expect(wrapper.vm.reposData).toBeDefined(); + }); +}); diff --git a/frontend/src/components/shared/RepoCards.vue b/frontend/src/components/shared/RepoCards.vue index 5b679fdf5..eb4fbc561 100644 --- a/frontend/src/components/shared/RepoCards.vue +++ b/frontend/src/components/shared/RepoCards.vue @@ -45,7 +45,7 @@ height="18" /> - {{ $t(`${repoType}s`) }} + {{ $t(`${repoType}s.title`) }} {{ totalRepos }} @@ -276,7 +276,7 @@ } else { let tempTaskTags = {} const allTaskTags = data.value.data.filter(tag => tag.category === 'task' && tag.scope === props.repoType && tag.built_in === true) - tagFields[props.repoType].forEach((field) => { + tagFields[props.repoType]?.forEach((field) => { const fieldTags = allTaskTags.filter(tag => tag.group === field) tempTaskTags[field] = fieldTags })