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 { 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"); expect(singleHtml).toContain(">high"); 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"); expect(mixedHtml).toContain("legacy"); expect(mixedHtml.match(/>调整倍率<\/button>/g)).toHaveLength(2); }); });