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

332 lines
11 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
buildAudienceProfileCsv,
listAudienceProfileSelectableFieldGroups,
listAudienceProfileCsvHeaders
} 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("新一线城市占比");
});
test("filters export columns by selected headers", () => {
const row = buildSuccessRow();
const csv = buildAudienceProfileCsv([row], {
selectedHeaders: [
"内容数据-个人视频-播放量中位数",
"观众画像-男性占比"
]
});
const [headerLine, rowLine] = csv.split("\n");
expect(headerLine).toBe(
"达人信息,连接用户数,内容数据-个人视频-播放量中位数,观众画像-男性占比"
);
expect(rowLine).toBe("达人 A,300w,3738.4w,71.7%");
expect(headerLine).not.toContain("秒思api-看后搜数");
expect(headerLine).not.toContain("粉丝画像-女性占比");
});
test("always keeps fixed id export headers when filtering", () => {
const row = buildSuccessRow({
exportFields: {
ID: "123",
: "达人 A",
: "成功",
: ""
}
});
const csv = buildAudienceProfileCsv([row], {
selectedHeaders: ["内容数据-个人视频-播放量中位数"]
});
const [headerLine, rowLine] = csv.split("\n");
expect(headerLine).toBe(
"达人ID,达人名称,导出状态,失败原因,内容数据-个人视频-播放量中位数"
);
expect(rowLine).toBe("123,达人 A,成功,,3738.4w");
});
test("lists headers for field picker defaults", () => {
expect(listAudienceProfileCsvHeaders([buildSuccessRow()])).toEqual(
expect.arrayContaining([
"达人信息",
"连接用户数",
"秒思api-看后搜数",
"内容数据-个人视频-播放量中位数",
"效果预估-20-60s视频-预期CPM",
"观众画像-男性占比",
"铁粉画像-小镇青年占比"
])
);
});
test("groups selectable profile export fields", () => {
expect(listAudienceProfileSelectableFieldGroups()).toEqual(
expect.arrayContaining([
expect.objectContaining({
headers: expect.arrayContaining(["秒思api-看后搜数"]),
label: "秒思api数据"
}),
expect.objectContaining({
headers: expect.arrayContaining(["内容数据-个人视频-播放量中位数"]),
label: "内容数据"
}),
expect.objectContaining({
headers: expect.arrayContaining(["效果预估-20-60s视频-预期CPM"]),
label: "效果预估"
}),
expect.objectContaining({
headers: expect.arrayContaining(["观众画像-男性占比"]),
label: "观众画像"
})
])
);
});
});
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] ?? "";
}
function buildSuccessRow(
overrides: Partial<AudienceProfileExportRow["record"]> = {}
): AudienceProfileExportRow {
return {
profiles: {
audience: {
age: [{ label: "31-40", value: "50%" }],
cityTier: [{ label: "一线城市", value: "100%" }],
crowd: [{ label: "都市蓝领", value: "100%" }],
gender: [{ label: "男性", value: "71.7%" }],
status: "success"
},
fans: { status: "success" },
longtimeFans: { status: "success" }
},
businessAbility: {
estimates: {
twentyToSixty: {
expectedCpe: "3.7",
expectedCpm: "212.0",
expectedPlay: "250w",
hotRate: "缺失"
}
},
status: "success",
videos: {
personalVideo: {
medianPlay: "3738.4w"
}
}
},
record: {
authorId: "123",
authorName: "达人 A",
exportFields: {
: "达人 A",
: "300w"
},
status: "success",
...overrides
}
};
}