feat(canvas): 模型选择 - 支持彩色展示标签并优化目录与积分交互
This commit is contained in:
@@ -11,10 +11,12 @@ describe("channel model editor drafts", () => {
|
||||
test("new drafts select an enabled protocol and never share price state", () => {
|
||||
const first = initialChannelModelValues(null, protocols);
|
||||
first.priceTiers[0].unitPrice = 42;
|
||||
first.tags.push({ text: "限时特价", color: "purple" });
|
||||
const second = initialChannelModelValues(null, protocols);
|
||||
expect(second.protocol).toBe("text");
|
||||
expect(second.priceTiers[0].unitPrice).toBe(0);
|
||||
expect(second.modelKey).toBe("");
|
||||
expect(second.tags).toEqual([]);
|
||||
});
|
||||
test("missing catalogs do not invent a protocol", () => {
|
||||
expect(initialChannelModelValues(null, []).protocol).toBeUndefined();
|
||||
|
||||
@@ -53,6 +53,16 @@ test("grouping uses display name even when model keys differ, without merging ch
|
||||
expect(groups[0].models.map((item) => item.label)).toEqual(["秘塔(满血渠道)", "秘塔(满血渠道)"]);
|
||||
});
|
||||
|
||||
test("catalog projection keeps promotional tags scoped to each channel model", () => {
|
||||
const channels = systemChannelModelChannels([
|
||||
{ id: "a", name: "A", displayName: "A", models: [{ ...model("A", 100_000), tags: [{ text: "限时特价", color: "purple" }] }] },
|
||||
{ id: "b", name: "B", displayName: "B", models: [{ ...model("B", 200_000), tags: [{ text: "官方1折", color: "gold" }] }] },
|
||||
]);
|
||||
const config = normalizeConfigSnapshot({ config: { ...defaultConfig, channels } }).config;
|
||||
expect(config.channels[0].modelCosts![0].tags).toEqual([{ text: "限时特价", color: "purple" }]);
|
||||
expect(config.channels[1].modelCosts![0].tags).toEqual([{ text: "官方1折", color: "gold" }]);
|
||||
});
|
||||
|
||||
test("selection and quote keep the chosen channel even when another channel is cheaper", () => {
|
||||
const config = fixture();
|
||||
const value = "b::seedance-2.0";
|
||||
|
||||
@@ -56,6 +56,20 @@ test("candidate rows do not disguise missing prices as zero", () => {
|
||||
expect(markup).not.toContain("0 积分");
|
||||
});
|
||||
|
||||
test("model rows preserve multiple promotional labels and colors without altering prices", () => {
|
||||
const config = fixture();
|
||||
config.channels[0].modelCosts![0].tags = [{ text: "限时特价", color: "purple" }, { text: "官方1折", color: "gold" }];
|
||||
for (const theme of [canvasThemes.light, canvasThemes.dark]) {
|
||||
const markup = renderToStaticMarkup(<ModelLabel config={config} model={config.model} capability="video" theme={theme} creationVariant showConfiguredModelName={false} showPrice showDescription />);
|
||||
expect(markup).toContain('data-color="purple">限时特价');
|
||||
expect(markup).toContain('data-color="gold">官方1折');
|
||||
expect(markup).toContain("0.1-0.2 积分/秒");
|
||||
}
|
||||
config.channels[0].modelCosts![0].tags = [];
|
||||
const markup = renderToStaticMarkup(<ModelLabel config={config} model={config.model} capability="video" theme={canvasThemes.light} showDescription />);
|
||||
expect(markup).not.toContain('aria-label="模型标签"');
|
||||
});
|
||||
|
||||
test("the selected model and request estimate still require an exact specification match", () => {
|
||||
const config = fixture();
|
||||
const render = (value: ModelRequirements) => renderToStaticMarkup(<ModelPicker config={config} value={config.model} capability="video" requirements={value} onChange={() => {}} />);
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("模型行只保留选中高亮,价格使用独立的彩色标签", async () => {
|
||||
const [component, styles, workspace] = await Promise.all([
|
||||
Bun.file(new URL("../src/components/model-picker.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/shared/model-picker.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/workspace-product.css", import.meta.url)).text(),
|
||||
]);
|
||||
expect(component).not.toContain("previewedModel");
|
||||
expect(component).not.toContain("onMouseEnter");
|
||||
expect(styles).not.toMatch(/canvas-model-picker-(?:brand|option)(?:\[[^\]]*\])?:hover/);
|
||||
expect(workspace).not.toContain(".canvas-model-picker-brand.is-active");
|
||||
expect(styles).toContain('.canvas-model-picker-brand[aria-pressed="true"]');
|
||||
expect(styles).toContain('.canvas-model-picker-option[aria-selected="true"]');
|
||||
expect(styles).toContain(".canvas-model-picker-option:focus-visible");
|
||||
const price = styles.match(/\.model-picker-price \{([^}]+)\}/)?.[1] || "";
|
||||
expect(price).toContain("color: var(--model-price-ink)");
|
||||
expect(price).toContain("font-weight: 650");
|
||||
expect(price).not.toContain("background:");
|
||||
expect(styles).toContain(".dark .model-picker-price");
|
||||
const badge = styles.match(/\.canvas-model-picker-option \.model-picker-price \{([^}]+)\}/)?.[1] || "";
|
||||
expect(badge).toContain("border-radius: var(--r-sm)");
|
||||
expect(badge).toContain("background: color-mix");
|
||||
expect(badge).toContain("padding: 2px 5px");
|
||||
expect(price).toContain("--model-price-ink: #946900");
|
||||
expect(component).toContain('<Coins className="model-picker-price-icon" aria-hidden="true" />');
|
||||
expect(styles).toContain("width: min(800px, calc(100vw - 24px))");
|
||||
});
|
||||
|
||||
test("每次打开菜单都展开当前选中模型所属目录,无有效选中时显示一级目录", async () => {
|
||||
const component = await Bun.file(new URL("../src/components/model-picker.tsx", import.meta.url)).text();
|
||||
const opening = component.match(/const setPickerOpen = \(nextOpen: boolean\) => \{([\s\S]*?)\n \};/)?.[1] || "";
|
||||
expect(opening).toContain("setActiveGroupKey(optionGroups.find((group) => group.models.some((item) => item.models.includes(current)))?.key ?? null)");
|
||||
expect(opening).not.toContain("setActiveGroupKey(null)");
|
||||
});
|
||||
|
||||
test("选择模型保留菜单及行内焦点,仍可通过 Escape 和外部点击关闭", async () => {
|
||||
const component = await Bun.file(new URL("../src/components/model-picker.tsx", import.meta.url)).text();
|
||||
const selection = component.match(/onClick=\{\(\) => \{\s*if \(!model\) return;([\s\S]*?)\}\}/)?.[1] || "";
|
||||
expect(selection).toContain("onChange(model)");
|
||||
expect(selection).not.toContain("setOpen(false)");
|
||||
expect(selection).not.toContain("focus()");
|
||||
expect(component).toContain('event.key === "Escape"');
|
||||
expect(component).toContain('window.addEventListener("pointerdown", closeOnOutsidePointer, true)');
|
||||
});
|
||||
|
||||
test("ModelPicker 样式独立加载,并保留模型列表的视口边界", async () => {
|
||||
const [application, globals, pickerStyles] = await Promise.all([
|
||||
Bun.file(new URL("../src/application.tsx", import.meta.url)).text(),
|
||||
|
||||
Reference in New Issue
Block a user