Files
NianAIGC/tests/billing-price-catalog.test.ts
2026-09-23 15:46:21 +08:00

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);
});
});