import { describe, expect, test } from "vitest"; import { buildAudienceProfileCsv } from "../src/content/market/audience-profile-csv"; import type { AudienceProfileExportRow } from "../src/content/market/audience-profile-types"; describe("audience-profile-csv", () => { test("exports only requested profile distribution columns", () => { const csv = buildAudienceProfileCsv([ { profiles: { audience: { age: [{ label: "31-40", value: "50%" }], cityTier: [{ label: "一线城市", value: "100%" }], crowd: [{ label: "都市蓝领", value: "100%" }], gender: [ { label: "男性", value: "71.7%" }, { label: "女性", value: "28.3%" } ], status: "success" }, fans: { age: [{ label: "31-40", value: "40%" }], cityTier: [{ label: "一线城市", value: "80%" }], crowd: [{ label: "都市蓝领", value: "60%" }], gender: [ { label: "男性", value: "60%" }, { label: "女性", value: "40%" } ], status: "success" }, longtimeFans: { age: [{ label: "31-40", value: "30%" }], cityTier: [{ label: "一线城市", value: "70%" }], crowd: [{ label: "都市蓝领", value: "50%" }], status: "success" } }, businessAbility: { estimates: { oneToTwenty: { expectedCpe: "2.1", expectedCpm: "120.0", expectedPlay: "250w", hotRate: "100%" }, twentyToSixty: { expectedCpe: "3.7", expectedCpm: "212.0", expectedPlay: "250w", hotRate: "缺失" } }, status: "success", videos: { personalVideo: { averageComment: "4.5w", averageDuration: "150s", averageLike: "113.2w", averageShare: "26.5w", finishRate: "15.8%", interactionRate: "3.9%", medianPlay: "3738.4w", publishedItems: "<5" }, xingtuVideo: { averageComment: "5.1w", averageDuration: "170s", averageLike: "150.3w", averageShare: "68.4w", finishRate: "19.9%", interactionRate: "5.5%", medianPlay: "4059.7w", publishedItems: "<5" } } }, record: { authorId: "123", authorName: "达人 A", exportFields: { 达人信息: "达人 A", 连接用户数: "300w" }, status: "success" } } satisfies AudienceProfileExportRow ]); const [headerLine, rowLine] = csv.split("\n"); expect(headerLine).toContain("达人信息,连接用户数"); expect(headerLine).not.toContain("抓取状态"); expect(headerLine).not.toContain("失败原因"); expect(headerLine).toContain("内容数据-个人视频-播放量中位数"); expect(headerLine).toContain("内容数据-星图视频-平均转发"); expect(headerLine).toContain("效果预估-1-20s视频-预期CPM"); expect(headerLine).toContain("效果预估-20-60s视频-爆文率"); expect(headerLine).toContain("效果预估-60s以上视频-预期播放量"); expect(headerLine).not.toContain("商业能力-个人视频-播放量中位数"); expect(headerLine).not.toContain("商业能力-20-60s视频-预期CPM"); expect(headerLine).toContain("观众画像-男性占比"); expect(headerLine).toContain("粉丝画像-女性占比"); expect(headerLine).not.toContain("铁粉画像-男性占比"); expect(headerLine).toContain("观众画像-31-40占比"); expect(headerLine).toContain("粉丝画像-一线城市占比"); expect(headerLine).toContain("铁粉画像-都市蓝领占比"); expect(headerLine).not.toContain("观众画像-新一线城市占比"); expect(headerLine).not.toContain("粉丝画像-新一线城市占比"); expect(headerLine).not.toContain("铁粉画像-新一线城市占比"); expect(headerLine).not.toContain("省份"); expect(headerLine).not.toContain("地域TOP"); expect(headerLine).not.toContain("兴趣TOP"); expect(rowLine).toContain("71.7%"); expect(rowLine).toContain("60%"); expect(readCsvValue(csv, "内容数据-个人视频-播放量中位数")).toBe("3738.4w"); expect(readCsvValue(csv, "内容数据-星图视频-平均转发")).toBe("68.4w"); expect(readCsvValue(csv, "效果预估-1-20s视频-预期CPM")).toBe("120.0"); expect(readCsvValue(csv, "效果预估-20-60s视频-爆文率")).toBe("缺失"); }); test("leaves distribution cells empty when profile loading fails", () => { const csv = buildAudienceProfileCsv([ { profiles: { audience: { failureReason: "request-failed", status: "failed" }, fans: { failureReason: "timeout", status: "failed" }, longtimeFans: { status: "failed" } }, record: { authorId: "123", authorName: "达人 A", status: "success" } } satisfies AudienceProfileExportRow ]); const [, rowLine] = csv.split("\n"); expect(rowLine).not.toContain("失败"); expect(rowLine).not.toContain("request-failed"); expect(rowLine).not.toContain("timeout"); }); test("fills missing fixed distribution buckets with zero for successful profiles", () => { const csv = buildAudienceProfileCsv([ { profiles: { audience: { status: "success" }, fans: { status: "success" }, longtimeFans: { age: [ { label: "18-23", value: "11.1%" }, { label: "24-30", value: "33.3%" }, { label: "31-40", value: "55.6%" } ], cityTier: [ { label: "一线城市", value: "10%" }, { label: "二线城市", value: "20%" }, { label: "三线城市", value: "40%" }, { label: "四线城市", value: "30%" } ], crowd: [ { label: "精致妈妈", value: "30%" }, { label: "新锐白领", value: "20%" }, { label: "资深中产", value: "10%" }, { label: "都市蓝领", value: "20%" }, { label: "小镇中老年", value: "10%" }, { label: "小镇青年", value: "10%" } ], status: "success" } }, record: { authorId: "123", authorName: "达人 A", status: "success" } } satisfies AudienceProfileExportRow ]); expect(readCsvValue(csv, "铁粉画像-41-50占比")).toBe("0%"); expect(readCsvValue(csv, "铁粉画像-50+占比")).toBe("0%"); expect(readCsvValue(csv, "铁粉画像-五线城市占比")).toBe("0%"); expect(readCsvValue(csv, "铁粉画像-都市银发占比")).toBe("0%"); expect(readCsvValue(csv, "铁粉画像-Z世代占比")).toBe("0%"); expect(csv.split("\n")[0]).not.toContain("新一线城市占比"); }); }); function readCsvValue(csv: string, header: string): string { const [headerLine, rowLine] = csv.split("\n"); const headers = headerLine.split(","); const values = rowLine.split(","); const index = headers.indexOf(header); expect(index).toBeGreaterThanOrEqual(0); return values[index] ?? ""; }