star-chart-search-enhancer/tests/audience-profile-csv.test.ts

202 lines
7.2 KiB
TypeScript

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).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("地域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, "铁粉画像-都市银发占比")).toBe("0%");
expect(readCsvValue(csv, "铁粉画像-Z世代占比")).toBe("0%");
});
});
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] ?? "";
}