Skip to content

Commit 7e9bad1

Browse files
zhendizhendi.wang
andauthored
feat(CsgButton): emit click event on button click (#1240)
* Draft MR * feat(CsgButton): emit click event on button click * feat(tests): enhance NewModel component tests - Mock vue-i18n for translations - Mock UserStore for user data - Mock useFetchApi for API calls - Improve form validation tests - Add success message on form submission --------- Co-authored-by: zhendi.wang <[email protected]>
1 parent b99971a commit 7e9bad1

File tree

7 files changed

+126
-55
lines changed

7 files changed

+126
-55
lines changed

frontend/src/components/__tests__/application_spaces/ApplicationSpaceSettings.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ describe('ApplicationSpaceSettings', () => {
8484
it('updates application space nickname when button is clicked', async () => {
8585
const wrapper = createWrapper()
8686
await wrapper.setData({ theApplicationSpaceNickname: 'New Name' })
87-
await wrapper.find('[data-test="update-nickname"]').trigger('click')
87+
await wrapper.findComponent('[data-test="update-nickname"]').vm.$emit('click')
8888
expect(ElMessage.success).toHaveBeenCalledWith('Success')
8989
})
9090

9191
it('update application space description when button is clicked', async () => {
9292
const wrapper = createWrapper()
9393
await wrapper.setData({ theApplicationSpaceDesc: 'New Description' })
94-
await wrapper.find('[data-test="update-description"]').trigger('click')
94+
await wrapper.findComponent('[data-test="update-description"]').vm.$emit('click')
9595
expect(ElMessage.success).toHaveBeenCalledWith('Success')
9696
})
9797
})

frontend/src/components/__tests__/application_spaces/NewApplicationSpace.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const createWrapper = (props) => {
1919

2020
const triggerFormButton = async (wrapper) => {
2121
const button = wrapper.findComponent({ name: 'CsgButton' })
22-
await button.trigger('click')
22+
await button.vm.$emit('click') // Trigger the form submission
2323
await wrapper.vm.$nextTick()
2424
}
2525

@@ -120,8 +120,8 @@ describe('NewApplicationSpace', () => {
120120
describe('when select docker sdk', async () => {
121121
it('fetchs the docker templates', async () => {
122122
const wrapper = createWrapper()
123-
const dockerRadio = wrapper.find('#sdk-docker')
124-
await dockerRadio.trigger('click')
123+
const dockerRadio = wrapper.findComponent('#sdk-docker')
124+
await dockerRadio.vm.$emit('click')
125125
await waitFor(() => {
126126
expect(wrapper.vm.dockerTemplates[0].name).toEqual('ChatUI')
127127
})

frontend/src/components/__tests__/codes/CodeSettings.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ describe('CodeSettings', () => {
6767
it('updates code nickname when button is clicked', async () => {
6868
const wrapper = createWrapper()
6969
await wrapper.setData({ theCodeNickname: 'New Name' })
70-
await wrapper.find('[data-test="update-nickname"]').trigger('click')
70+
await wrapper.findComponent('[data-test="update-nickname"]').vm.$emit('click')
7171
expect(ElMessage.success).toHaveBeenCalledWith('Success')
7272
})
7373

7474
it('shows warning when trying to update empty nickname', async () => {
7575
const wrapper = createWrapper()
7676
await wrapper.setData({ theCodeNickname: '' })
77-
await wrapper.find('[data-test="update-nickname"]').trigger('click')
77+
await wrapper.findComponent('[data-test="update-nickname"]').vm.$emit('click')
7878
expect(ElMessage.success).toHaveBeenCalled()
7979
})
8080
})

frontend/src/components/__tests__/codes/NewCode.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const createWrapper = (props) => {
1919

2020
const triggerFormButton = async (wrapper) => {
2121
const button = wrapper.findComponent({ name: 'CsgButton' })
22-
await button.trigger('click')
22+
await button.vm.$emit('click')
2323
await wrapper.vm.$nextTick()
2424
}
2525

Lines changed: 113 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,70 @@
1-
import { describe, it, expect, beforeEach, vi } from "vitest";
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
22
import { mount } from "@vue/test-utils";
33
import NewModel from "@/components/models/NewModel.vue";
4-
import SvgIcon from '@/components/shared/SvgIcon.vue';
54
import ElementPlus from 'element-plus'
6-
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
5+
import waitForExpect from 'wait-for-expect';
76

8-
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
7+
// Mock vue-i18n
8+
vi.mock('vue-i18n', () => ({
9+
useI18n: () => ({
10+
t: (key) => key,
11+
}),
12+
}));
913

14+
// Mock UserStore
15+
vi.mock('../../../stores/UserStore', () => ({
16+
default: () => ({
17+
username: 'testuser',
18+
orgs: [{ path: 'testorg' }]
19+
})
20+
}));
21+
22+
// Mock useFetchApi to handle license fetching and form submission
23+
vi.mock('../../../packs/useFetchApi', () => {
24+
const mock = vi.fn().mockImplementation((url) => {
25+
if (url.startsWith('/tags')) {
26+
return {
27+
json: () => Promise.resolve({
28+
data: { value: { data: [{name: 'MIT'}, {name: 'Apache-2.0'}] } }
29+
})
30+
};
31+
}
32+
if (url === '/models') {
33+
return {
34+
post: () => ({
35+
json: () => Promise.resolve({
36+
data: { value: { data: { path: 'testuser/testmodel' } } },
37+
error: { value: null }
38+
})
39+
})
40+
};
41+
}
42+
// Default mock for any other calls
43+
return {
44+
post: () => ({
45+
json: () => Promise.resolve({ data: { value: {} }, error: { value: null } })
46+
}),
47+
json: () => Promise.resolve({ data: { value: {} } })
48+
};
49+
});
50+
return { default: mock };
51+
});
1052

1153
const createWrapper = (props) => {
1254
return mount(NewModel, {
1355
global: {
56+
plugins: [ElementPlus],
1457
provide: {
1558
nameRule: /^[a-zA-Z][a-zA-Z0-9-_.]*[a-zA-Z0-9]$/,
59+
},
60+
stubs: {
61+
SvgIcon: true,
62+
PublicAndPrivateRadioGroup: true,
63+
// Stub CsgButton to be a simple button that emits a click event
64+
CsgButton: {
65+
template: '<button @click="$emit(\'click\')"><slot/></button>',
66+
props: ['loading']
67+
}
1668
}
1769
},
1870
props: {
@@ -22,67 +74,94 @@ const createWrapper = (props) => {
2274
});
2375
};
2476

25-
77+
// Helper to trigger form submission and wait for reactivity
2678
async function triggerFormButton(wrapper) {
27-
const button = wrapper.findComponent({ name: 'CsgButton' })
79+
const button = wrapper.find('button');
2880
await button.trigger('click');
29-
await delay(300);
30-
await wrapper.vm.$nextTick()
81+
// Wait for the next DOM update cycle
82+
await wrapper.vm.$nextTick();
3183
}
3284

33-
// Mock stores
34-
vi.mock('../../../stores/UserStore', () => ({
35-
default: () => ({
36-
username: 'testuser',
37-
orgs: [{ path: 'testorg' }]
38-
})
39-
}));
40-
41-
const buttonClass = '.btn.btn-primary'
42-
4385
describe("NewModel", () => {
44-
describe("mount", async () => {
45-
it("mounts correctly", () => {
86+
beforeEach(() => {
87+
// Reset mocks and window.location before each test
88+
vi.clearAllMocks();
89+
delete window.location;
90+
window.location = { href: '' };
91+
});
92+
93+
describe("mount", () => {
94+
it("mounts correctly", async () => {
4695
const wrapper = createWrapper();
96+
// Wait for async operations in onMounted to complete
97+
await new Promise(resolve => setTimeout(resolve, 0));
4798
expect(wrapper.exists()).toBe(true);
4899
});
49100
});
50101

51102
describe("form validation", () => {
52103
it("validates required fields", async () => {
53104
const wrapper = createWrapper();
105+
await new Promise(resolve => setTimeout(resolve, 0)); // Wait for mount
106+
// Clear pre-filled fields to test required rule
107+
wrapper.vm.dataForm.owner = '';
108+
wrapper.vm.dataForm.name = '';
109+
wrapper.vm.dataForm.license = '';
54110
await triggerFormButton(wrapper);
55-
const formErrors = wrapper.findAll('.el-form-item__error');
56-
expect(formErrors.length).toBeGreaterThan(0);
111+
112+
await waitForExpect(() => {
113+
const formErrors = wrapper.findAll('.el-form-item__error');
114+
expect(formErrors.length).toBe(3); // owner, name, license
115+
});
57116
});
58117

59118
it("validates invalid model name", async () => {
60119
const wrapper = createWrapper();
120+
await new Promise(resolve => setTimeout(resolve, 0));
61121
wrapper.vm.dataForm.name = 'a'; // Invalid length
122+
wrapper.vm.dataForm.license = 'MIT'; // Provide valid license
62123
await triggerFormButton(wrapper);
63-
expect(wrapper.find('.el-form-item__error').exists()).toBe(true);
124+
125+
await waitForExpect(() => {
126+
const error = wrapper.find('.el-form-item__error');
127+
expect(error.exists()).toBe(true);
128+
expect(error.text()).toContain('rule.lengthLimit');
129+
});
64130
});
65131

66-
it("validates valid model name", async () => {
132+
it("validates valid form", async () => {
67133
const wrapper = createWrapper();
68-
wrapper.vm.dataForm.name = 'valid_model'; // Invalid length
69-
wrapper.vm.dataForm.license = 'apache-2.0'; // Invalid length
134+
await new Promise(resolve => setTimeout(resolve, 0));
135+
wrapper.vm.dataForm.owner = 'testuser';
136+
wrapper.vm.dataForm.name = 'valid_model';
137+
wrapper.vm.dataForm.license = 'MIT';
70138
await triggerFormButton(wrapper);
71-
expect(wrapper.find('.el-form-item__error').exists()).toBe(false);
139+
140+
await waitForExpect(() => {
141+
expect(wrapper.find('.el-form-item__error').exists()).toBe(false);
142+
});
72143
});
73144

74145
it("validates owner selection", async () => {
75146
const wrapper = createWrapper();
76-
wrapper.vm.dataForm.owner = ''; // Invalid owner
147+
await new Promise(resolve => setTimeout(resolve, 0));
148+
wrapper.vm.dataForm.owner = ''; // Set invalid owner
77149
await triggerFormButton(wrapper);
78-
expect(wrapper.find('.el-form-item__error').exists()).toBe(true);
150+
151+
await waitForExpect(() => {
152+
const formErrors = wrapper.findAll('.el-form-item__error');
153+
const ownerError = formErrors.filter(e => e.text().includes('all.pleaseSelect'));
154+
expect(ownerError.length).toBeGreaterThan(0);
155+
});
79156
});
80157
});
81158

82159
describe("form submission", () => {
83-
it("shows success message on successful submission", async () => {
160+
it("redirects on successful submission", async () => {
84161
const wrapper = createWrapper();
162+
await new Promise(resolve => setTimeout(resolve, 0));
85163

164+
// Fill the form with valid data
86165
wrapper.vm.dataForm = {
87166
owner: 'testuser',
88167
name: 'valid-model',
@@ -92,23 +171,11 @@ describe("NewModel", () => {
92171
visibility: 'public'
93172
};
94173

95-
// Mock the API response
96-
vi.mock('../../../packs/useFetchApi', () => ({
97-
default: () => ({
98-
post: () => ({
99-
json: () => Promise.resolve({
100-
data: { value: { data: { path: 'testuser/testmodel' } } },
101-
error: { value: null }
102-
})
103-
})
104-
})
105-
}));
106-
107174
await triggerFormButton(wrapper);
108-
109-
// validate href is correct
110-
expect(window.location.href).toBe('/models/testuser/testmodel');
175+
176+
await waitForExpect(() => {
177+
expect(window.location.href).toBe('/models/testuser/testmodel');
178+
});
111179
});
112-
113180
});
114181
});

frontend/src/components/__tests__/organizations/NewOrganization.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const createWrapper = (props) => {
1818

1919
const triggerFormButton = async (wrapper) => {
2020
const button = wrapper.findComponent({ name: 'CsgButton' })
21-
await button.trigger('click')
21+
await button.vm.$emit('click')
2222

2323
// deprecate this, as we can use waitFor to wait for the async operation to complete
2424
// return new Promise((resolve) => { setTimeout(resolve, 2000) })

frontend/src/components/shared/CsgButton.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:class="btnClass"
99
:type="btnType"
1010
:disabled="disabled || loading || hasDisabledClass"
11+
@click="$emit('click', $event)"
1112
>
1213
<i v-if="loading" class="el-icon is-loading"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"></path></svg></i>
1314
<SvgIcon
@@ -28,6 +29,9 @@
2829
<script setup>
2930
import { computed } from 'vue'
3031
import { ElTooltip } from 'element-plus';
32+
33+
defineEmits(['click']);
34+
3135
const props = defineProps({
3236
name: String,
3337
btnType: {

0 commit comments

Comments
 (0)