Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete the unit tests for model components #859

Merged
merged 13 commits into from
Dec 19, 2024
Merged
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
10 changes: 10 additions & 0 deletions frontend/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

40 changes: 35 additions & 5 deletions frontend/setupTests.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import { config } from '@vue/test-utils';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import CsgButton from './src/components/shared/CsgButton.vue';
import SvgIcon from './src/components/shared/SvgIcon.vue';
import CsgButton from '@/components/shared/CsgButton.vue';
import SvgIcon from '@/components/shared/SvgIcon.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'

config.global.plugins = [ElementPlus];
const pinia = createPinia();
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en,
zh
}
});

config.global.plugins = [ElementPlus, i18n, pinia];

// register global components
config.global.components = {
CsgButton,
SvgIcon
SvgIcon,
CsgButton,
...ElementPlusIconsVue
};

// gllbal mock

// Mock window.location
const mockLocation = {
href: '',
search: ''
};

Object.defineProperty(window, 'location', {
value: mockLocation,
writable: true
});
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ describe("NewDataset", () => {
}

await wrapper.find('button').trigger('click');
await new Promise(resolve => setTimeout(resolve, 300));
expect(window.location.href).toBe('/datasets/testuser/testdataset');
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { mount } from "@vue/test-utils";
import ModelRelationsCard from "@/components/models/ModelRelationsCard.vue";
import RepoItem from "@/components/shared/RepoItem.vue";

const createWrapper = (props) => {
return mount(ModelRelationsCard, {
props: {
namespacePath: 'test/namespace',
models: [],
...props
}
});
};

describe("ModelRelationsCard", () => {
it("mounts correctly", () => {
const wrapper = createWrapper();
expect(wrapper.vm).toBeDefined();
});

it.skip("renders correctly with props", () => {
const models = [{
id: 1,
name: 'Model 1',
path: 'user/model-1',
updated_at: '2024-03-20 10:00:00',
downloads: 100
}, {
id: 2,
name: 'Model 2',
path: 'user/model-2',
updated_at: '2024-03-20 10:00:00',
downloads: 200
}];
const wrapper = createWrapper({ models });

expect(wrapper.find('h3').text()).toContain('Model used to traintest/namespace');
expect(wrapper.find('.text-gray-700').text()).toBe('Model used to train');
expect(wrapper.findAllComponents(RepoItem).length).toBe(models.length);
});
});
119 changes: 119 additions & 0 deletions frontend/src/components/__tests__/models/ModelSettings.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { mount } from "@vue/test-utils";
import ModelSettings from "@/components/models/ModelSettings.vue";
import { createPinia, setActivePinia } from 'pinia';
import { ElMessage, ElMessageBox } from 'element-plus';

// Mock Element Plus components and functions
vi.mock('element-plus', () => ({
ElMessage: {
install: vi.fn(),
error: vi.fn(),
success: vi.fn(),
warning: vi.fn()
},
ElMessageBox: {
install: vi.fn(),
confirm: vi.fn()
}
}));

// Mock the API response
vi.mock('../../../packs/useFetchApi', () => ({
default: (url) => ({
post: () => ({
json: () => Promise.resolve({
data: { value: { data: { path: 'testuser/testmodel' } } },
error: { value: null }
})
}),
json: () => {
// different url return different data
if (url === '/tags') {
return Promise.resolve({
data: {
value: {
data: [
{ name: 'tag1', category: 'industry', scope: 'model', show_name: 'Tag 1' },
{ name: 'tag2', category: 'industry', scope: 'model', show_name: 'Tag 2' },
{ name: 'tag3', category: 'other', scope: 'model', show_name: 'Tag 3' }
]
}
},
error: { value: null }
})
}
// return empty data
return Promise.resolve({
data: { value: null },
error: { value: null }
})
}
})
}));

const createWrapper = (props = {}) => {
return mount(ModelSettings, {
props: {
path: "test/model",
modelNickname: "Test Model",
modelDesc: "Test Description",
default_branch: "main",
tagList: [],
tags: {
task_tags: [],
other_tags: [],
industry_tags: []
},
...props
},
});
};

describe("ModelSettings", () => {
beforeEach(() => {
setActivePinia(createPinia());
});

it("mounts correctly", () => {
const wrapper = createWrapper();
expect(wrapper.vm).toBeDefined();
});

it("displays model path correctly", () => {
const wrapper = createWrapper();
expect(wrapper.find('.bg-gray-50').text()).toBe("test/model");
});

it.skip("updates model nickname when button is clicked", async () => {
const wrapper = createWrapper();
await wrapper.setData({ theModelNickname: "New Name" });
await wrapper.find('button').trigger('click');
expect(ElMessage.success).toHaveBeenCalled();
});

it.skip("shows warning when trying to update empty nickname", async () => {
const wrapper = createWrapper();
await wrapper.setData({ theModelNickname: "" });
const updateButton = wrapper.findAll('button').find(btn => btn.text() === 'all.update');
await updateButton.trigger('click');
expect(ElMessage.warning).toHaveBeenCalled();
});

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);
});
});
110 changes: 110 additions & 0 deletions frontend/src/components/__tests__/models/NewModel.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { mount } from "@vue/test-utils";
import NewModel from "@/components/models/NewModel.vue";
import SvgIcon from '@/components/shared/SvgIcon.vue';
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));


