202 lines
7.2 KiB
TypeScript
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] ?? "";
|
|
}
|