Skip to content

Commit b3ce78c

Browse files
ice201508Jiuling.Lei
andauthored
Csghub feature notebook (#1418)
* Main feature notebook * feature-notebook-fix-logs-1 * feature-add-notebook-instance-wakeup-btn * merge feature-notebook --------- Co-authored-by: Jiuling.Lei <[email protected]>
1 parent 99aa9d4 commit b3ce78c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2366
-37
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import NotebookDetail from '@/components/notebooks/NotebookDetail.vue'
4+
5+
// Mock vue-router usage is minimal here; component uses provide/inject and props.
6+
7+
// Mock vue3-cookies
8+
vi.mock('vue3-cookies', () => ({
9+
useCookies: () => ({ cookies: { get: vi.fn(() => 'mock-jwt') } })
10+
}))
11+
12+
// Mock RepoHeader/RepoTabs/LoadingSpinner
13+
vi.mock('@/components/shared/RepoHeader.vue', () => ({
14+
default: { name: 'RepoHeader', template: '<div>RepoHeader</div>' }
15+
}))
16+
vi.mock('@/components/shared/RepoTabs.vue', () => ({
17+
default: { name: 'RepoTabs', template: '<div>RepoTabs</div>' }
18+
}))
19+
vi.mock('@/components/shared/LoadingSpinner.vue', () => ({
20+
default: { name: 'LoadingSpinner', props: ['loading','text'], template: '<div v-if="loading">loading</div>' }
21+
}))
22+
23+
// Mock RepoTabStore
24+
vi.mock('@/stores/RepoTabStore', () => ({
25+
useRepoTabStore: () => ({ setRepoTab: vi.fn() })
26+
}))
27+
28+
// Mock RepoDetailStore
29+
const storeState = () => ({
30+
deployName: '', id: '', status: '', hardware: '', notebookResource: '',
31+
repositoryId: 0, deployId: 0, nickName: '', description: '', endpoint: '',
32+
modelId: '', clusterId: '', privateVisibility: false, actualReplica: 0,
33+
activeInstance: '', failedReason: '', repoType: 'notebook',
34+
initialize: vi.fn(function (data) {
35+
this.deployName = data.deploy_name || 'nb-name'
36+
this.id = String(data.id || '1')
37+
this.status = data.status || 'Running'
38+
this.hardware = data.hardware || 'GPU'
39+
this.notebookResource = data.resource_name || 'r1'
40+
this.repositoryId = data.repository_id || 1
41+
this.deployId = data.deploy_id || 2
42+
this.endpoint = data.endpoint || 'nb.endpoint'
43+
})
44+
})
45+
vi.mock('@/stores/RepoDetailStore', () => ({
46+
default: () => ({ ...storeState(), isInitialized: { value: true } })
47+
}))
48+
49+
// Mock Element Plus
50+
vi.mock('element-plus', () => ({ ElMessage: vi.fn() }))
51+
52+
// Mock utils
53+
vi.mock('@/packs/utils', () => ({ ToNotFoundPage: vi.fn() }))
54+
55+
// Mock refreshJWT
56+
vi.mock('@/packs/refreshJWT.js', () => ({ default: vi.fn() }))
57+
58+
// Mock fetch-event-source for status sse
59+
vi.mock('@microsoft/fetch-event-source', () => ({
60+
fetchEventSource: vi.fn((url, opts) => {
61+
setTimeout(() => {
62+
opts.onopen && opts.onopen({ ok: true, status: 200 })
63+
opts.onmessage && opts.onmessage({ data: JSON.stringify({ status: 'Running', details: [{ name: 'pod-1' }] }) })
64+
}, 10)
65+
return { close: vi.fn() }
66+
})
67+
}))
68+
69+
// Mock API
70+
const apiMock = vi.fn((url) => ({
71+
json: () => {
72+
if (url.startsWith('/notebooks/')) {
73+
return Promise.resolve({
74+
response: { value: { status: 200 } },
75+
data: { value: { data: {
76+
id: 1,
77+
namespace: { Path: 'tester', Type: 'user', Avatar: '' },
78+
deploy_id: 2,
79+
repository_id: 3,
80+
deploy_name: 'NB Demo',
81+
status: 'Running',
82+
hardware: 'A100',
83+
endpoint: 'nb.endpoint',
84+
resource_id: 11,
85+
order_detail_id: 101,
86+
resource_name: 'GPU-A100'
87+
} } },
88+
error: { value: null }
89+
})
90+
}
91+
return Promise.resolve({ data: { value: { data: [] } }, error: { value: null }, response: { value: { status: 200 } } })
92+
}
93+
}))
94+
vi.mock('@/packs/useFetchApi', () => ({ default: (url) => apiMock(url) }))
95+
96+
const createWrapper = async (props = {}) => {
97+
global.ENABLE_HTTPS = 'false'
98+
const wrapper = mount(NotebookDetail, {
99+
global: {
100+
provide: { csghubServer: 'http://server' }
101+
},
102+
props: { notebookId: '1', notebookName: 'nb', ...props }
103+
})
104+
await new Promise(r => setTimeout(r, 60))
105+
await wrapper.vm.$nextTick()
106+
return wrapper
107+
}
108+
109+
describe('NotebookDetail', () => {
110+
beforeEach(() => apiMock.mockClear())
111+
112+
it('mounts and fetches detail', async () => {
113+
const wrapper = await createWrapper()
114+
expect(wrapper.exists()).toBe(true)
115+
expect(apiMock).toHaveBeenCalledWith('/notebooks/1')
116+
})
117+
118+
it('connects status SSE and updates store fields', async () => {
119+
const wrapper = await createWrapper()
120+
await new Promise(r => setTimeout(r, 100))
121+
expect(wrapper.vm.repoDetailStore.status).toBe('Running')
122+
})
123+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import NotebookItem from '@/components/notebooks/NotebookItem.vue'
4+
5+
// Mock sub components used inside
6+
vi.mock('@/components/application_spaces/AppStatus.vue', () => ({
7+
default: { name: 'AppStatus', template: '<div>AppStatus</div>' }
8+
}))
9+
vi.mock('@/components/application_spaces/AppPayMode.vue', () => ({
10+
default: { name: 'AppPayMode', template: '<div>AppPayMode</div>' }
11+
}))
12+
13+
// Mock clipboard util
14+
const copySpy = vi.fn()
15+
vi.mock('@/packs/clipboard', () => ({ copyToClipboard: (...args) => copySpy(...args) }))
16+
17+
const mountItem = (notebook) => mount(NotebookItem, {
18+
global: { stubs: { SvgIcon: { template: '<div />' } } },
19+
props: { notebook }
20+
})
21+
22+
describe('NotebookItem', () => {
23+
it('renders and copies endpoint when running', async () => {
24+
const wrapper = mountItem({
25+
id: 1,
26+
deploy_name: 'nb',
27+
status: 'Running',
28+
endpoint: 'nb.endpoint',
29+
hardware: 'GPU',
30+
pay_mode: 'postpaid',
31+
runtime_framework: 'PyTorch',
32+
runtime_framework_version: '2.1',
33+
resource_name: 'A100'
34+
})
35+
36+
// copy button exists and triggers copy
37+
const copyContainer = wrapper.find('div[class*="rounded-br-xl"]')
38+
expect(copyContainer.exists()).toBe(true)
39+
await copyContainer.trigger('click')
40+
expect(copySpy).toHaveBeenCalledWith('nb.endpoint')
41+
})
42+
43+
it('detailLink navigates to notebook detail by id', async () => {
44+
const originalHref = window.location.href
45+
Object.defineProperty(window, 'location', { value: { href: '' }, writable: true })
46+
47+
const wrapper = mountItem({ id: 123, deploy_name: 'nb', status: 'Stopped' })
48+
await wrapper.trigger('click')
49+
expect(window.location.href).toBe('/notebooks/123')
50+
51+
window.location.href = originalHref
52+
})
53+
})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import NotebookLogs from '@/components/notebooks/NotebookLogs.vue'
4+
5+
// Mock RepoDetailStore with activeInstance
6+
vi.mock('@/stores/RepoDetailStore.js', () => ({
7+
default: vi.fn(() => ({
8+
activeInstance: 'pod-1',
9+
status: 'Running',
10+
isInitialized: true
11+
}))
12+
}))
13+
14+
// Mock vue3-cookies
15+
vi.mock('vue3-cookies', () => ({
16+
useCookies: () => ({ cookies: { get: vi.fn(() => 'mock-jwt') } })
17+
}))
18+
19+
// Mock refreshJWT
20+
vi.mock('@/packs/refreshJWT.js', () => ({ default: vi.fn() }))
21+
22+
// Mock CsgButton
23+
vi.mock('@/components/shared/CsgButton.vue', () => ({
24+
default: { name: 'CsgButton', template: '<button @click="$emit(\'click\')"><slot /></button>' }
25+
}))
26+
27+
// Capture created anchors for download
28+
const createElementSpy = vi.spyOn(document, 'createElement')
29+
30+
// Mock fetch-event-source
31+
const fesMock = vi.fn((url, opts) => {
32+
setTimeout(() => {
33+
opts.onopen && opts.onopen({ ok: true, status: 200 })
34+
// push a Container event log line
35+
opts.onmessage && opts.onmessage({ event: 'Container', data: 'line-1' })
36+
}, 10)
37+
return { close: vi.fn() }
38+
})
39+
vi.mock('@microsoft/fetch-event-source', () => ({ fetchEventSource: (...args) => fesMock(...args) }))
40+
41+
const mountLogs = async (props = {}) => {
42+
const wrapper = mount(NotebookLogs, {
43+
global: {
44+
provide: { csghubServer: 'http://server' }
45+
},
46+
props: { instances: [{ name: 'pod-1' }], notebookId: '1', deployId: 2, ...props }
47+
})
48+
await new Promise(r => setTimeout(r, 60))
49+
await wrapper.vm.$nextTick()
50+
return wrapper
51+
}
52+
53+
describe('NotebookLogs', () => {
54+
beforeEach(() => {
55+
fesMock.mockClear()
56+
})
57+
58+
it('connects logs SSE on mount and appends logs', async () => {
59+
const wrapper = await mountLogs()
60+
61+
// 等待组件完成初始化和异步操作
62+
await new Promise(r => setTimeout(r, 50))
63+
64+
// 验证 fetchEventSource 被调用
65+
expect(fesMock).toHaveBeenCalled()
66+
67+
// 等待 onmessage 被处理
68+
await new Promise(r => setTimeout(r, 50))
69+
70+
// 验证日志内容被正确渲染
71+
expect(wrapper.html()).toContain('line-1')
72+
})
73+
74+
// it('downloadLog generates file', async () => {
75+
// const linkMock = { href: '', click: vi.fn() }
76+
// createElementSpy.mockImplementation((tag) => {
77+
// if (tag === 'a') return linkMock
78+
// // Call the original, un-spied function
79+
// return createElementSpy.mock.original.call(document, tag)
80+
// })
81+
82+
// const wrapper = await mountLogs()
83+
// await new Promise(r => setTimeout(r, 80))
84+
85+
// // trigger download via button
86+
// const btn = wrapper.find('button')
87+
// await btn.trigger('click')
88+
// expect(linkMock.click).toHaveBeenCalled()
89+
// })
90+
})

0 commit comments

Comments
 (0)