fix(merge): 本地整合 - 保留主分支交互与样式并排除不安全用户删除

This commit is contained in:
ddCat committed 2026-09-07 23:02:34 +08:00
commit 14adaa1e99
100 files changed
+4845 -865

No files matched your search

+23
View File
@@ -87,6 +87,13 @@ test("channel model fetch requires explicit selection before import", async () =
expect(component).toContain("默认已全选");
expect(component).toContain("setFetchPreviewOpen(true)");
expect(component).toContain("setSelectedFetchModels(result.models)");
expect(component).toContain("已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型");
expect(component).toContain("disabled={importing || allFetchModelsSelected}");
expect(component).toContain("onClick={() => setSelectedFetchModels(fetchPreviewModels)}");
expect(component).toContain("disabled={importing || selectedFetchModels.length === 0}");
expect(component).toContain("onClick={() => setSelectedFetchModels([])}");
expect(component).toContain("取消全选");
expect(component).toContain("disabled={importing}");
expect(component).toContain("importAdminChannelModels(channel.id, selectedFetchModels)");
expect(component).toContain("disabled={!selectedFetchModels.length}");
expect(component).not.toContain("disabled: alreadyExists");
@@ -95,6 +102,22 @@ test("channel model fetch requires explicit selection before import", async () =
expect(adminCssSource).toContain("border-color: var(--control-check-fg) !important");
});
test("channel model manager supports bounded atomic batch deletion", async () => {
const [componentSource, apiSource] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/channel-model-manager.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/wallet.ts", import.meta.url)).text()]);
const component = compactSource(componentSource);
expect(apiSource).toContain("api.post(`/admin/channels/${encodeURIComponent(channelId)}/models/batch-delete`, { modelIds })");
expect(component).toContain("<AdminBatchBar count={selectedModelIds.length}");
expect(component).toContain("rowSelection:");
expect(component).toContain("selectedRowKeys: selectedModelIds");
expect(component).toContain("preserveSelectedRowKeys: true");
expect(component).toContain("setSelectedModelIds(next.slice(0, 100))");
expect(component).toContain("deleteAdminChannelModels(channel.id, selectedModelIds)");
expect(component).toContain("okButtonProps: { danger: true }");
expect(component).toContain("本次就不会删除任何模型");
expect(component).toContain("批量删除");
});
test("analytics keeps fixed range presets distinct and uses enabled channel models for pricing", async () => {
const source = compactSource(await Bun.file(new URL("../src/pages/admin/components/analytics-panel.tsx", import.meta.url)).text());
+13
View File
@@ -28,6 +28,12 @@ test("a custom login video never falls back to the built-in poster", () => {
expect(appearance.authHeroTitle).toBe("把灵感,\n变成可见的故事。");
expect(appearance.authHeroDescription).toBe("从同一个创作空间持续推进。");
expect(appearance.authVideoPosterUrl).toBe("");
expect(appearance.authVideoAutoplay).toBe(true);
});
test("login video autoplay defaults on and can be disabled explicitly", () => {
expect(normalizePublicAppearance({}).authVideoAutoplay).toBe(true);
expect(normalizePublicAppearance({ authVideoAutoplay: false }).authVideoAutoplay).toBe(false);
});
test("appearance URLs reject executable and insecure remote schemes", () => {
@@ -64,6 +70,7 @@ test("auth scene consumes resolved appearance instead of hardcoded media constan
const source = await Bun.file(new URL("../src/pages/auth/auth-scene.tsx", import.meta.url)).text();
expect(source).toContain("appearance.authVideoUrl");
expect(source).toContain("appearance.authVideoAutoplay");
expect(source).toContain("appearance.authVideoPosterUrl || undefined");
expect(source).toContain("appearance.brandName");
expect(source).toContain("appearance.authHeroTitle");
@@ -91,8 +98,14 @@ test("appearance management exposes light and dark logo uploads plus the frame s
expect(pageSource).toContain("setLogoFrameEnabled(!checked)");
expect(pageSource).not.toContain("<Checkbox");
expect(pageSource).toContain("深浅模式 Logo 预览");
expect(pageSource).toContain("登录页视频自动播放");
expect(pageSource).toContain("authVideoAutoplay");
expect(brandSource).toContain("useThemeStore");
expect(brandSource).toContain("data-logo-frame-enabled");
expect(brandSource).toContain("failedSource === source");
expect(brandSource).toContain('aria-hidden="true"');
expect(brandSource).toContain('style.visibility = "hidden"');
expect(brandSource).toContain("setFailedSource(source)");
expect(adminStyles).toContain(".admin-appearance-logo-preview-mark.is-unframed img");
expect(globalStyles).toContain('.brand-logo-frame[data-logo-frame-enabled="false"] > :is(img, svg)');
});
+2 -2
View File
@@ -5,8 +5,8 @@ import { resolve } from "node:path";
describe("assets page batch toolbar", () => {
test("places select all before cancel selection", () => {
const source = readFileSync(resolve(import.meta.dir, "../src/pages/assets/index.tsx"), "utf8");
const selectAllIndex = source.indexOf(">全选</Button>");
const clearSelectionIndex = source.indexOf(">取消选择</Button>");
const selectAllIndex = source.search(/>\s*全选\s*<\/Button>/);
const clearSelectionIndex = source.search(/>\s*取消选择\s*<\/Button>/);
expect(selectAllIndex).toBeGreaterThanOrEqual(0);
expect(clearSelectionIndex).toBeGreaterThanOrEqual(0);
+18
View File
@@ -0,0 +1,18 @@
import { readFileSync } from "node:fs";
import { describe, expect, test } from "bun:test";
const source = readFileSync(new URL("../src/pages/canvas/project.tsx", import.meta.url), "utf8");
describe("image angle editor (#432)", () => {
test("uses a centered dismissible modal instead of a node-anchored overlay", () => {
const dialog = source.slice(source.indexOf("{angleNode?.metadata?.content"), source.indexOf("{emotionNode?.metadata?.content"));
expect(dialog).toContain("<Modal");
expect(dialog).toContain("centered");
expect(dialog).toContain("onCancel={() => setAngleNodeId(null)}");
expect(dialog).toContain("onClose={() => setAngleNodeId(null)}");
expect(dialog).toContain("destroyOnHidden");
expect(dialog).not.toContain("CanvasNodePanelOverlay");
expect(dialog).not.toContain("isCanvasNodeMoving");
expect(dialog).toContain("generateAngleNode(angleNode, params)");
});
});
@@ -0,0 +1,46 @@
import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { CanvasNode } from "../src/components/canvas/canvas-node";
import { CanvasNodeType } from "../src/types/canvas";
const noop = () => {};
function renderChild(options: { selected?: boolean; opening?: boolean; closing?: boolean } = {}) {
return renderToStaticMarkup(
<CanvasNode
data={{ id: "child", type: CanvasNodeType.Image, title: "子图", position: { x: 600, y: 100 }, width: 340, height: 240, metadata: { batchRootId: "root", status: "idle" } }}
scale={1}
isSelected={Boolean(options.selected)}
isRelated={false}
isFocusRelated={false}
isConnectionTarget={false}
showImageInfo={false}
batchOpening={options.opening}
batchClosing={options.closing}
batchMotion={{ x: -566, y: -86, index: 0 }}
onMouseDown={noop}
onHoverStart={noop}
onHoverEnd={noop}
onConnectStart={noop}
onResize={noop}
onContentChange={noop}
onContextMenu={noop}
/>,
);
}
describe("图片组子图动画", () => {
test("普通挂载与选中子图都不播放展开动画", () => {
for (const selected of [false, true]) {
const markup = renderChild({ selected });
expect(markup).not.toContain("canvas-batch-child-in");
expect(markup).not.toContain("canvas-batch-child-out");
}
});
test("仅在图片组展开或收起期间播放对应动画", () => {
expect(renderChild({ opening: true })).toContain("canvas-batch-child-in");
expect(renderChild({ closing: true })).toContain("canvas-batch-child-out");
});
});
@@ -93,4 +93,16 @@ describe("canvas node generation position mentions", () => {
expect(context.prompt).toBe("让 @图片1 进入画面");
expect(context.prompt).not.toContain("@[node:");
});
test("取消引用后的提示词不再因为悬挂 @图片1 阻断生成", () => {
const target = targetNode();
const image = node("image-a", CanvasNodeType.Image, "data:image/png;base64,a");
const { applyCanvasConnectionPromptSync } = require("../src/lib/canvas/canvas-resource-references") as typeof import("../src/lib/canvas/canvas-resource-references");
const [nextTarget] = applyCanvasConnectionPromptSync([image, target], [connection(image.id)], [image, target], []).filter((item: typeof target) => item.id === target.id);
const context = buildNodeGenerationContext(nextTarget.id, [image, nextTarget], [], nextTarget.metadata?.composerContent || "", []);
expect(nextTarget.metadata?.composerContent).toBe("让 进入画面");
expect(context.referenceImages).toEqual([]);
expect(context.prompt).toBe("让 进入画面");
});
});
+43 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { buildAssetMentionReferences, buildCanvasNodeMentionReferenceMap, buildNodeMentionReferences, buildOrderedCanvasResourceReferences, canvasResourceMentionToken, collectUpstreamVideoNodes } from "../src/lib/canvas/canvas-resource-references";
import { applyCanvasConnectionPromptSync, buildAssetMentionReferences, buildCanvasNodeMentionReferenceMap, buildNodeMentionReferences, buildOrderedCanvasResourceReferences, canvasResourceMentionToken, collectUpstreamVideoNodes } from "../src/lib/canvas/canvas-resource-references";
import { canvasNodeToAsset } from "../src/lib/canvas/canvas-node-asset";
import { buildNodeGenerationInputs } from "../src/components/canvas/canvas-node-generation";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
@@ -205,3 +205,45 @@ describe("canvas resource mention slots", () => {
})).toBe("@[asset:asset-a]");
});
});
describe("remove canvas resource mention tokens", () => {
test("取消引用后会清掉对应的 @图片N", () => {
const image = imageNode("image-a");
const target = {
...videoNode("target"),
metadata: { composerContent: "@图片1 生成角色设定图:保持同一角色身份。" },
};
const previousConnections = [connection(image.id, target.id)];
const [nextTarget] = applyCanvasConnectionPromptSync([image, target], previousConnections, [image, target], []).filter((node) => node.id === target.id);
expect(nextTarget.metadata?.composerContent).toBe("生成角色设定图:保持同一角色身份。");
expect(nextTarget.metadata?.composerContent).not.toContain("@图片1");
});
test("同时存在节点 token 时一并清掉", () => {
const image = imageNode("image-a");
const target = {
...videoNode("target"),
metadata: { composerContent: "让 @图片1 和 @[node:image-a] 一起进入画面" },
};
const [nextTarget] = applyCanvasConnectionPromptSync([image, target], [connection(image.id, target.id)], [image, target], []).filter((node) => node.id === target.id);
expect(nextTarget.metadata?.composerContent).toBe("让 和 一起进入画面");
expect(nextTarget.metadata?.composerContent).not.toContain("@图片1");
expect(nextTarget.metadata?.composerContent).not.toContain("@[node:image-a]");
});
test("多图时只清被移除的那张,并把剩余引用重新编号", () => {
const imageA = imageNode("image-a");
const imageB = imageNode("image-b");
const target = {
...videoNode("target"),
metadata: { composerContent: "比较 @图片1 和 @图片2" },
};
const previousConnections = [connection(imageA.id, target.id), connection(imageB.id, target.id)];
const nextConnections = [connection(imageB.id, target.id)];
const [nextTarget] = applyCanvasConnectionPromptSync([imageA, imageB, target], previousConnections, [imageA, imageB, target], nextConnections).filter((node) => node.id === target.id);
expect(nextTarget.metadata?.composerContent).toBe("比较 和 @图片1");
});
});
+27
View File
@@ -0,0 +1,27 @@
import { readFileSync } from "node:fs";
import { describe, expect, test } from "bun:test";
const source = readFileSync(new URL("../src/components/canvas/canvas-node-content.tsx", import.meta.url), "utf8");
const textContent = source.slice(source.indexOf("function TextContent("), source.indexOf("function ", source.indexOf("function TextContent(") + 1));
describe("text node selection (#429)", () => {
test("opens the text generation panel on a node click", () => {
const project = readFileSync(new URL("../src/pages/canvas/project.tsx", import.meta.url), "utf8");
const clickHandler = project.slice(project.indexOf("const handleSelectedNodeClick ="), project.indexOf("const handleNodeBringToFront ="));
expect(clickHandler).toMatch(/node.type === CanvasNodeType.Text\)\s*\{\s*setDialogNodeId\(node.id\)/);
});
test("allows plain and rich text previews to reach node selection", () => {
const preview = textContent.split(") : richTextHTML ? (")[1];
expect(preview).toBeDefined();
expect(preview).not.toContain("onMouseDown");
expect(preview).not.toContain("onPointerDown");
expect(preview?.match(/onWheel=/g)).toHaveLength(2);
});
test("keeps editing gestures isolated from canvas dragging", () => {
const editor = textContent.split(") : richTextHTML ? (")[0];
expect(editor).toContain("onMouseDown={(event) => event.stopPropagation()}");
expect(editor).toContain("onPointerDown={(event) => event.stopPropagation()}");
});
});
+2 -2
View File
@@ -316,8 +316,8 @@ test("Create forwards owned result assets through one new canvas and the project
const canvasProject = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
expect(create).toContain('import { creationCanvasHandoffPath, creationResultAssetIds } from "@/lib/canvas/canvas-asset-handoff"');
expect(create).toContain("const resultAssetIds = result && resultUrls.length ? creationResultAssetIds(assets, { messageId: result.id, taskIds: result.taskIds || [], resultUrls }) : [];");
expect(create).toContain('const canvasHandoffPath = result ? creationCanvasHandoffPath(resultAssetIds, resultUrls.length) : "";');
expect(create).toContain("const resultAssetIds = resultUrls.length ? creationResultAssetIds(assets, { messageId: item.id, taskIds: item.taskIds || [], resultUrls }) : [];");
expect(create).toContain("const canvasHandoffPath = creationCanvasHandoffPath(resultAssetIds, resultUrls.length);");
expect(create).toContain('const canvasPath = canvasHandoffPath || "/canvas";');
expect(create).toContain('<Link to={canvasPath}>{canvasHandoffPath ? "添加到画布" : "打开画布"}</Link>');
expect(canvasIndex).toContain('const handoffMode = mode === "handoff"');
+12
View File
@@ -9,6 +9,7 @@ import {
supportsImageResolutionPresets,
} from "../src/lib/image-resolution-tiers";
import { defaultImageCapabilityConfig } from "../src/lib/model-capabilities";
import { resolveImageRequestSize } from "../src/services/api/image-validation";
const sizes = [
"1024x1024", "1360x1024", "1024x1360", "1536x1024", "1024x1536", "1024x1280", "1280x1024", "2048x878", "1824x1024", "1024x1824",
@@ -17,6 +18,17 @@ const sizes = [
];
describe("image resolution tiers", () => {
test("精确像素预设保持请求原值,比例协议发送比例", () => {
const profile = defaultImageCapabilityConfig("openai-image", "test");
profile.size = { parameter: "size", values: ["1920x1080", "3840x2160", "2160x3840", "1824x1024"], default: "1920x1080", allowCustom: false };
expect(imageResolutionChoices(profile.size.values)).toEqual(["1k", "2k", "4k"]);
expect(imageSizeForResolution(buildImageResolutionOptions(profile.size.values), "1k", "16:9")).toBe("1824x1024");
expect(resolveImageRequestSize(profile, undefined, "1920x1080")).toEqual({ parameter: "size", value: "1920x1080" });
profile.size = { parameter: "aspect_ratio", values: ["16:9"], default: "16:9", allowCustom: false };
expect(resolveImageRequestSize(profile, undefined, "16:9")).toEqual({ parameter: "aspect_ratio", value: "16:9" });
profile.size = { parameter: "size", values: [], default: "auto", allowCustom: true };
expect(() => resolveImageRequestSize(profile, undefined, "1920x1080")).toThrow("16 的倍数");
});
test("将 Xiaobaishu 的精确尺寸整理为 1K、2K、4K 各十种比例", () => {
const options = buildImageResolutionOptions(sizes);
+156
View File
@@ -0,0 +1,156 @@
import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { ImageSizePicker } from "../src/components/image-size-picker";
import { ImageSizePresetsEditor } from "../src/components/image-size-presets-editor";
import { ModelCapabilityEditor } from "../src/components/model-capability-editor";
import { defaultImageCapabilityConfig, normalizeModelCapabilityConfig } from "../src/lib/model-capabilities";
import { IMAGE_RATIOS, IMAGE_RESOLUTIONS, imagePresetForRatio, imagePresetValue, imageQualityForTier, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "../src/lib/image-size-presets";
import { buildImageResolutionOptions } from "../src/lib/image-resolution-tiers";
import { resolveImageRequestSize, validateImageSize } from "../src/services/api/image-validation";
import { buildGeminiImageGenerationConfig } from "../src/lib/gemini-image";
describe("统一图片分辨率与宽高比", () => {
test("像素协议从旧比例配置切换 4K 后发送真实像素,不回退到 1K", () => {
const profile = defaultImageCapabilityConfig();
profile.size = { parameter: "size", values: ["16:9"], default: "16:9", allowCustom: true };
const value = imagePresetValue(profile, imagePresetForRatio("4k", "16:9"));
expect(value).toBe("3840x2160");
expect(resolveImageRequestSize(profile, "auto", value)).toEqual({ parameter: "size", value: "3840x2160" });
const legacy = renderToStaticMarkup(<ImageSizePicker profile={profile} size="16:9" onChange={() => {}} />);
expect(legacy).toContain("1824 × 1024 px");
const reopened = renderToStaticMarkup(<ImageSizePicker profile={profile} size="3840x1920" onChange={() => {}} />);
expect(reopened).not.toMatch(/<button[^>]*>4K/);
expect(reopened).toContain("2:1 · 3840 × 1920 px");
});
test("常用比例保持明确尺寸,等价比例归一", () => {
expect(imagePresetForRatio("1k", "16:9").size).toBe("1824x1024");
expect(imagePresetForRatio("4k", "9:16").size).toBe("2160x3840");
expect(imagePresetForRatio("1k", "32:18")).toEqual(imagePresetForRatio("1k", "16:9"));
expect(imagePresetForRatio("2k", "7:3").ratio).toBe("21:9");
expect(imagePresetForRatio("2k", "34:22").ratio).toBe("17:11");
});
test("三档预设与任意合理比例都满足像素范围", () => {
for (const tier of IMAGE_RESOLUTIONS)
for (const ratio of [...IMAGE_RATIOS, "17:11", "3:1", "1:3", "99999:99998"]) {
const preset = imagePresetForRatio(tier, ratio);
expect(() => validateImageSize(preset.width, preset.height, false)).not.toThrow();
}
for (const ratio of ["0:9", "4:1", "1:4", "NaN:1", "1.5:1", "999999999:1"]) {
expect(() => imagePresetForRatio("1k", ratio)).toThrow();
}
});
test("管理员设置同步支持值、默认值,并保持精确尺寸", () => {
const profile = defaultImageCapabilityConfig();
profile.size = { parameter: "size", values: ["1920x1080"], default: "1920x1080", allowCustom: false };
const existing = imageSizePresets(profile);
const added = imagePresetForRatio("1k", "17:11");
profile.size = imageSizeConfigWithPresets(profile, [...existing, added]);
expect(profile.size.values).toEqual(["1920x1080", added.size]);
expect(profile.size.default).toBe("1920x1080");
expect(resolveImageRequestSize(profile, undefined, "1920x1080")?.value).toBe("1920x1080");
expect(normalizeModelCapabilityConfig({ version: 1, image: profile }).image?.size.presets).toEqual(profile.size.presets);
expect(imageSizeConfigWithPresets(profile, [added]).default).toBe(added.size);
});
test("非标准精确像素不会从选项中消失", () => {
expect(buildImageResolutionOptions(["1360x880"])[0]).toMatchObject({ size: "1360x880", ratio: "17:11" });
expect(buildImageResolutionOptions(["0x1024", "1024x0"])).toEqual([]);
});
test("比例协议分别发送比例和分辨率,不把自定义权限当作 4K 能力", () => {
const grok = defaultImageCapabilityConfig("grok-image", "grok-imagine-image");
grok.size.allowCustom = true;
expect(imageTierAvailable(grok, "4k")).toBe(false);
expect(imageSizePresets(grok).some((preset) => preset.tier === "4k")).toBe(false);
const gemini = defaultImageCapabilityConfig("gemini-image", "test");
const preset = imagePresetForRatio("4k", "16:9");
expect(buildGeminiImageGenerationConfig(imagePresetValue(gemini, preset), imageQualityForTier(gemini, "4k")).imageConfig).toEqual({ aspectRatio: "16:9", imageSize: "4K" });
gemini.size.allowCustom = true;
const reopened = renderToStaticMarkup(<ImageSizePicker profile={gemini} size="17:11" quality="high" onChange={() => {}} />);
expect(reopened).toMatch(/aria-pressed="true"[^>]*>4K/);
expect(reopened).toContain("17:11");
});
test("比例与档位的组合必须在该档位配置中,允许自定义则放开比例", () => {
const profile = defaultImageCapabilityConfig("gemini-image", "test");
profile.size = imageSizeConfigWithPresets(profile, [imagePresetForRatio("1k", "1:1"), imagePresetForRatio("2k", "16:9")]);
expect(() => resolveImageRequestSize(profile, "medium", "1:1")).toThrow("当前分辨率");
expect(resolveImageRequestSize(profile, "medium", "16:9")).toEqual({ parameter: "aspect_ratio", value: "16:9" });
profile.size.allowCustom = true;
expect(resolveImageRequestSize(profile, "medium", "1:1")?.value).toBe("1:1");
});
test("管理员固定显示三档,用户端隐藏未支持档位,不发送尺寸时隐藏", () => {
const profile = defaultImageCapabilityConfig("grok-image", "grok-imagine-image");
profile.size.allowCustom = true;
const editor = renderToStaticMarkup(<ImageSizePresetsEditor profile={profile} onChange={() => {}} />);
for (const label of ["1K", "2K", "4K"]) expect(editor).toMatch(new RegExp(`<strong[^>]*>${label}</strong>`));
const picker = renderToStaticMarkup(<ImageSizePicker profile={profile} size="16:9" quality="2k" onChange={() => {}} />);
expect(picker).not.toMatch(/<button[^>]*>4K/);
expect(picker).toContain("换算参考");
profile.size.parameter = "none";
expect(renderToStaticMarkup(<ImageSizePicker profile={profile} size="auto" onChange={() => {}} />)).toBe("");
});
test("空档位始终隐藏,允许自定义也不显示;旧尺寸不导致比例区域为空", () => {
for (const protocol of ["openai-image", "gemini-image"]) {
const profile = defaultImageCapabilityConfig(protocol, "test");
profile.size = imageSizeConfigWithPresets(profile, [imagePresetForRatio("1k", "1:1"), imagePresetForRatio("4k", "16:9")]);
for (const allowCustom of [false, true]) {
profile.size.allowCustom = allowCustom;
const picker = renderToStaticMarkup(<ImageSizePicker profile={profile} size={profile.size.parameter === "aspect_ratio" ? "16:9" : "2752x1536"} quality="medium" onChange={() => {}} />);
expect(picker).toMatch(/<button[^>]*>1K/);
expect(picker).toMatch(/<button[^>]*>4K/);
expect(picker).not.toMatch(/<button[^>]*>2K/);
expect(picker).not.toContain("未配置比例");
expect(picker).toContain('title="1:1 · 1024 × 1024"');
expect(picker).not.toMatch(/aria-pressed="true"[^>]*>1K/);
}
}
});
test("仅配置一档时只显示该档;全空时保留自动尺寸与自定义入口", () => {
const profile = defaultImageCapabilityConfig();
profile.size = imageSizeConfigWithPresets(profile, [imagePresetForRatio("4k", "16:9")]);
const picker = renderToStaticMarkup(<ImageSizePicker profile={profile} size="3840x2160" onChange={() => {}} />);
expect(picker).not.toMatch(/<button[^>]*>[12]K/);
expect(picker).toMatch(/aria-pressed="true"[^>]*>4K/);
profile.size = imageSizeConfigWithPresets(profile, []);
profile.size.allowCustom = true;
const empty = renderToStaticMarkup(<ImageSizePicker profile={profile} size="auto" onChange={() => {}} />);
expect(empty).not.toContain('class="image-size-tiers"');
expect(empty).not.toContain('class="image-size-label"');
expect(empty).toContain("自动尺寸");
expect(empty).toContain("自定义比例或尺寸");
});
test("管理员比例直接点选和输入,默认输出不再使用下拉框", () => {
const profile = defaultImageCapabilityConfig();
profile.size = imageSizeConfigWithPresets(profile, [imagePresetForRatio("1k", "16:9")]);
const editor = renderToStaticMarkup(<ImageSizePresetsEditor profile={profile} onChange={() => {}} />);
expect(editor).not.toContain('role="combobox"');
expect(editor).not.toContain("ant-select");
for (const tier of ["1K", "2K", "4K"]) {
expect(editor).toContain(`aria-label="${tier} 自定义比例"`);
expect(editor).toContain(`aria-label="添加 ${tier} 比例"`);
expect(editor).toContain(`aria-label="${tier} 16:9"`);
}
expect(editor).toContain('aria-label="默认输出"');
const readOnly = renderToStaticMarkup(<ImageSizePresetsEditor profile={profile} disabled onChange={() => {}} />);
const buttons = [...readOnly.matchAll(/<button\b[^>]*>/g)];
expect(buttons.length).toBeGreaterThan(24);
expect(buttons.every(([button]) => button.includes('disabled=""'))).toBe(true);
});
test("图片协议尺寸独占整行且先于辅助参数,视频布局不受影响", () => {
const image = renderToStaticMarkup(<ModelCapabilityEditor capability="image" section="protocol" />);
expect(image).toContain("admin-image-protocol-grid");
expect(image).toContain("admin-image-size-card");
expect(image.indexOf("尺寸参数")).toBeLessThan(image.indexOf("输出数量"));
const video = renderToStaticMarkup(<ModelCapabilityEditor capability="video" section="protocol" />);
expect(video).not.toContain("admin-image-protocol-grid");
});
});
+12
View File
@@ -0,0 +1,12 @@
import { readFileSync } from "node:fs";
import { describe, expect, test } from "bun:test";
const source = readFileSync(new URL("../src/pages/plugins/index.tsx", import.meta.url), "utf8");
describe("plugin category sections (#428)", () => {
test("limits rendered sections to the selected category, not just matching plugins", () => {
const sections = source.slice(source.indexOf("const pluginSections ="), source.indexOf("const selectCategory ="));
expect(sections).toContain('categoryFilter === "all" || section.key === categoryFilter');
expect(sections).toContain("[categoryFilter, filteredPlugins]");
});
});
@@ -155,6 +155,10 @@ describe("site appearance and editable skin library", () => {
expect(editorSource).toContain("后台菜单");
expect(globalStyles).toContain("--control-switch-checked-bg: #16a34a");
expect(globalStyles).toContain("--plugin-switch-checked-bg: var(--control-switch-checked-bg)");
expect(globalStyles).toContain("--ant-tooltip-arrow-background-color: var(--popover) !important");
expect(globalStyles).toContain("--ant-tooltip-overlay-color: var(--popover-foreground) !important");
expect(globalStyles).toContain(":where(.ant-tooltip-container, .ant-tooltip-inner)");
expect(globalStyles).toContain("color: var(--popover-foreground) !important");
expect(adminStyles).toContain("--admin-status-warning: var(--palette-status-warning)");
expect(adminStyles).toContain("border-radius: var(--menu-radius);");
});