返回 DeepSeek-Reasonix
provider-editor-compact.test.tsx
根目录 / desktop / frontend / src / __tests__ / provider-editor-compact.test.tsx
1 import { JSDOM } from 'jsdom';
2 import React, { act } from 'react';
3 import assert from 'node:assert/strict';
4 const dom = new JSDOM('<div id="root"></div>', { url: 'http://localhost', pretendToBeVisual: true });
5 Object.assign(globalThis, { window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage, Node: dom.window.Node, HTMLElement: dom.window.HTMLElement, requestAnimationFrame: dom.window.requestAnimationFrame.bind(dom.window), cancelAnimationFrame: dom.window.cancelAnimationFrame.bind(dom.window), IS_REACT_ACT_ENVIRONMENT: true });
6 window.HTMLDialogElement.prototype.showModal = function() { this.setAttribute('open',''); this.querySelector('input')?.focus(); };
7 const { createRoot } = await import('react-dom/client');
8 await import('../components/ProviderModelDialog');
9 const { ProviderEditor } = await import('../components/SettingsPanel');
10 const { LocaleProvider } = await import('../lib/i18n');
11 const root = createRoot(document.getElementById('root')!);
12 let reject = true, saved: any;
13 const provider = { name:'custom', kind:'openai', baseUrl:'https://example.com/v1', models:['model-a'], default:'model-a', apiKeyEnv:'TEST_KEY', keySet:true, added:true, builtIn:false, visionModels:[], supportedEfforts:[], modelsUrl:'', balanceUrl:'', contextWindow:1000000, modelOverrides:[{model:'model-a',reasoningProtocol:'deepseek',supportedEfforts:['high'],defaultEffort:'high',maxOutputTokens:8192}] };
14 await act(async () => root.render(<LocaleProvider><ProviderEditor initial={provider as any} kinds={['openai']} busy={false} onCancel={()=>{}} onSave={async p => { if (reject) throw new Error('save failed'); saved=p; }}/></LocaleProvider>));
15 const save = document.querySelector('.provider-editor-footer .btn--primary') as HTMLButtonElement;
16 const edit = () => (document.querySelector('.provider-model-draft__option button') as HTMLButtonElement).click();
17 async function change(input: HTMLInputElement,value:string) { await act(async()=>{Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value')!.set!.call(input,value);input.dispatchEvent(new window.Event('input',{bubbles:true}));}); }
18 async function changeSelect(select: HTMLSelectElement,value:string) { await act(async()=>{Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype,'value')!.set!.call(select,value);select.dispatchEvent(new window.Event('change',{bubbles:true}));}); }
19 const dialog = () => document.querySelector('dialog')!;
20 const submit = async () => act(async()=>{dialog().querySelector('form')!.dispatchEvent(new window.Event('submit',{bubbles:true,cancelable:true}));});
21 assert.equal(save.disabled,true);
22 await act(async()=>{edit();await new Promise(r=>setTimeout(r,30));});
23 assert.ok(dialog());
24 assert.equal(dialog().querySelector('.provider-model-dialog__id input'),null,'existing model ID is immutable');
25 assert.ok(!document.querySelector('.provider-model-draft__list')!.contains(dialog()));
26 await change(dialog().querySelector('input[type="number"]')!,'128000');
27 assert.equal(save.disabled,true,'dialog changes remain isolated until applied');
28 await act(async()=>dialog().dispatchEvent(new window.Event('cancel',{cancelable:true})));
29 assert.equal(document.querySelector('dialog'),null);
30 assert.equal(save.disabled,true,'cancel discards dialog edits');
31 await act(async()=>edit());
32 const numbers=dialog().querySelectorAll<HTMLInputElement>('input[type="number"]');
33 assert.equal(numbers[0].value,'','cancel restored inherited context');
34 await change(numbers[0],'128000');await change(numbers[1],'4096');
35 const image=dialog().querySelector<HTMLInputElement>('aside input:not(:disabled)')!;
36 await act(async()=>image.click());
37 assert.equal(dialog().querySelectorAll('aside input:disabled').length,4,'text is locked; video and PDF unavailable');
38 await changeSelect(dialog().querySelector('select')!, '');
39 await submit();
40 assert.equal(save.disabled,false);
41 assert.equal(document.querySelector('.provider-context-badge')!.textContent,'128K');
42 await act(async()=>save.click());
43 assert.equal(save.disabled,false,'failed save retains complete model draft');
44 reject=false;await act(async()=>save.click());
45 assert.equal(save.disabled,true);
46 assert.equal(saved.modelOverrides[0].contextWindow,128000);
47 assert.equal(saved.modelOverrides[0].maxOutputTokens,4096);
48 assert.equal(saved.modelOverrides[0].vision,true);
49 assert.equal(saved.modelOverrides[0].reasoningProtocol,'deepseek','preserves unrelated reasoning configuration');
50 assert.deepEqual(saved.modelOverrides[0].supportedEfforts,['high'],'automatic default preserves configured effort options');
51 assert.equal(saved.modelOverrides[0].defaultEffort,'','automatic default is stored without a model default');
52 await act(async()=>edit());
53 const highLevel = Array.from(dialog().querySelectorAll<HTMLInputElement>('.provider-model-dialog__effort-card input[type="checkbox"]')).find(input => input.getAttribute('aria-label') === 'high' || input.parentElement?.textContent?.trim() === 'high');
54 assert.ok(highLevel, 'configured effort checkbox is rendered');
55 await act(async()=>highLevel!.click());
56 await submit();await act(async()=>save.click());
57 assert.deepEqual(saved.modelOverrides[0].supportedEfforts,[],'removing the default effort clears the invalid option');
58 assert.equal(saved.modelOverrides[0].defaultEffort,'','removing the default effort falls back to automatic');
59 await act(async()=>edit());
60 await act(async()=>{
61 dialog().querySelectorAll<HTMLButtonElement>('section label button').forEach(button=>button.click());
62 (dialog().querySelector('aside > button') as HTMLButtonElement).click();
63 });
64 await submit();await act(async()=>save.click());
65 assert.equal(saved.modelOverrides[0].maxOutputTokens,0,'reset output restores inheritance');
66 assert.equal(saved.modelOverrides[0].contextWindow,0,'reset context restores inheritance');
67 assert.equal(saved.modelOverrides[0].vision,null,'automatic capability removes manual override');
68 const add=document.querySelector('.provider-model-toolbar .provider-model-draft__tools > button:nth-child(2)') as HTMLButtonElement;
69 await act(async()=>add.click());
70 await change(dialog().querySelector('.provider-model-dialog__id input')!,'model-a');await submit();
71 assert.ok(dialog().querySelector('[role="alert"]'),'duplicate IDs rejected');
72 assert.equal(dialog().querySelector('[role="alert"]')!.textContent,'This model ID is already added.');
73 await change(dialog().querySelector('.provider-model-dialog__id input')!,'Model-A');await submit();
74 assert.equal(document.querySelectorAll('.provider-model-draft__option').length,2);
75 assert.equal(document.querySelectorAll('.provider-model-draft__option input:checked').length,2,'added model enabled in draft');
76 await act(async()=>save.click());
77 assert.deepEqual(saved.models,['model-a','Model-A'],'save preserves exact model IDs');
78 // Deletion is a draft edit and clears only the exact model's state.
79 const deletionProvider = {...provider, models:['model-a','Model-A'], visionModels:['model-a','Model-A'],
80 modelCapabilities:[{model:'model-a',state:'supported',inputModalities:['text','image']},{model:'Model-A',state:'supported',inputModalities:['text','image']}],
81 modelOverrides:['model-a','Model-A'].map(model=>({...provider.modelOverrides[0],model,contextWindow:128000,vision:true}))};
82 saved=undefined;
83 await act(async()=>root.render(<LocaleProvider><ProviderEditor key="deletion" initial={deletionProvider as any} kinds={['openai']} busy={false} onCancel={()=>{}} onSave={p=>{saved=p;}}/></LocaleProvider>));
84 await act(async()=>edit());
85 await act(async()=>(dialog().querySelector('footer .btn--danger') as HTMLButtonElement).click());
86 assert.equal(document.querySelector('dialog'),null);
87 assert.equal(document.querySelectorAll('.provider-model-draft__option').length,1);
88 assert.equal(saved,undefined,'delete does not persist until the connection is saved');
89 assert.ok(document.querySelector('.provider-model-draft__option')!.textContent!.includes('Model-A'));
90 const deletionSave=document.querySelector('.provider-editor-footer .btn--primary') as HTMLButtonElement;
91 await act(async()=>deletionSave.click());
92 assert.deepEqual(saved.models,['Model-A']);
93 assert.deepEqual(saved.visionModels,['Model-A']);
94 assert.deepEqual(saved.modelOverrides.map((item:any)=>item.model),['Model-A']);
95 assert.deepEqual(saved.modelCapabilities.map((item:any)=>item.model),['Model-A']);
96 await act(async()=>(document.querySelector('.provider-model-toolbar .provider-model-draft__tools > button:nth-child(2)') as HTMLButtonElement).click());
97 assert.equal(dialog().querySelector('footer .btn--danger'),null,'add dialog has no delete action');
98 await change(dialog().querySelector('.provider-model-dialog__id input')!,'model-a');await submit();
99 await act(async()=>deletionSave.click());
100 assert.deepEqual(saved.visionModels,['Model-A'],'re-adding does not revive legacy vision');
101 const restored=saved.modelOverrides.find((item:any)=>item.model==='model-a');
102 assert.equal(restored?.contextWindow ?? 0,0);
103 assert.equal(restored?.maxOutputTokens ?? 0,0);
104 assert.equal(restored?.vision ?? null,null);
105 // Built-in capabilities are visible without turning them into user overrides.
106 const inheritedProvider = {...provider, modelOverrides: [], modelCapabilities: [{model:'model-a', state:'unknown', inputModalities:['text'], reasoning:{state:'supported', options:[{id:'high',name:'High'},{id:'max',name:'Max'}], default:'high'}}]};
107 saved=undefined;
108 await act(async()=>root.render(<LocaleProvider><ProviderEditor key="inherited" initial={inheritedProvider as any} kinds={['openai']} busy={false} onCancel={()=>{}} onSave={p=>{saved=p;}}/></LocaleProvider>));
109 await act(async()=>edit());
110 assert.equal(dialog().querySelectorAll('.provider-model-dialog__effort-card input:checked').length,2,'inherited levels are displayed');
111 await change(dialog().querySelector('input[type="number"]')!,'128000');
112 await submit();
113 await act(async()=>(document.querySelector('.provider-editor-footer .btn--primary') as HTMLButtonElement).click());
114 assert.deepEqual(saved.modelOverrides[0].supportedEfforts,[],'editing context does not freeze built-in levels');
115 assert.equal(saved.modelOverrides[0].defaultEffort,'','editing context does not freeze the default');
116 const unknownProvider = {...inheritedProvider, modelCapabilities:[{model:'model-a',state:'unknown',inputModalities:['text'],reasoning:{state:'unknown',options:[]}}]};
117 await act(async()=>root.render(<LocaleProvider><ProviderEditor key="unknown" initial={unknownProvider as any} kinds={['openai']} busy={false} onCancel={()=>{}} onSave={()=>{}}/></LocaleProvider>));
118 await act(async()=>edit());
119 assert.match(dialog().textContent!, /Reasoning levels have not been identified\. Use Automatic/,'unknown metadata has a recovery explanation');
120 await act(async()=>root.unmount());
121 console.log('PASS: unified add/edit, isolated cancel, duplicate validation, complete model overrides and failed/successful save');
122
122 lines Plain Text