返回 DeepSeek-Reasonix
provider-name-readonly.test.tsx
根目录 / desktop / frontend / src / __tests__ / provider-name-readonly.test.tsx
1 // Run: tsx src/__tests__/provider-name-readonly.test.tsx
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6 import { JSDOM } from "jsdom";
7 import React from "react";
8 import { act } from "react";
9 import { createRoot } from "react-dom/client";
10 import { ProviderEditor } from "../components/SettingsPanel";
11 import { LocaleProvider } from "../lib/i18n";
12 import type { ProviderView } from "../lib/types";
13 import { en } from "../locales/en";
14 import { zh } from "../locales/zh";
15 import { zhTW } from "../locales/zh-TW";
16
17 const here = dirname(fileURLToPath(import.meta.url));
18 const styles = readFileSync(resolve(here, "../styles.css"), "utf8");
19
20 let passed = 0;
21 let failed = 0;
22
23 function ok(value: boolean, label: string) {
24 if (value) {
25 process.stdout.write(` PASS ${label}\n`);
26 passed += 1;
27 } else {
28 process.stdout.write(` FAIL ${label}\n`);
29 failed += 1;
30 }
31 }
32
33 function eq(a: unknown, b: unknown, label: string) {
34 if (a === b) {
35 process.stdout.write(` PASS ${label}\n`);
36 passed += 1;
37 } else {
38 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
39 failed += 1;
40 }
41 }
42
43 function flushPromises(): Promise<void> {
44 return new Promise((resolve) => setTimeout(resolve, 0));
45 }
46
47 function matchingBlocks(selector: string): string[] {
48 const blocks: string[] = [];
49 const rule = /([^{}]+)\{([^{}]*)\}/g;
50 let match: RegExpExecArray | null;
51 while ((match = rule.exec(styles)) !== null) {
52 const selectors = match[1].split(",").map((part) => part.trim());
53 if (selectors.includes(selector)) blocks.push(match[2]);
54 }
55 return blocks;
56 }
57
58 function finalDeclaration(selector: string, property: string): string | undefined {
59 let value: string | undefined;
60 for (const block of matchingBlocks(selector)) {
61 const declaration = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "g");
62 let match: RegExpExecArray | null;
63 while ((match = declaration.exec(block)) !== null) {
64 value = match[1].trim();
65 }
66 }
67 return value;
68 }
69
70 const customProvider: ProviderView = {
71 name: "my-proxy",
72 builtIn: false,
73 added: true,
74 kind: "openai",
75 baseUrl: "https://example.com/v1",
76 models: ["demo"],
77 visionModels: [],
78 visionModelsConfigured: false,
79 modelsUrl: "",
80 default: "demo",
81 apiKeyEnv: "",
82 keySet: false,
83 balanceUrl: "",
84 contextWindow: 128_000,
85 reasoningProtocol: "",
86 thinking: "",
87 supportedEfforts: [],
88 defaultEffort: "",
89 };
90
91 function nameHint(root: Element): Element | null {
92 const input = root.querySelector<HTMLInputElement>('input[placeholder="e.g. my-proxy"]');
93 const next = input?.nextElementSibling;
94 return next?.classList.contains("mem-hint") ? next : null;
95 }
96
97 function renderEditor(initial?: ProviderView) {
98 return (
99 <LocaleProvider>
100 <ProviderEditor
101 key={initial?.name ?? "new-provider"}
102 initial={initial}
103 kinds={["openai"]}
104 busy={false}
105 onCancel={() => undefined}
106 onSave={() => undefined}
107 />
108 </LocaleProvider>
109 );
110 }
111
112 console.log("\nprovider name readonly");
113
114 eq(en["settings.customProviderNameReadonlyHint"], "Changing the provider name is not supported yet", "English rename hint");
115 eq(zh["settings.customProviderNameReadonlyHint"], "暂不支持供应商名称修改", "Simplified Chinese rename hint");
116 eq(zhTW["settings.customProviderNameReadonlyHint"], "暫不支援供應商名稱修改", "Traditional Chinese rename hint");
117
118 eq(finalDeclaration(".provider-name-input:disabled", "opacity"), "0.6", "disabled provider-name input is faded");
119 eq(finalDeclaration(".provider-name-input:disabled", "cursor"), "not-allowed", "disabled provider-name input uses not-allowed cursor");
120 eq(finalDeclaration(".mem-hint.provider-name-readonly-hint", "color"), "var(--fg-dim)", "readonly hint uses the stronger secondary text color");
121 eq(finalDeclaration(".mem-input:disabled", "opacity"), undefined, "no global mem-input:disabled fade rule remains");
122 eq(finalDeclaration(".mem-select:disabled", "opacity"), undefined, "no global mem-select:disabled fade rule remains");
123
124 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
125 pretendToBeVisual: true,
126 url: "http://localhost/",
127 });
128 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
129 globalThis.window = dom.window as unknown as Window & typeof globalThis;
130 globalThis.document = dom.window.document;
131 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
132 globalThis.Node = dom.window.Node;
133 globalThis.HTMLElement = dom.window.HTMLElement;
134 globalThis.Event = dom.window.Event;
135 globalThis.CustomEvent = dom.window.CustomEvent;
136 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
137 globalThis.MouseEvent = dom.window.MouseEvent;
138 globalThis.localStorage = dom.window.localStorage;
139 globalThis.sessionStorage = dom.window.sessionStorage;
140 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
141 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
142 window.scrollTo = () => {};
143
144 const rootEl = document.getElementById("root");
145 if (!rootEl) throw new Error("missing root");
146 const root = createRoot(rootEl);
147
148 await act(async () => {
149 root.render(renderEditor());
150 await flushPromises();
151 });
152 const newNameInput = rootEl.querySelector<HTMLInputElement>('input[placeholder="e.g. my-proxy"]');
153 ok(newNameInput?.disabled !== true, "new custom provider name stays editable");
154 ok(newNameInput?.classList.contains("mem-input") === true, "provider name keeps mem-input base styling");
155 ok(newNameInput?.classList.contains("provider-name-input") === true, "provider name carries the scoped provider-name-input class");
156 ok(Boolean(newNameInput?.id), "new custom provider name has a stable input id");
157 eq(rootEl.querySelector<HTMLLabelElement>(`label[for="${newNameInput?.id}"]`)?.textContent, en["settings.customProviderName"], "new custom provider name has a programmatic label");
158 eq(newNameInput?.getAttribute("aria-describedby"), null, "editable provider name omits the readonly description reference");
159 ok(nameHint(rootEl) === null, "new custom provider editor omits the rename hint");
160
161 await act(async () => {
162 root.render(renderEditor(customProvider));
163 await flushPromises();
164 });
165 const existingNameInput = rootEl.querySelector<HTMLInputElement>(".provider-name-input");
166 ok(existingNameInput?.disabled === false, "existing connection label is editable");
167 ok(existingNameInput?.classList.contains("mem-input") === true, "connection label retains base styling");
168 eq(rootEl.querySelector<HTMLLabelElement>(`label[for="${existingNameInput?.id}"]`)?.textContent, en["settings.connections.name"], "connection label has a programmatic label");
169 eq(rootEl.querySelector("details code")?.textContent, customProvider.name, "stable identity is shown separately");
170 ok(nameHint(rootEl) === null, "obsolete rename restriction is absent");
171
172 await act(async () => {
173 root.unmount();
174 });
175 dom.window.close();
176
177 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
178 if (failed > 0) process.exit(1);
179
179 lines Plain Text