diff --git a/src/content/market/business-ability-client.ts b/src/content/market/business-ability-client.ts index 6ae5e17..f11f927 100644 --- a/src/content/market/business-ability-client.ts +++ b/src/content/market/business-ability-client.ts @@ -158,19 +158,19 @@ export function mapBusinessAbilityEstimateResponse( return { oneToTwenty: { expectedCpe: formatDecimal(readNumber(data?.cpe_1_20), 1), - expectedCpm: formatDecimal(readNumber(data?.cpm_1_20), 0), + expectedCpm: formatFixedDecimal(readNumber(data?.cpm_1_20), 1), expectedPlay, hotRate }, overSixty: { expectedCpe: formatDecimal(readNumber(data?.cpe_60), 1), - expectedCpm: formatDecimal(readNumber(data?.cpm_60), 0), + expectedCpm: formatFixedDecimal(readNumber(data?.cpm_60), 1), expectedPlay, hotRate }, twentyToSixty: { expectedCpe: formatDecimal(readNumber(data?.cpe_20_60), 1), - expectedCpm: formatDecimal(readNumber(data?.cpm_20_60), 0), + expectedCpm: formatFixedDecimal(readNumber(data?.cpm_20_60), 1), expectedPlay, hotRate } @@ -203,7 +203,7 @@ function formatBasisPointRate(value: number | null): string { function formatDecimalRate(value: number | null): string { if (value === null) { - return ""; + return "缺失"; } return `${formatDecimal(value * 100, 0)}%`; @@ -230,6 +230,14 @@ function formatDecimal(value: number | null, digits: number): string { return fixed.replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1"); } +function formatFixedDecimal(value: number | null, digits: number): string { + if (value === null || !Number.isFinite(value)) { + return ""; + } + + return value.toFixed(digits); +} + function readNestedNumber( data: Record | null, objectKey: string, diff --git a/tests/audience-profile-csv.test.ts b/tests/audience-profile-csv.test.ts index 3e294a1..75cbc2c 100644 --- a/tests/audience-profile-csv.test.ts +++ b/tests/audience-profile-csv.test.ts @@ -39,15 +39,15 @@ describe("audience-profile-csv", () => { estimates: { oneToTwenty: { expectedCpe: "2.1", - expectedCpm: "120", + expectedCpm: "120.0", expectedPlay: "250w", hotRate: "100%" }, twentyToSixty: { expectedCpe: "3.7", - expectedCpm: "212", + expectedCpm: "212.0", expectedPlay: "250w", - hotRate: "100%" + hotRate: "缺失" } }, status: "success", @@ -109,8 +109,8 @@ describe("audience-profile-csv", () => { expect(rowLine).toContain("60%"); expect(readCsvValue(csv, "商业能力-个人视频-播放量中位数")).toBe("3738.4w"); expect(readCsvValue(csv, "商业能力-星图视频-平均转发")).toBe("68.4w"); - expect(readCsvValue(csv, "商业能力-1-20s视频-预期CPM")).toBe("120"); - expect(readCsvValue(csv, "商业能力-20-60s视频-爆文率")).toBe("100%"); + expect(readCsvValue(csv, "商业能力-1-20s视频-预期CPM")).toBe("120.0"); + expect(readCsvValue(csv, "商业能力-20-60s视频-爆文率")).toBe("缺失"); }); test("leaves distribution cells empty when profile loading fails", () => { diff --git a/tests/business-ability-client.test.ts b/tests/business-ability-client.test.ts index a81f6a8..7b05969 100644 --- a/tests/business-ability-client.test.ts +++ b/tests/business-ability-client.test.ts @@ -47,25 +47,57 @@ describe("business-ability-client", () => { expect(mapBusinessAbilityEstimateResponse(buildEstimatePayload())).toEqual({ oneToTwenty: { expectedCpe: "2.1", - expectedCpm: "120", + expectedCpm: "120.0", expectedPlay: "250w", hotRate: "100%" }, overSixty: { expectedCpe: "4.2", - expectedCpm: "240", + expectedCpm: "240.0", expectedPlay: "250w", hotRate: "100%" }, twentyToSixty: { expectedCpe: "3.7", - expectedCpm: "212", + expectedCpm: "212.0", expectedPlay: "250w", hotRate: "100%" } }); }); + test("keeps decimal CPM values and marks missing hot rate", () => { + expect(mapBusinessAbilityEstimateResponse({ + base_resp: { status_code: 0, status_message: "" }, + cpe_1_20: "1.6347", + cpe_20_60: "2.002", + cpe_60: "2.104", + cpm_1_20: "21.7955", + cpm_20_60: "27.5628", + cpm_60: "29.877", + vv: "1010234" + })).toEqual({ + oneToTwenty: { + expectedCpe: "1.6", + expectedCpm: "21.8", + expectedPlay: "101w", + hotRate: "缺失" + }, + overSixty: { + expectedCpe: "2.1", + expectedCpm: "29.9", + expectedPlay: "101w", + hotRate: "缺失" + }, + twentyToSixty: { + expectedCpe: "2", + expectedCpm: "27.6", + expectedPlay: "101w", + hotRate: "缺失" + } + }); + }); + test("loads personal video, Xingtu video, and duration estimate metrics", async () => { const requestedUrls: string[] = []; const fetchImpl = vi.fn(async (input: string) => { @@ -92,7 +124,7 @@ describe("business-ability-client", () => { }) ).resolves.toMatchObject({ estimates: expect.objectContaining({ - twentyToSixty: expect.objectContaining({ expectedCpm: "212" }) + twentyToSixty: expect.objectContaining({ expectedCpm: "212.0" }) }), status: "success", videos: {