ai-progress-controls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +823 -0
- package/dist/ai-progress-controls.es.js +7191 -0
- package/dist/ai-progress-controls.es.js.map +1 -0
- package/dist/ai-progress-controls.umd.js +2 -0
- package/dist/ai-progress-controls.umd.js.map +1 -0
- package/dist/index.d.ts +2212 -0
- package/package.json +105 -0
- package/src/__tests__/setup.ts +93 -0
- package/src/core/base/AIControl.ts +230 -0
- package/src/core/base/index.ts +3 -0
- package/src/core/base/types.ts +77 -0
- package/src/core/base/utils.ts +168 -0
- package/src/core/batch-progress/BatchProgress.test.ts +458 -0
- package/src/core/batch-progress/BatchProgress.ts +760 -0
- package/src/core/batch-progress/index.ts +14 -0
- package/src/core/batch-progress/styles.ts +480 -0
- package/src/core/batch-progress/types.ts +169 -0
- package/src/core/model-loader/ModelLoader.test.ts +311 -0
- package/src/core/model-loader/ModelLoader.ts +673 -0
- package/src/core/model-loader/index.ts +2 -0
- package/src/core/model-loader/styles.ts +496 -0
- package/src/core/model-loader/types.ts +127 -0
- package/src/core/parameter-panel/ParameterPanel.test.ts +856 -0
- package/src/core/parameter-panel/ParameterPanel.ts +877 -0
- package/src/core/parameter-panel/index.ts +14 -0
- package/src/core/parameter-panel/styles.ts +323 -0
- package/src/core/parameter-panel/types.ts +278 -0
- package/src/core/parameter-slider/ParameterSlider.test.ts +299 -0
- package/src/core/parameter-slider/ParameterSlider.ts +653 -0
- package/src/core/parameter-slider/index.ts +8 -0
- package/src/core/parameter-slider/styles.ts +493 -0
- package/src/core/parameter-slider/types.ts +107 -0
- package/src/core/queue-progress/QueueProgress.test.ts +344 -0
- package/src/core/queue-progress/QueueProgress.ts +563 -0
- package/src/core/queue-progress/index.ts +5 -0
- package/src/core/queue-progress/styles.ts +469 -0
- package/src/core/queue-progress/types.ts +130 -0
- package/src/core/retry-progress/RetryProgress.test.ts +397 -0
- package/src/core/retry-progress/RetryProgress.ts +957 -0
- package/src/core/retry-progress/index.ts +6 -0
- package/src/core/retry-progress/styles.ts +530 -0
- package/src/core/retry-progress/types.ts +176 -0
- package/src/core/stream-progress/StreamProgress.test.ts +531 -0
- package/src/core/stream-progress/StreamProgress.ts +517 -0
- package/src/core/stream-progress/index.ts +2 -0
- package/src/core/stream-progress/styles.ts +349 -0
- package/src/core/stream-progress/types.ts +82 -0
- package/src/index.ts +19 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { ParameterPanel } from './ParameterPanel';
|
|
3
|
+
import type { ParameterPanelConfig, ExportedConfig } from './types';
|
|
4
|
+
import { waitForElement, waitForNextTick } from '../../__tests__/setup';
|
|
5
|
+
|
|
6
|
+
describe('ParameterPanel Component', () => {
|
|
7
|
+
let panel: ParameterPanel;
|
|
8
|
+
const defaultConfig: ParameterPanelConfig = {
|
|
9
|
+
title: 'Test Panel',
|
|
10
|
+
showPresets: true,
|
|
11
|
+
parameters: [
|
|
12
|
+
{ id: 'temperature', label: 'Temperature', min: 0, max: 2, value: 0.7, step: 0.1 },
|
|
13
|
+
{ id: 'topP', label: 'Top-P', min: 0, max: 1, value: 0.9, step: 0.05 },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
panel = new ParameterPanel(defaultConfig);
|
|
19
|
+
document.body.appendChild(panel);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (panel.parentNode) {
|
|
24
|
+
panel.parentNode.removeChild(panel);
|
|
25
|
+
}
|
|
26
|
+
localStorage.clear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('Constructor & Configuration', () => {
|
|
30
|
+
it('should create instance with required config', () => {
|
|
31
|
+
expect(panel).toBeInstanceOf(ParameterPanel);
|
|
32
|
+
expect(panel.tagName.toLowerCase()).toBe('ai-parameter-panel');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should throw error without parameters', () => {
|
|
36
|
+
expect(() => new ParameterPanel({ parameters: [] } as ParameterPanelConfig)).toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should have shadow root', async () => {
|
|
40
|
+
await waitForElement(panel);
|
|
41
|
+
expect(panel.shadowRoot).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should accept custom title', () => {
|
|
45
|
+
const customPanel = new ParameterPanel({
|
|
46
|
+
title: 'Custom Title',
|
|
47
|
+
parameters: [{ id: 'test', label: 'Test', min: 0, max: 1, value: 0.5 }],
|
|
48
|
+
});
|
|
49
|
+
document.body.appendChild(customPanel);
|
|
50
|
+
|
|
51
|
+
expect(customPanel.shadowRoot?.textContent).toContain('Custom Title');
|
|
52
|
+
customPanel.remove();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should accept layout configuration', () => {
|
|
56
|
+
const gridPanel = new ParameterPanel({
|
|
57
|
+
...defaultConfig,
|
|
58
|
+
layout: 'grid',
|
|
59
|
+
columns: 3,
|
|
60
|
+
});
|
|
61
|
+
document.body.appendChild(gridPanel);
|
|
62
|
+
|
|
63
|
+
expect(gridPanel.shadowRoot?.querySelector('.parameters-grid.layout-grid')).toBeTruthy();
|
|
64
|
+
gridPanel.remove();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('State Management', () => {
|
|
69
|
+
it('should initialize with parameter values', () => {
|
|
70
|
+
const values = panel.getAllValues();
|
|
71
|
+
expect(values.temperature).toBe(0.7);
|
|
72
|
+
expect(values.topP).toBe(0.9);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should track isDirty state', async () => {
|
|
76
|
+
await waitForElement(panel);
|
|
77
|
+
|
|
78
|
+
// Initially not dirty
|
|
79
|
+
expect(panel.shadowRoot?.querySelector('.dirty-indicator.show')).toBeFalsy();
|
|
80
|
+
|
|
81
|
+
// Set value makes it dirty
|
|
82
|
+
panel.setValue('temperature', 1.0);
|
|
83
|
+
await waitForNextTick();
|
|
84
|
+
expect(panel.shadowRoot?.querySelector('.dirty-indicator.show')).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should clear isDirty when loading preset', async () => {
|
|
88
|
+
const panelWithPreset = new ParameterPanel({
|
|
89
|
+
...defaultConfig,
|
|
90
|
+
presets: {
|
|
91
|
+
test: { name: 'Test', values: { temperature: 0.5, topP: 0.8 } },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
document.body.appendChild(panelWithPreset);
|
|
95
|
+
await waitForElement(panelWithPreset);
|
|
96
|
+
|
|
97
|
+
panelWithPreset.setValue('temperature', 1.0);
|
|
98
|
+
await waitForNextTick();
|
|
99
|
+
expect(panelWithPreset.shadowRoot?.querySelector('.dirty-indicator.show')).toBeTruthy();
|
|
100
|
+
|
|
101
|
+
panelWithPreset.loadPreset('test');
|
|
102
|
+
await waitForNextTick();
|
|
103
|
+
expect(panelWithPreset.shadowRoot?.querySelector('.dirty-indicator.show')).toBeFalsy();
|
|
104
|
+
|
|
105
|
+
panelWithPreset.remove();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Methods', () => {
|
|
110
|
+
describe('getAllValues()', () => {
|
|
111
|
+
it('should return all parameter values', () => {
|
|
112
|
+
const values = panel.getAllValues();
|
|
113
|
+
expect(Object.keys(values)).toHaveLength(2);
|
|
114
|
+
expect(values.temperature).toBeDefined();
|
|
115
|
+
expect(values.topP).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return copy of values', () => {
|
|
119
|
+
const values1 = panel.getAllValues();
|
|
120
|
+
const values2 = panel.getAllValues();
|
|
121
|
+
expect(values1).toEqual(values2);
|
|
122
|
+
expect(values1).not.toBe(values2); // Different objects
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('getValue()', () => {
|
|
127
|
+
it('should return specific parameter value', () => {
|
|
128
|
+
expect(panel.getValue('temperature')).toBe(0.7);
|
|
129
|
+
expect(panel.getValue('topP')).toBe(0.9);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return undefined for non-existent parameter', () => {
|
|
133
|
+
expect(panel.getValue('nonexistent')).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('setValue()', () => {
|
|
138
|
+
it('should update parameter value', () => {
|
|
139
|
+
panel.setValue('temperature', 1.5);
|
|
140
|
+
expect(panel.getValue('temperature')).toBe(1.5);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should clamp value to min/max range', () => {
|
|
144
|
+
panel.setValue('temperature', 5.0); // max is 2
|
|
145
|
+
expect(panel.getValue('temperature')).toBe(2.0);
|
|
146
|
+
|
|
147
|
+
panel.setValue('temperature', -1.0); // min is 0
|
|
148
|
+
expect(panel.getValue('temperature')).toBe(0.0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should emit panelchange event', async () => {
|
|
152
|
+
const changeHandler = vi.fn();
|
|
153
|
+
const emitPanel = new ParameterPanel({
|
|
154
|
+
...defaultConfig,
|
|
155
|
+
emitChangeEvents: true,
|
|
156
|
+
});
|
|
157
|
+
document.body.appendChild(emitPanel);
|
|
158
|
+
emitPanel.addEventListener('panelchange', changeHandler);
|
|
159
|
+
|
|
160
|
+
emitPanel.setValue('temperature', 1.0);
|
|
161
|
+
await waitForNextTick();
|
|
162
|
+
|
|
163
|
+
expect(changeHandler).toHaveBeenCalled();
|
|
164
|
+
const event = changeHandler.mock.calls[0][0];
|
|
165
|
+
expect(event.detail.parameterId).toBe('temperature');
|
|
166
|
+
expect(event.detail.value).toBe(1.0);
|
|
167
|
+
expect(event.detail.allValues).toEqual({ temperature: 1.0, topP: 0.9 });
|
|
168
|
+
emitPanel.remove();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should track value source', async () => {
|
|
172
|
+
const changeHandler = vi.fn();
|
|
173
|
+
const emitPanel = new ParameterPanel({
|
|
174
|
+
...defaultConfig,
|
|
175
|
+
emitChangeEvents: true,
|
|
176
|
+
});
|
|
177
|
+
document.body.appendChild(emitPanel);
|
|
178
|
+
emitPanel.addEventListener('panelchange', changeHandler);
|
|
179
|
+
|
|
180
|
+
emitPanel.setValue('temperature', 1.0, 'slider');
|
|
181
|
+
await waitForNextTick();
|
|
182
|
+
|
|
183
|
+
expect(changeHandler.mock.calls[0][0].detail.source).toBe('slider');
|
|
184
|
+
emitPanel.remove();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should clear active preset when manually changed', async () => {
|
|
188
|
+
const panelWithPreset = new ParameterPanel({
|
|
189
|
+
...defaultConfig,
|
|
190
|
+
showPresets: true,
|
|
191
|
+
presets: {
|
|
192
|
+
test: { name: 'Test', values: { temperature: 0.5, topP: 0.8 } },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
document.body.appendChild(panelWithPreset);
|
|
196
|
+
await waitForElement(panelWithPreset);
|
|
197
|
+
|
|
198
|
+
panelWithPreset.loadPreset('test');
|
|
199
|
+
await waitForNextTick();
|
|
200
|
+
expect(panelWithPreset.shadowRoot?.querySelector('.preset-btn.active')).toBeTruthy();
|
|
201
|
+
|
|
202
|
+
panelWithPreset.setValue('temperature', 1.0);
|
|
203
|
+
await waitForNextTick();
|
|
204
|
+
expect(panelWithPreset.shadowRoot?.querySelector('.preset-btn.active')).toBeFalsy();
|
|
205
|
+
|
|
206
|
+
panelWithPreset.remove();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('loadPreset()', () => {
|
|
211
|
+
let panelWithPresets: ParameterPanel;
|
|
212
|
+
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
panelWithPresets = new ParameterPanel({
|
|
215
|
+
...defaultConfig,
|
|
216
|
+
showPresets: true,
|
|
217
|
+
presets: {
|
|
218
|
+
chatgpt: {
|
|
219
|
+
name: 'ChatGPT',
|
|
220
|
+
description: 'Balanced',
|
|
221
|
+
values: { temperature: 0.7, topP: 0.9 },
|
|
222
|
+
},
|
|
223
|
+
code: {
|
|
224
|
+
name: 'Code',
|
|
225
|
+
description: 'Focused',
|
|
226
|
+
values: { temperature: 0.2, topP: 0.8 },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
document.body.appendChild(panelWithPresets);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
afterEach(() => {
|
|
234
|
+
panelWithPresets.remove();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should load preset values', async () => {
|
|
238
|
+
panelWithPresets.loadPreset('code');
|
|
239
|
+
await waitForNextTick();
|
|
240
|
+
|
|
241
|
+
expect(panelWithPresets.getValue('temperature')).toBe(0.2);
|
|
242
|
+
expect(panelWithPresets.getValue('topP')).toBe(0.8);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should set active preset', async () => {
|
|
246
|
+
await waitForElement(panelWithPresets);
|
|
247
|
+
|
|
248
|
+
panelWithPresets.loadPreset('chatgpt');
|
|
249
|
+
await waitForNextTick();
|
|
250
|
+
|
|
251
|
+
const activeBtn = panelWithPresets.shadowRoot?.querySelector('.preset-btn.active');
|
|
252
|
+
expect(activeBtn?.textContent?.trim()).toBe('ChatGPT');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should emit presetload event', async () => {
|
|
256
|
+
const loadHandler = vi.fn();
|
|
257
|
+
panelWithPresets.addEventListener('presetload', loadHandler);
|
|
258
|
+
|
|
259
|
+
panelWithPresets.loadPreset('code');
|
|
260
|
+
await waitForNextTick();
|
|
261
|
+
|
|
262
|
+
expect(loadHandler).toHaveBeenCalled();
|
|
263
|
+
const event = loadHandler.mock.calls[0][0];
|
|
264
|
+
expect(event.detail.presetId).toBe('code');
|
|
265
|
+
expect(event.detail.preset.name).toBe('Code');
|
|
266
|
+
expect(event.detail.preset.values).toEqual({ temperature: 0.2, topP: 0.8 });
|
|
267
|
+
expect(event.detail.previousValues).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should ignore non-existent preset', () => {
|
|
271
|
+
const values = panelWithPresets.getAllValues();
|
|
272
|
+
panelWithPresets.loadPreset('nonexistent');
|
|
273
|
+
|
|
274
|
+
// Values should remain unchanged
|
|
275
|
+
expect(panelWithPresets.getAllValues()).toEqual(values);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('resetAll()', () => {
|
|
280
|
+
it('should reset all parameters to defaults', () => {
|
|
281
|
+
panel.setValue('temperature', 1.5);
|
|
282
|
+
panel.setValue('topP', 0.5);
|
|
283
|
+
|
|
284
|
+
panel.resetAll();
|
|
285
|
+
|
|
286
|
+
expect(panel.getValue('temperature')).toBe(0.7);
|
|
287
|
+
expect(panel.getValue('topP')).toBe(0.9);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should clear active preset', async () => {
|
|
291
|
+
const panelWithPreset = new ParameterPanel({
|
|
292
|
+
...defaultConfig,
|
|
293
|
+
showPresets: true,
|
|
294
|
+
presets: {
|
|
295
|
+
test: { name: 'Test', values: { temperature: 0.5, topP: 0.8 } },
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
document.body.appendChild(panelWithPreset);
|
|
299
|
+
await waitForElement(panelWithPreset);
|
|
300
|
+
|
|
301
|
+
panelWithPreset.loadPreset('test');
|
|
302
|
+
await waitForNextTick();
|
|
303
|
+
|
|
304
|
+
panelWithPreset.resetAll();
|
|
305
|
+
await waitForNextTick();
|
|
306
|
+
|
|
307
|
+
expect(panelWithPreset.shadowRoot?.querySelector('.preset-btn.active')).toBeFalsy();
|
|
308
|
+
panelWithPreset.remove();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should emit panelreset event', async () => {
|
|
312
|
+
const resetHandler = vi.fn();
|
|
313
|
+
panel.addEventListener('panelreset', resetHandler);
|
|
314
|
+
|
|
315
|
+
panel.resetAll();
|
|
316
|
+
await waitForNextTick();
|
|
317
|
+
|
|
318
|
+
expect(resetHandler).toHaveBeenCalled();
|
|
319
|
+
expect(resetHandler.mock.calls[0][0].detail.previousValues).toBeDefined();
|
|
320
|
+
expect(resetHandler.mock.calls[0][0].detail.newValues).toEqual({
|
|
321
|
+
temperature: 0.7,
|
|
322
|
+
topP: 0.9,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should clear isDirty flag', async () => {
|
|
327
|
+
await waitForElement(panel);
|
|
328
|
+
|
|
329
|
+
panel.setValue('temperature', 1.0);
|
|
330
|
+
await waitForNextTick();
|
|
331
|
+
expect(panel.shadowRoot?.querySelector('.dirty-indicator.show')).toBeTruthy();
|
|
332
|
+
|
|
333
|
+
panel.resetAll();
|
|
334
|
+
await waitForNextTick();
|
|
335
|
+
expect(panel.shadowRoot?.querySelector('.dirty-indicator.show')).toBeFalsy();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('exportConfig()', () => {
|
|
340
|
+
it('should export configuration as JSON', () => {
|
|
341
|
+
const config = panel.exportConfig();
|
|
342
|
+
|
|
343
|
+
expect(config.version).toBe('1.0');
|
|
344
|
+
expect(config.parameters).toBeDefined();
|
|
345
|
+
expect(config.parameters.temperature).toBe(0.7);
|
|
346
|
+
expect(config.parameters.topP).toBe(0.9);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should include active preset in export', () => {
|
|
350
|
+
const panelWithPreset = new ParameterPanel({
|
|
351
|
+
...defaultConfig,
|
|
352
|
+
presets: {
|
|
353
|
+
test: { name: 'Test', values: { temperature: 0.5, topP: 0.8 } },
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
document.body.appendChild(panelWithPreset);
|
|
357
|
+
|
|
358
|
+
panelWithPreset.loadPreset('test');
|
|
359
|
+
const config = panelWithPreset.exportConfig();
|
|
360
|
+
|
|
361
|
+
expect(config.preset).toBe('test');
|
|
362
|
+
panelWithPreset.remove();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should include metadata', () => {
|
|
366
|
+
const config = panel.exportConfig();
|
|
367
|
+
|
|
368
|
+
expect(config.metadata).toBeDefined();
|
|
369
|
+
expect(config.metadata?.created).toBeDefined();
|
|
370
|
+
expect(config.metadata?.name).toBe('Parameters');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should emit configexport event', async () => {
|
|
374
|
+
const exportHandler = vi.fn();
|
|
375
|
+
panel.addEventListener('configexport', exportHandler);
|
|
376
|
+
|
|
377
|
+
panel.exportConfig();
|
|
378
|
+
await waitForNextTick();
|
|
379
|
+
|
|
380
|
+
expect(exportHandler).toHaveBeenCalled();
|
|
381
|
+
expect(exportHandler.mock.calls[0][0].detail.format).toBe('json');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('importConfig()', () => {
|
|
386
|
+
it('should import configuration from JSON', () => {
|
|
387
|
+
const config: ExportedConfig = {
|
|
388
|
+
version: '1.0',
|
|
389
|
+
parameters: {
|
|
390
|
+
temperature: 1.5,
|
|
391
|
+
topP: 0.5,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
panel.importConfig(config);
|
|
396
|
+
|
|
397
|
+
expect(panel.getValue('temperature')).toBe(1.5);
|
|
398
|
+
expect(panel.getValue('topP')).toBe(0.5);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should load active preset from import', async () => {
|
|
402
|
+
const panelWithPreset = new ParameterPanel({
|
|
403
|
+
...defaultConfig,
|
|
404
|
+
showPresets: true,
|
|
405
|
+
presets: {
|
|
406
|
+
test: { name: 'Test', values: { temperature: 0.5, topP: 0.8 } },
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
document.body.appendChild(panelWithPreset);
|
|
410
|
+
await waitForElement(panelWithPreset);
|
|
411
|
+
|
|
412
|
+
const config: ExportedConfig = {
|
|
413
|
+
version: '1.0',
|
|
414
|
+
preset: 'test',
|
|
415
|
+
parameters: {
|
|
416
|
+
temperature: 0.5,
|
|
417
|
+
topP: 0.8,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
panelWithPreset.importConfig(config);
|
|
422
|
+
await waitForNextTick();
|
|
423
|
+
|
|
424
|
+
expect(panelWithPreset.shadowRoot?.querySelector('.preset-btn.active')).toBeTruthy();
|
|
425
|
+
panelWithPreset.remove();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should emit configimport event', async () => {
|
|
429
|
+
const importHandler = vi.fn();
|
|
430
|
+
panel.addEventListener('configimport', importHandler);
|
|
431
|
+
|
|
432
|
+
const config: ExportedConfig = {
|
|
433
|
+
version: '1.0',
|
|
434
|
+
parameters: {
|
|
435
|
+
temperature: 1.0,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
panel.importConfig(config);
|
|
440
|
+
await waitForNextTick();
|
|
441
|
+
|
|
442
|
+
expect(importHandler).toHaveBeenCalled();
|
|
443
|
+
expect(importHandler.mock.calls[0][0].detail.config).toBeDefined();
|
|
444
|
+
expect(importHandler.mock.calls[0][0].detail.previousValues).toBeDefined();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should ignore invalid config', () => {
|
|
448
|
+
const values = panel.getAllValues();
|
|
449
|
+
panel.importConfig({} as ExportedConfig);
|
|
450
|
+
|
|
451
|
+
// Values should remain unchanged
|
|
452
|
+
expect(panel.getAllValues()).toEqual(values);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('toggleCollapse()', () => {
|
|
457
|
+
it('should toggle collapsed state', async () => {
|
|
458
|
+
const collapsiblePanel = new ParameterPanel({
|
|
459
|
+
...defaultConfig,
|
|
460
|
+
collapsible: true,
|
|
461
|
+
});
|
|
462
|
+
document.body.appendChild(collapsiblePanel);
|
|
463
|
+
await waitForElement(collapsiblePanel);
|
|
464
|
+
|
|
465
|
+
expect(collapsiblePanel.shadowRoot?.querySelector('.content.collapsed')).toBeFalsy();
|
|
466
|
+
|
|
467
|
+
collapsiblePanel.toggleCollapse();
|
|
468
|
+
await waitForNextTick();
|
|
469
|
+
|
|
470
|
+
expect(collapsiblePanel.shadowRoot?.querySelector('.content.collapsed')).toBeTruthy();
|
|
471
|
+
|
|
472
|
+
collapsiblePanel.remove();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should not toggle if not collapsible', async () => {
|
|
476
|
+
await waitForElement(panel);
|
|
477
|
+
const content = panel.shadowRoot?.querySelector('.content');
|
|
478
|
+
|
|
479
|
+
panel.toggleCollapse();
|
|
480
|
+
await waitForNextTick();
|
|
481
|
+
|
|
482
|
+
expect(content?.classList.contains('collapsed')).toBeFalsy();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('validateAll()', () => {
|
|
487
|
+
it('should validate all parameters', () => {
|
|
488
|
+
const panelWithValidation = new ParameterPanel({
|
|
489
|
+
parameters: [
|
|
490
|
+
{
|
|
491
|
+
id: 'value1',
|
|
492
|
+
label: 'Value 1',
|
|
493
|
+
min: 0,
|
|
494
|
+
max: 10,
|
|
495
|
+
value: 5,
|
|
496
|
+
validate: (val) => val > 0 || 'Must be positive',
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: 'value2',
|
|
500
|
+
label: 'Value 2',
|
|
501
|
+
min: 0,
|
|
502
|
+
max: 10,
|
|
503
|
+
value: 5,
|
|
504
|
+
validate: (val) => val < 8 || 'Must be less than 8',
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
validateOnChange: false, // Disable validation on change so setValue succeeds
|
|
508
|
+
});
|
|
509
|
+
document.body.appendChild(panelWithValidation);
|
|
510
|
+
|
|
511
|
+
expect(panelWithValidation.validateAll()).toBe(true);
|
|
512
|
+
|
|
513
|
+
panelWithValidation.setValue('value2', 9);
|
|
514
|
+
expect(panelWithValidation.validateAll()).toBe(false);
|
|
515
|
+
|
|
516
|
+
panelWithValidation.remove();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should display validation errors', async () => {
|
|
520
|
+
const panelWithValidation = new ParameterPanel({
|
|
521
|
+
parameters: [
|
|
522
|
+
{
|
|
523
|
+
id: 'value1',
|
|
524
|
+
label: 'Value 1',
|
|
525
|
+
min: 0,
|
|
526
|
+
max: 10,
|
|
527
|
+
value: 5,
|
|
528
|
+
validate: (val) => val > 0 || 'Must be positive',
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
});
|
|
532
|
+
document.body.appendChild(panelWithValidation);
|
|
533
|
+
await waitForElement(panelWithValidation);
|
|
534
|
+
|
|
535
|
+
panelWithValidation.setValue('value1', 0);
|
|
536
|
+
panelWithValidation.validateAll();
|
|
537
|
+
await waitForNextTick();
|
|
538
|
+
|
|
539
|
+
expect(
|
|
540
|
+
panelWithValidation.shadowRoot?.querySelector('.validation-errors.show')
|
|
541
|
+
).toBeTruthy();
|
|
542
|
+
expect(panelWithValidation.shadowRoot?.textContent).toContain('Must be positive');
|
|
543
|
+
|
|
544
|
+
panelWithValidation.remove();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe('addPreset() / removePreset()', () => {
|
|
549
|
+
it('should add custom preset', async () => {
|
|
550
|
+
await waitForElement(panel);
|
|
551
|
+
|
|
552
|
+
panel.addPreset('custom', 'Custom', { temperature: 1.0, topP: 0.7 });
|
|
553
|
+
await waitForNextTick();
|
|
554
|
+
|
|
555
|
+
const presetBtn = Array.from(panel.shadowRoot?.querySelectorAll('.preset-btn') || []).find(
|
|
556
|
+
(btn) => btn.textContent?.trim() === 'Custom'
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
expect(presetBtn).toBeTruthy();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should remove custom preset', async () => {
|
|
563
|
+
await waitForElement(panel);
|
|
564
|
+
|
|
565
|
+
panel.addPreset('custom', 'Custom', { temperature: 1.0, topP: 0.7 });
|
|
566
|
+
await waitForNextTick();
|
|
567
|
+
|
|
568
|
+
panel.removePreset('custom');
|
|
569
|
+
await waitForNextTick();
|
|
570
|
+
|
|
571
|
+
const presetBtn = Array.from(panel.shadowRoot?.querySelectorAll('.preset-btn') || []).find(
|
|
572
|
+
(btn) => btn.textContent?.trim() === 'Custom'
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
expect(presetBtn).toBeFalsy();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should not remove built-in preset', async () => {
|
|
579
|
+
const panelWithBuiltIn = new ParameterPanel({
|
|
580
|
+
...defaultConfig,
|
|
581
|
+
showPresets: true,
|
|
582
|
+
presets: {
|
|
583
|
+
builtin: { name: 'Built-in', values: { temperature: 0.5, topP: 0.8 }, isBuiltIn: true },
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
document.body.appendChild(panelWithBuiltIn);
|
|
587
|
+
await waitForElement(panelWithBuiltIn);
|
|
588
|
+
|
|
589
|
+
panelWithBuiltIn.removePreset('builtin');
|
|
590
|
+
await waitForNextTick();
|
|
591
|
+
|
|
592
|
+
const presetBtn = Array.from(
|
|
593
|
+
panelWithBuiltIn.shadowRoot?.querySelectorAll('.preset-btn') || []
|
|
594
|
+
).find((btn) => btn.textContent?.trim() === 'Built-in');
|
|
595
|
+
|
|
596
|
+
expect(presetBtn).toBeTruthy();
|
|
597
|
+
panelWithBuiltIn.remove();
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe('Persistence', () => {
|
|
603
|
+
it('should save values to localStorage when enabled', () => {
|
|
604
|
+
const persistPanel = new ParameterPanel({
|
|
605
|
+
...defaultConfig,
|
|
606
|
+
persistValues: true,
|
|
607
|
+
storageKey: 'test-panel',
|
|
608
|
+
});
|
|
609
|
+
document.body.appendChild(persistPanel);
|
|
610
|
+
|
|
611
|
+
persistPanel.setValue('temperature', 1.5);
|
|
612
|
+
|
|
613
|
+
const stored = localStorage.getItem('test-panel');
|
|
614
|
+
expect(stored).toBeTruthy();
|
|
615
|
+
|
|
616
|
+
const parsed = JSON.parse(stored!);
|
|
617
|
+
expect(parsed.values.temperature).toBe(1.5);
|
|
618
|
+
|
|
619
|
+
persistPanel.remove();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should load values from localStorage when enabled', () => {
|
|
623
|
+
localStorage.setItem(
|
|
624
|
+
'test-panel-load',
|
|
625
|
+
JSON.stringify({
|
|
626
|
+
values: { temperature: 1.2, topP: 0.6 },
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const persistPanel = new ParameterPanel({
|
|
631
|
+
...defaultConfig,
|
|
632
|
+
persistValues: true,
|
|
633
|
+
storageKey: 'test-panel-load',
|
|
634
|
+
});
|
|
635
|
+
document.body.appendChild(persistPanel);
|
|
636
|
+
|
|
637
|
+
expect(persistPanel.getValue('temperature')).toBe(1.2);
|
|
638
|
+
expect(persistPanel.getValue('topP')).toBe(0.6);
|
|
639
|
+
|
|
640
|
+
persistPanel.remove();
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('Events', () => {
|
|
645
|
+
it('should emit panelchange event with correct details', async () => {
|
|
646
|
+
const changeHandler = vi.fn();
|
|
647
|
+
const emitPanel = new ParameterPanel({
|
|
648
|
+
...defaultConfig,
|
|
649
|
+
emitChangeEvents: true,
|
|
650
|
+
});
|
|
651
|
+
document.body.appendChild(emitPanel);
|
|
652
|
+
emitPanel.addEventListener('panelchange', changeHandler);
|
|
653
|
+
|
|
654
|
+
emitPanel.setValue('temperature', 1.5);
|
|
655
|
+
await waitForNextTick();
|
|
656
|
+
|
|
657
|
+
expect(changeHandler).toHaveBeenCalledTimes(1);
|
|
658
|
+
const event = changeHandler.mock.calls[0][0];
|
|
659
|
+
expect(event.detail.parameterId).toBe('temperature');
|
|
660
|
+
expect(event.detail.value).toBe(1.5);
|
|
661
|
+
expect(event.detail.previousValue).toBe(0.7);
|
|
662
|
+
expect(event.detail.allValues).toEqual({ temperature: 1.5, topP: 0.9 });
|
|
663
|
+
expect(event.detail.source).toBe('slider');
|
|
664
|
+
emitPanel.remove();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should emit validationerror event when validation fails', async () => {
|
|
668
|
+
const errorHandler = vi.fn();
|
|
669
|
+
const panelWithValidation = new ParameterPanel({
|
|
670
|
+
parameters: [
|
|
671
|
+
{
|
|
672
|
+
id: 'value1',
|
|
673
|
+
label: 'Value 1',
|
|
674
|
+
min: 0,
|
|
675
|
+
max: 10,
|
|
676
|
+
value: 5,
|
|
677
|
+
validate: (val) => val > 0 || 'Must be positive',
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
validateOnChange: true,
|
|
681
|
+
});
|
|
682
|
+
document.body.appendChild(panelWithValidation);
|
|
683
|
+
panelWithValidation.addEventListener('validationerror', errorHandler);
|
|
684
|
+
|
|
685
|
+
panelWithValidation.setValue('value1', 0);
|
|
686
|
+
await waitForNextTick();
|
|
687
|
+
|
|
688
|
+
expect(errorHandler).toHaveBeenCalled();
|
|
689
|
+
const event = errorHandler.mock.calls[0][0];
|
|
690
|
+
expect(event.detail.parameterId).toBe('value1');
|
|
691
|
+
expect(event.detail.error).toBe('Must be positive');
|
|
692
|
+
|
|
693
|
+
panelWithValidation.remove();
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe('Rendering', () => {
|
|
698
|
+
it('should render all parameters', async () => {
|
|
699
|
+
await waitForElement(panel);
|
|
700
|
+
|
|
701
|
+
const wrappers = panel.shadowRoot?.querySelectorAll('.parameter-wrapper');
|
|
702
|
+
expect(wrappers?.length).toBe(2);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('should render preset buttons', async () => {
|
|
706
|
+
const panelWithPresets = new ParameterPanel({
|
|
707
|
+
...defaultConfig,
|
|
708
|
+
showPresets: true,
|
|
709
|
+
presets: {
|
|
710
|
+
preset1: { name: 'Preset 1', values: { temperature: 0.5, topP: 0.8 } },
|
|
711
|
+
preset2: { name: 'Preset 2', values: { temperature: 1.0, topP: 0.9 } },
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
document.body.appendChild(panelWithPresets);
|
|
715
|
+
await waitForElement(panelWithPresets);
|
|
716
|
+
|
|
717
|
+
const presetButtons = panelWithPresets.shadowRoot?.querySelectorAll('.preset-btn');
|
|
718
|
+
expect(presetButtons?.length).toBe(2);
|
|
719
|
+
|
|
720
|
+
panelWithPresets.remove();
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should hide presets section when showPresets is false', async () => {
|
|
724
|
+
const panelNoPresets = new ParameterPanel({
|
|
725
|
+
...defaultConfig,
|
|
726
|
+
showPresets: false,
|
|
727
|
+
presets: {
|
|
728
|
+
test: { name: 'Test', values: { temperature: 0.5, topP: 0.8 } },
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
document.body.appendChild(panelNoPresets);
|
|
732
|
+
await waitForElement(panelNoPresets);
|
|
733
|
+
|
|
734
|
+
expect(panelNoPresets.shadowRoot?.querySelector('.presets-section')).toBeFalsy();
|
|
735
|
+
|
|
736
|
+
panelNoPresets.remove();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should render action buttons', async () => {
|
|
740
|
+
const actionPanel = new ParameterPanel({
|
|
741
|
+
...defaultConfig,
|
|
742
|
+
showResetAll: true,
|
|
743
|
+
showExportImport: true,
|
|
744
|
+
});
|
|
745
|
+
document.body.appendChild(actionPanel);
|
|
746
|
+
await waitForElement(actionPanel);
|
|
747
|
+
|
|
748
|
+
expect(actionPanel.shadowRoot?.querySelector('#reset-btn')).toBeTruthy();
|
|
749
|
+
expect(actionPanel.shadowRoot?.querySelector('#export-btn')).toBeTruthy();
|
|
750
|
+
expect(actionPanel.shadowRoot?.querySelector('#import-btn')).toBeTruthy();
|
|
751
|
+
actionPanel.remove();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('should apply grid layout', async () => {
|
|
755
|
+
const gridPanel = new ParameterPanel({
|
|
756
|
+
...defaultConfig,
|
|
757
|
+
layout: 'grid',
|
|
758
|
+
columns: 2,
|
|
759
|
+
});
|
|
760
|
+
document.body.appendChild(gridPanel);
|
|
761
|
+
await waitForElement(gridPanel);
|
|
762
|
+
|
|
763
|
+
const grid = gridPanel.shadowRoot?.querySelector('.parameters-grid.layout-grid');
|
|
764
|
+
expect(grid).toBeTruthy();
|
|
765
|
+
|
|
766
|
+
gridPanel.remove();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('should apply vertical layout', async () => {
|
|
770
|
+
const verticalPanel = new ParameterPanel({
|
|
771
|
+
...defaultConfig,
|
|
772
|
+
layout: 'vertical',
|
|
773
|
+
});
|
|
774
|
+
document.body.appendChild(verticalPanel);
|
|
775
|
+
await waitForElement(verticalPanel);
|
|
776
|
+
|
|
777
|
+
const vertical = verticalPanel.shadowRoot?.querySelector('.parameters-grid.layout-vertical');
|
|
778
|
+
expect(vertical).toBeTruthy();
|
|
779
|
+
|
|
780
|
+
verticalPanel.remove();
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
describe('Accessibility', () => {
|
|
785
|
+
it('should be keyboard navigable', async () => {
|
|
786
|
+
await waitForElement(panel);
|
|
787
|
+
|
|
788
|
+
const presetBtn = panel.shadowRoot?.querySelector('.preset-btn') as HTMLElement;
|
|
789
|
+
expect(presetBtn?.tabIndex).not.toBe(-1);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('should have proper ARIA labels', async () => {
|
|
793
|
+
const ariaPanel = new ParameterPanel({
|
|
794
|
+
...defaultConfig,
|
|
795
|
+
showResetAll: true,
|
|
796
|
+
});
|
|
797
|
+
document.body.appendChild(ariaPanel);
|
|
798
|
+
await waitForElement(ariaPanel);
|
|
799
|
+
|
|
800
|
+
const resetBtn = ariaPanel.shadowRoot?.querySelector('#reset-btn');
|
|
801
|
+
expect(resetBtn?.textContent).toContain('Reset');
|
|
802
|
+
ariaPanel.remove();
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
describe('Edge Cases', () => {
|
|
807
|
+
it('should handle empty preset values', () => {
|
|
808
|
+
const panelWithEmptyPreset = new ParameterPanel({
|
|
809
|
+
...defaultConfig,
|
|
810
|
+
presets: {
|
|
811
|
+
empty: { name: 'Empty', values: {} },
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
document.body.appendChild(panelWithEmptyPreset);
|
|
815
|
+
|
|
816
|
+
const values = panelWithEmptyPreset.getAllValues();
|
|
817
|
+
panelWithEmptyPreset.loadPreset('empty');
|
|
818
|
+
|
|
819
|
+
// Values should remain unchanged
|
|
820
|
+
expect(panelWithEmptyPreset.getAllValues()).toEqual(values);
|
|
821
|
+
|
|
822
|
+
panelWithEmptyPreset.remove();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('should handle parameter with no default value', () => {
|
|
826
|
+
const panelNoDefault = new ParameterPanel({
|
|
827
|
+
parameters: [{ id: 'test', label: 'Test', min: 0, max: 10, value: 5 }],
|
|
828
|
+
});
|
|
829
|
+
document.body.appendChild(panelNoDefault);
|
|
830
|
+
|
|
831
|
+
// Should use the value property
|
|
832
|
+
expect(panelNoDefault.getValue('test')).toBe(5);
|
|
833
|
+
|
|
834
|
+
panelNoDefault.remove();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('should handle rapid value changes', async () => {
|
|
838
|
+
const changeHandler = vi.fn();
|
|
839
|
+
const rapidPanel = new ParameterPanel({
|
|
840
|
+
...defaultConfig,
|
|
841
|
+
emitChangeEvents: true,
|
|
842
|
+
});
|
|
843
|
+
document.body.appendChild(rapidPanel);
|
|
844
|
+
rapidPanel.addEventListener('panelchange', changeHandler);
|
|
845
|
+
|
|
846
|
+
rapidPanel.setValue('temperature', 0.5);
|
|
847
|
+
rapidPanel.setValue('temperature', 0.8);
|
|
848
|
+
rapidPanel.setValue('temperature', 1.2);
|
|
849
|
+
await waitForNextTick();
|
|
850
|
+
|
|
851
|
+
expect(changeHandler).toHaveBeenCalledTimes(3);
|
|
852
|
+
expect(rapidPanel.getValue('temperature')).toBe(1.2);
|
|
853
|
+
rapidPanel.remove();
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
});
|