Files
UHD_Media_Tool/apps/web/app/lib/secret.test.ts
T
rootandClaude Opus 5.5 fa344a7184 feat(web): token 数以 K 输入显示,已存凭证显示掩码占位(FIX-8)
- 新增 UnitInput 与 kind=tokens:上下文预算、单次 token 上限、自定义模型上下文窗口以 K 输入(允许小数),存储仍为原始 token 数;校验与提示用 K 表述;列表/详情 token 数统一 K/M 简写(formatTokens)
- 新增 SecretInput 与 kind=secret:已保存时显示「已设置 ••••••••」(有服务端掩码时带末 4 位),聚焦直接输入替换,留空保持,「清除」确认后保存生效;替换模型服务、TMDB、网页搜索、下载器、媒体服务器、浏览器服务、站点账号、Passkey、本机 Token 各处
- 统一只写凭证服务端语义:省略或空串 = 不变,null = 清除(contracts secretInput);下载器/媒体服务器新增清除;站点凭证同类更新可留空保持密码/Cookie/API Key/TOTP,TOTP 可 null 清除,不再误删 TOTP 与 UA
- contextBudget 最小值 4096→4000、contextWindow 1024→1000,便于按 K 填写
- 测试:换算与校验、SecretInput 渲染与状态、各接口留空不变/显式清除

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 00:10:46 +08:00

90 lines
3.9 KiB
TypeScript

import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { SecretInput } from "~/components/console/secret-input.tsx";
import { MASK_DOTS, maskTail, SECRET_CLEAR, secretHint, secretPatch, secretPlaceholder, secretView } from "./secret.ts";
const render = (props: Partial<Parameters<typeof SecretInput>[0]>) =>
renderToStaticMarkup(
createElement(SecretInput, { id: "k", value: "", onChange: () => {}, isSet: false, label: "API Key", ...props }),
);
describe("只写凭证的表单值约定", () => {
it("表单值 → 请求值:空串不改、哨兵清除、其余去空白替换", () => {
expect(secretPatch("")).toBeUndefined();
expect(secretPatch(" ")).toBeUndefined();
expect(secretPatch(undefined)).toBeUndefined();
expect(secretPatch(true)).toBeUndefined();
expect(secretPatch(SECRET_CLEAR)).toBeNull();
expect(secretPatch(" sk-new ")).toBe("sk-new");
});
it("掩码只取服务端 hint 的末 4 位,拼不出更多", () => {
expect(maskTail("sk-l…1234")).toBe("1234");
expect(maskTail("****")).toBeNull();
expect(maskTail(null)).toBeNull();
expect(secretPlaceholder("sk-l…1234")).toBe(`已设置 ${MASK_DOTS}1234`);
expect(secretPlaceholder(null)).toBe(`已设置 ${MASK_DOTS}`);
expect(secretPlaceholder("sk-l…1234")).not.toContain("sk-l");
});
it("显示状态:空 / 已设置 / 输入中(替换)/ 待清除", () => {
expect(secretView("", { isSet: false })).toEqual({ state: "empty" });
expect(secretView("", { isSet: true, clearable: true })).toEqual({
state: "set",
placeholder: `已设置 ${MASK_DOTS}`,
canClear: true,
});
expect(secretView("abc", { isSet: true })).toEqual({ state: "typing", replacing: true });
expect(secretView("abc", { isSet: false })).toEqual({ state: "typing", replacing: false });
expect(secretView(SECRET_CLEAR, { isSet: true })).toEqual({ state: "cleared" });
});
it("说明文字随状态变", () => {
expect(secretHint("在控制台生成", "", false)).toBe("在控制台生成");
expect(secretHint("在控制台生成", "", true)).toBe("在控制台生成;留空不修改");
expect(secretHint(undefined, "", true)).toBe("留空不修改");
expect(secretHint("x", SECRET_CLEAR, true)).toBe("x");
});
});
describe("SecretInput 渲染", () => {
it("未设置:普通密码框,用调用方占位,没有清除", () => {
const html = render({ placeholder: "qbt_…" });
expect(html).toContain('type="password"');
expect(html).toContain('placeholder="qbt_…"');
expect(html).not.toContain("清除");
});
it("已设置:框里是掩码占位(不回填 value),可清除时有「清除」", () => {
const html = render({ isSet: true, mask: "sk-l…1234", clearable: true });
expect(html).toContain(`placeholder="已设置 ${MASK_DOTS}1234"`);
expect(html).toContain('value=""');
expect(html).toContain("清除");
expect(html).not.toContain("sk-l");
});
it("已设置但不可清除(必填凭证):没有「清除」", () => {
expect(render({ isSet: true })).not.toContain("清除");
});
it("输入了新值:可撤销回保持原值", () => {
const html = render({ isSet: true, value: "new-key" });
expect(html).toContain("撤销");
expect(html).toContain('aria-label="不替换API Key"');
});
it("待清除:只读框提示保存后清除,可撤销", () => {
const html = render({ isSet: true, clearable: true, value: SECRET_CLEAR });
expect(html).toContain('placeholder="保存后清除"');
expect(html).toContain("撤销");
expect(html).not.toContain(SECRET_CLEAR);
});
it("多行(Cookie):textarea,不回填", () => {
const html = render({ isSet: true, multiline: true });
expect(html).toContain("<textarea");
expect(html).toContain(`placeholder="已设置 ${MASK_DOTS}"`);
});
});