| 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 |