Skip to content

Commit 9ea249c

Browse files
axel7083benoitf
andauthored
feat: start instructlab session (#1849)
* feat(ui): basic start instructlab session page Signed-off-by: axel7083 <[email protected]> * feat: better new InstructLab session page Signed-off-by: axel7083 <[email protected]> * test: ensuring expected behaviour Signed-off-by: axel7083 <[email protected]> * fix: prettier Signed-off-by: axel7083 <[email protected]> * fix: apply suggestion by @benoitf Signed-off-by: axel7083 <[email protected]> * Update packages/frontend/src/pages/NewInstructLabSession.svelte Co-authored-by: Florent BENOIT <[email protected]> Signed-off-by: axel7083 <[email protected]> * fix: apply suggestion by @benoitf & adding unit tests Signed-off-by: axel7083 <[email protected]> * fix: button left-align Signed-off-by: axel7083 <[email protected]> --------- Signed-off-by: axel7083 <[email protected]> Co-authored-by: Florent BENOIT <[email protected]>
1 parent 81d72e5 commit 9ea249c

File tree

4 files changed

+474
-3
lines changed

4 files changed

+474
-3
lines changed

packages/frontend/src/App.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { ExtensionConfiguration } from '@shared/src/models/IExtensionConfig
2727
import type { Unsubscriber } from 'svelte/store';
2828
import { Messages } from '@shared/Messages';
2929
import GPUPromotion from '/@/lib/notification/GPUPromotion.svelte';
30+
import NewInstructLabSession from '/@/pages/NewInstructLabSession.svelte';
3031
3132
router.mode.hash();
3233
@@ -95,8 +96,13 @@ onDestroy(() => {
9596
</Route>
9697
{#if experimentalTuning}
9798
<!-- Tune with InstructLab -->
98-
<Route path="/tune/*">
99-
<TuneSessions />
99+
<Route path="/tune/*" firstmatch>
100+
<Route path="/start">
101+
<NewInstructLabSession />
102+
</Route>
103+
<Route path="/*">
104+
<TuneSessions />
105+
</Route>
100106
</Route>
101107
{/if}
102108
<!-- Preferences -->
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**********************************************************************
2+
* Copyright (C) 2024 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import '@testing-library/jest-dom/vitest';
20+
import { vi, test, expect, beforeEach, describe } from 'vitest';
21+
import { fireEvent, render } from '@testing-library/svelte';
22+
import type { ModelInfo } from '@shared/src/models/IModelInfo';
23+
import NewInstructLabSession from '/@/pages/NewInstructLabSession.svelte';
24+
import { writable, type Writable } from 'svelte/store';
25+
import { modelsInfo } from '/@/stores/modelsInfo';
26+
import { studioClient } from '/@/utils/client';
27+
import type { Uri } from '@shared/src/uri/Uri';
28+
import type { RenderResult } from '@testing-library/svelte';
29+
import { router } from 'tinro';
30+
31+
vi.mock('../stores/modelsInfo', async () => ({
32+
modelsInfo: {
33+
subscribe: vi.fn(),
34+
unsubscribe: vi.fn(),
35+
},
36+
}));
37+
38+
vi.mock('tinro', () => ({
39+
router: {
40+
goto: vi.fn(),
41+
},
42+
}));
43+
44+
vi.mock('../utils/client', async () => ({
45+
studioClient: {
46+
openURL: vi.fn(),
47+
openDialog: vi.fn(),
48+
},
49+
}));
50+
51+
beforeEach(() => {
52+
vi.resetAllMocks();
53+
54+
const infos: Writable<ModelInfo[]> = writable([]);
55+
vi.mocked(modelsInfo).subscribe.mockImplementation(run => infos.subscribe(run));
56+
});
57+
58+
test('empty form should have submit disabled', async () => {
59+
const { getByTitle } = render(NewInstructLabSession);
60+
61+
const submit = getByTitle('Start session');
62+
expect(submit).toBeDefined();
63+
expect(submit).toBeDisabled();
64+
});
65+
66+
test('breadcrumb click should goto sessions list', async () => {
67+
const { getByRole } = render(NewInstructLabSession);
68+
69+
const back = getByRole('link', { name: 'Back' });
70+
expect(back).toBeDefined();
71+
72+
await fireEvent.click(back);
73+
74+
expect(router.goto).toHaveBeenCalledWith('/tune');
75+
});
76+
77+
describe('radio selection', () => {
78+
test('expect knowledge radio to be selected by default', async () => {
79+
const { getByTitle } = render(NewInstructLabSession);
80+
81+
const selectKnowledgeFile = getByTitle('Select knowledge file');
82+
expect(selectKnowledgeFile).toBeDefined();
83+
expect(selectKnowledgeFile).toBeEnabled();
84+
85+
const selectSkillFile = getByTitle('Select skills file');
86+
expect(selectSkillFile).toBeDefined();
87+
expect(selectSkillFile).toBeDisabled();
88+
});
89+
90+
test('expect knowledge to be disabled if user select skills', async () => {
91+
const { getByTitle } = render(NewInstructLabSession);
92+
93+
const useSkills = getByTitle('Use Skills');
94+
expect(useSkills).toBeDefined();
95+
await fireEvent.click(useSkills);
96+
97+
const selectKnowledgeFile = getByTitle('Select knowledge file');
98+
expect(selectKnowledgeFile).toBeDefined();
99+
expect(selectKnowledgeFile).toBeDisabled();
100+
});
101+
});
102+
103+
/**
104+
* The file selection is the same for knowledge and skills so using each
105+
*/
106+
describe.each(['knowledge', 'skills'])('file selection %s', (type: string) => {
107+
/**
108+
* This function render the NewInstructLabSession with the radio expected selected (either skills or knowledge
109+
*/
110+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
111+
async function renderForm(): Promise<RenderResult<any>> {
112+
const renderResult = render(NewInstructLabSession);
113+
114+
if (type === 'skills') {
115+
const useSkills = renderResult.getByTitle('Use Skills');
116+
expect(useSkills).toBeDefined();
117+
await fireEvent.click(useSkills);
118+
}
119+
120+
return renderResult;
121+
}
122+
123+
test(`click on select ${type} should open dialog`, async () => {
124+
vi.mocked(studioClient.openDialog).mockResolvedValue([]);
125+
126+
const { getByTitle } = await renderForm();
127+
128+
const selectKnowledgeFile = getByTitle(`Select ${type} file`);
129+
expect(selectKnowledgeFile).toBeDefined();
130+
expect(selectKnowledgeFile).toBeEnabled();
131+
132+
await fireEvent.click(selectKnowledgeFile);
133+
134+
expect(studioClient.openDialog).toHaveBeenCalledWith({
135+
title: `Select ${type}`,
136+
selectors: ['openFile'],
137+
filters: [
138+
{
139+
name: 'YAML files',
140+
extensions: ['yaml', 'YAML', 'yml'],
141+
},
142+
],
143+
});
144+
});
145+
146+
test(`expect ${type} to be added on selection`, async () => {
147+
const file = '/random/folder/resource.yaml';
148+
vi.mocked(studioClient.openDialog).mockResolvedValue([
149+
{
150+
path: file,
151+
},
152+
] as Uri[]);
153+
const { getByTitle, getByText } = await renderForm();
154+
155+
const selectKnowledgeFile = getByTitle(`Select ${type} file`);
156+
await fireEvent.click(selectKnowledgeFile);
157+
158+
expect(studioClient.openDialog).toHaveBeenCalled();
159+
160+
const span = getByText(file);
161+
expect(span).toBeEnabled();
162+
});
163+
164+
test(`expect multiple ${type} to be added on multi selection`, async () => {
165+
const files = ['/random/folder/resource1.yaml', '/random/folder/resource2.yaml', '/random/folder/resource3.yaml'];
166+
vi.mocked(studioClient.openDialog).mockResolvedValue(
167+
files.map(file => ({
168+
path: file,
169+
})) as Uri[],
170+
);
171+
const { getByTitle, getByText } = await renderForm();
172+
173+
const selectKnowledgeFile = getByTitle(`Select ${type} file`);
174+
await fireEvent.click(selectKnowledgeFile);
175+
176+
expect(studioClient.openDialog).toHaveBeenCalled();
177+
178+
for (const file of files) {
179+
const span = getByText(file);
180+
expect(span).toBeDefined();
181+
}
182+
});
183+
184+
test('remove file button should remove a given file', async () => {
185+
const files = ['/random/folder/resource1.yaml', '/random/folder/resource2.yaml'];
186+
vi.mocked(studioClient.openDialog).mockResolvedValue(
187+
files.map(file => ({
188+
path: file,
189+
})) as Uri[],
190+
);
191+
const { getByTitle, queryByText } = await renderForm();
192+
193+
const selectKnowledgeFile = getByTitle(`Select ${type} file`);
194+
await fireEvent.click(selectKnowledgeFile);
195+
196+
expect(studioClient.openDialog).toHaveBeenCalled();
197+
198+
const removeBtn = getByTitle(`Remove ${files[1]}`);
199+
expect(removeBtn).toBeEnabled();
200+
if (!removeBtn) throw new Error('undefined remove btn');
201+
202+
await fireEvent.click(removeBtn);
203+
204+
await vi.waitFor(() => {
205+
expect(queryByText(files[1])).toBeNull();
206+
});
207+
});
208+
});

0 commit comments

Comments
 (0)