返回 DeepSeek-Reasonix
provider-editor-save-races.test.tsx
根目录 / desktop / frontend / src / __tests__ / provider-editor-save-races.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import React, { act } from "react";
4 import type { ProviderView } from "../lib/types";
5
6 const dom = new JSDOM('<div id="root"></div>', {url:"http://localhost",pretendToBeVisual:true});
7 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});
8 const {createRoot} = await import("react-dom/client");
9 const {ProviderEditor} = await import("../components/SettingsPanel");
10 const {LocaleProvider} = await import("../lib/i18n");
11 const root = createRoot(document.getElementById("root")!);
12 const initial = {name:"custom",kind:"openai",baseUrl:"https://example.com/v1",models:["chat"],default:"chat",apiKeyEnv:"OLD_REF",keySet:true,added:true,builtIn:false,visionModels:[],supportedEfforts:[],modelsUrl:"",balanceUrl:"",contextWindow:0} as ProviderView;
13 const pending: {resolve:()=>void;reject:(e:Error)=>void;provider:ProviderView;key?:string}[]=[];
14 const onSave = (provider:ProviderView,key?:string) => new Promise<void>((resolve,reject)=>pending.push({resolve,reject,provider,key}));
15 const render = async (value=initial) => act(async()=>root.render(<LocaleProvider><ProviderEditor initial={value} kinds={["openai"]} busy={false} onCancel={()=>{}} onSave={onSave}/></LocaleProvider>));
16 const save = () => document.querySelector<HTMLButtonElement>(".provider-editor-footer .btn--primary")!;
17 const url = () => document.querySelector<HTMLInputElement>(".provider-url-input")!;
18 const key = () => document.querySelector<HTMLInputElement>('input[type="password"]')!;
19 async function change(input:HTMLInputElement,value:string) {
20 await act(async()=>{Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,"value")!.set!.call(input,value);input.dispatchEvent(new window.Event("input",{bubbles:true}));});
21 }
22 await render();
23 await change(key(),"first-key");
24 await act(async()=>save().click());
25 assert.equal(pending.length,1);
26 await change(url(),"https://new.example/v1/chat/completions");
27 await change(key(),"newer-key");
28 await act(async()=>pending[0].resolve());
29 await render({...initial,apiKeyEnv:"NEW_SERVER_OWNED_REF"});
30 assert.equal(url().value,"https://new.example/v1/chat/completions","save readback preserves a newer URL draft");
31 assert.equal(key().value,"newer-key","old success does not clear a newer key");
32 assert.equal(save().disabled,false,"newer edits still need saving");
33 await act(async()=>save().click());
34 await act(async()=>pending[1].resolve());
35 assert.equal(key().value,"","current saved key is cleared");
36 assert.equal(save().disabled,true,"only the committed draft becomes clean");
37 await change(url(),"https://failure.example/v1/chat/completions");
38 await act(async()=>save().click());
39 await act(async()=>pending[2].reject(new Error("injected persistence failure")));
40 assert.equal(save().disabled,false,"failure retains unsaved changes");
41 assert.ok(document.body.textContent?.includes("injected persistence failure"));
42 await act(async()=>save().click());
43 await change(url(),"https://latest.example/v1/chat/completions");
44 await act(async()=>save().click());
45 await act(async()=>pending[4].resolve());
46 await act(async()=>pending[3].reject(new Error("stale failure must disappear")));
47 assert.equal(save().disabled,true,"stale completion cannot dirty the newer committed draft");
48 assert.ok(!document.body.textContent?.includes("stale failure must disappear"));
49 await act(async()=>root.unmount());
50 console.log("PASS: controlled save success/failure, newer URL/key drafts, readback and out-of-order completion");
51
51 lines Plain Text