133 lines
6.1 KiB
TypeScript
133 lines
6.1 KiB
TypeScript
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import { PriceCatalog } from "@/components/billing-manager";
|
|
import { groupedPriceRuleSections, priceCatalogBulkTarget, priceTierEditTarget } from "@/lib/client/billing-price-catalog";
|
|
import type { BillingParameterDimension, BillingPriceRule } from "@/lib/types";
|
|
|
|
const size: BillingParameterDimension = {
|
|
key: "size",
|
|
label: "分辨率",
|
|
baselineValue: "1.5K",
|
|
tiers: [
|
|
{ value: "1.5K", label: "1.5K", standardFactor: 1, markupMultiplier: 1.2, enabled: true },
|
|
{ value: "2K", label: "2K", standardFactor: 2, markupMultiplier: 1.3, enabled: true }
|
|
]
|
|
};
|
|
|
|
function rule(id: string, overrides: Partial<BillingPriceRule> = {}): BillingPriceRule {
|
|
return {
|
|
id,
|
|
provider: "seedream",
|
|
capability: "image.generate",
|
|
reqKey: "doubao-seedream-5-0-pro-260628",
|
|
unit: "image",
|
|
standardUnitPriceFen: 30,
|
|
markupMultiplier: 1.2,
|
|
enabled: true,
|
|
createdAt: "2026-01-01T00:00:00Z",
|
|
updatedAt: "2026-01-01T00:00:00Z",
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
describe("billing price catalog grouped rules", () => {
|
|
it("targets the entire catalog, including disabled rules and every parameter tier", () => {
|
|
const sameModel = rule("basic", { parameterDimensions: [size] });
|
|
const disabledBusiness = rule("layers", { enabled: false, conditions: { layerDecomposition: true }, parameterDimensions: [{ ...size, tiers: [size.tiers[0]] }] });
|
|
const video = rule("video", { provider: "seedance", capability: "video.generate", reqKey: "seedance-2.0", unit: "video_second", markupMultiplier: 1.3 });
|
|
expect(priceCatalogBulkTarget([sameModel, disabledBusiness, video])).toEqual({
|
|
ruleIds: ["basic", "layers", "video"],
|
|
modelCount: 2,
|
|
tierCount: 4,
|
|
currentMultiplier: null
|
|
});
|
|
expect(priceCatalogBulkTarget([])).toEqual({ ruleIds: [], modelCount: 0, tierCount: 0, currentMultiplier: null });
|
|
});
|
|
|
|
it("only preselects the current multiplier when every rule and tier agrees", () => {
|
|
const uniformSize = { ...size, tiers: size.tiers.map((tier) => ({ ...tier, markupMultiplier: 1.2 })) };
|
|
expect(priceCatalogBulkTarget([rule("basic", { parameterDimensions: [uniformSize] }), rule("video")]).currentMultiplier).toBe(1.2);
|
|
expect(priceCatalogBulkTarget([rule("basic", { parameterDimensions: [size] })]).currentMultiplier).toBeNull();
|
|
});
|
|
|
|
it("keeps separate Seedream businesses and their resolution edit targets", () => {
|
|
const basic = rule("basic", { parameterDimensions: [size] });
|
|
const layers = rule("layers", {
|
|
standardUnitPriceFen: 15,
|
|
conditions: { layerDecomposition: true },
|
|
parameterDimensions: [size]
|
|
});
|
|
const sections = groupedPriceRuleSections([basic, layers]);
|
|
expect(sections.map(({ label, dimensions }) => [label, dimensions[0]?.key])).toEqual([
|
|
["基础生图", "size"],
|
|
["图层拆分", "size"]
|
|
]);
|
|
expect(priceTierEditTarget(sections[1].dimensions[0], size.tiers[1], sections[1].label)).toEqual({
|
|
dimensionKey: "size",
|
|
tierValue: "2K",
|
|
label: "图层拆分 · 分辨率 · 2K",
|
|
markupMultiplier: 1.3
|
|
});
|
|
|
|
expect(sections[0].rule.id).toBe("basic");
|
|
expect(sections[1].rule.id).toBe("layers");
|
|
expect(sections.map((section) => section.dimensions[0].tiers.map((tier) => tier.value))).toEqual([
|
|
["1.5K", "2K"],
|
|
["1.5K", "2K"]
|
|
]);
|
|
const html = renderToStaticMarkup(createElement(PriceCatalog, { rules: [basic, layers], onEdit: () => {}, disabled: false }));
|
|
expect(html).toContain("基础生图 · 分辨率");
|
|
expect(html).toContain("图层拆分 · 分辨率");
|
|
expect(html.match(/>调整倍率<\/button>/g)).toHaveLength(4);
|
|
expect(html).toContain("¥0.15");
|
|
expect(html).toContain("¥0.30");
|
|
});
|
|
|
|
it("keeps grouped legacy video rules as independent flat rows", () => {
|
|
const rules = [
|
|
rule("720p", { provider: "seedance", capability: "video.generate", reqKey: "seedance-2.0", variantKey: "resolution=720p", unit: "video_second", standardUnitPriceFen: 99 }),
|
|
rule("1080p", { provider: "seedance", capability: "video.generate", reqKey: "seedance-2.0", variantKey: "resolution=1080p", unit: "video_second", standardUnitPriceFen: 248 })
|
|
];
|
|
expect(groupedPriceRuleSections(rules).every((section) => section.dimensions.length === 0)).toBe(true);
|
|
expect(groupedPriceRuleSections(rules).map((section) => section.label)).toEqual(["resolution=720p", "resolution=1080p"]);
|
|
const html = renderToStaticMarkup(createElement(PriceCatalog, { rules, onEdit: () => {}, disabled: false }));
|
|
expect(html).toContain("resolution=720p");
|
|
expect(html).toContain("resolution=1080p");
|
|
expect(html.match(/>调整倍率<\/button>/g)).toHaveLength(2);
|
|
});
|
|
|
|
it("retains unnamed dimension and tier identifiers in edit labels", () => {
|
|
const dimension: BillingParameterDimension = {
|
|
key: "quality",
|
|
label: "",
|
|
baselineValue: "medium",
|
|
tiers: [{ value: "high", label: "", standardFactor: 1, markupMultiplier: 1.25, enabled: true }]
|
|
};
|
|
expect(priceTierEditTarget(dimension, dimension.tiers[0])).toEqual({
|
|
dimensionKey: "quality",
|
|
tierValue: "high",
|
|
label: "quality · high",
|
|
markupMultiplier: 1.25
|
|
});
|
|
const single = rule("evolink", {
|
|
provider: "evolink",
|
|
reqKey: "gpt-image-2",
|
|
parameterDimensions: [dimension]
|
|
});
|
|
const singleHtml = renderToStaticMarkup(createElement(PriceCatalog, { rules: [single], onEdit: () => {}, disabled: false }));
|
|
expect(singleHtml).toContain(">quality</strong>");
|
|
expect(singleHtml).toContain(">high</strong>");
|
|
expect(singleHtml.match(/>调整倍率<\/button>/g)).toHaveLength(1);
|
|
|
|
const mixedHtml = renderToStaticMarkup(createElement(PriceCatalog, {
|
|
rules: [single, rule("legacy", { provider: "evolink", reqKey: "gpt-image-2", variantKey: "legacy" })],
|
|
onEdit: () => {},
|
|
disabled: false
|
|
}));
|
|
expect(mixedHtml).toContain("规则 1 · quality</strong>");
|
|
expect(mixedHtml).toContain("legacy");
|
|
expect(mixedHtml.match(/>调整倍率<\/button>/g)).toHaveLength(2);
|
|
});
|
|
});
|