const createWrapper = (props) => {
return mount(NewModel, {
global: {
provide: {
nameRule: /^[a-zA-Z][a-zA-Z0-9-_.]*[a-zA-Z0-9]$/,
}
},
props: {
licenses: [['MIT', 'MIT License']],
...props
}
});
};


async function triggerFormButton(wrapper) {
const button = wrapper.findComponent({ name: 'CsgButton' })
await button.trigger('click');
await delay(300);
await wrapper.vm.$nextTick()
}

// Mock stores
vi.mock('../../../stores/UserStore', () => ({
default: () => ({
username: 'testuser',
orgs: [{ path: 'testorg' }]
})
}));

const buttonClass = '.btn.btn-primary'

describe("NewModel", () => {
describe("mount", async () => {
it("mounts correctly", () => {
const wrapper = createWrapper();
expect(wrapper.exists()).toBe(true);
});
});

describe("form validation", () => {
it("validates required fields", async () => {
const wrapper = createWrapper();
await triggerFormButton(wrapper);
const formErrors = wrapper.findAll('.el-form-item__error');
expect(formErrors.length).toBeGreaterThan(0);
});

it("validates model name length", async () => {
const wrapper = createWrapper();
wrapper.vm.dataForm.name = 'a'; // Invalid length
await triggerFormButton(wrapper);
expect(wrapper.find('.el-form-item__error').exists()).toBe(true);

wrapper.vm.dataForm.name = 'valid-model'; // Valid length
await triggerFormButton(wrapper);
expect(wrapper.find('.el-form-item__error').exists()).toBe(false);
});

it("validates owner selection", async () => {
const wrapper = createWrapper();
wrapper.vm.dataForm.owner = ''; // Invalid owner
await triggerFormButton(wrapper);
expect(wrapper.find('.el-form-item__error').exists()).toBe(true);
});
});

describe("form submission", () => {
it("shows success message on successful submission", async () => {
const wrapper = createWrapper();

wrapper.vm.dataForm = {
owner: 'testuser',
name: 'valid-model',
nickname: 'Test Model',
license: 'MIT',
desc: 'Test description',
visibility: 'public'
};

// Mock the API response
vi.mock('../../../packs/useFetchApi', () => ({
default: () => ({
post: () => ({
json: () => Promise.resolve({
data: { value: { data: { path: 'testuser/testmodel' } } },
error: { value: null }
})
})
})
}));

await triggerFormButton(wrapper);

// validate href is correct
expect(window.location.href).toBe('/models/testuser/testmodel');
});

});
});
Loading
Loading