feat: allow selecting audience export fields
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { buildAudienceProfileCsv } from "../src/content/market/audience-profile-csv";
|
||||
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", () => {
|
||||
@@ -193,6 +197,83 @@ describe("audience-profile-csv", () => {
|
||||
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 {
|
||||
@@ -204,3 +285,47 @@ function readCsvValue(csv: string, header: string): string {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("market-content-entry", () => {
|
||||
document.documentElement.removeAttribute("data-sces-market-rows");
|
||||
document.documentElement.removeAttribute("data-sces-market-request-snapshot");
|
||||
document.documentElement.removeAttribute("data-test-page-index");
|
||||
window.localStorage.clear();
|
||||
window.history.replaceState({}, "", "/");
|
||||
});
|
||||
|
||||
@@ -1689,17 +1690,22 @@ describe("market-content-entry", () => {
|
||||
{ authorType: 1, source: "fansDistribution" },
|
||||
{ authorType: 5, source: "fansDistribution" }
|
||||
]);
|
||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
||||
{
|
||||
profiles: {
|
||||
audience: expect.objectContaining({ status: "success" }),
|
||||
fans: expect.objectContaining({ status: "success" }),
|
||||
longtimeFans: expect.objectContaining({ status: "success" })
|
||||
},
|
||||
businessAbility: expect.objectContaining({ status: "success" }),
|
||||
record: expect.objectContaining({ authorId: "222" })
|
||||
}
|
||||
]);
|
||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
profiles: {
|
||||
audience: expect.objectContaining({ status: "success" }),
|
||||
fans: expect.objectContaining({ status: "success" }),
|
||||
longtimeFans: expect.objectContaining({ status: "success" })
|
||||
},
|
||||
businessAbility: expect.objectContaining({ status: "success" }),
|
||||
record: expect.objectContaining({ authorId: "222" })
|
||||
}
|
||||
],
|
||||
expect.objectContaining({
|
||||
selectedHeaders: expect.arrayContaining(["秒思api-看后搜数"])
|
||||
})
|
||||
);
|
||||
expect(onCsvReady).toHaveBeenCalledWith(
|
||||
"profile-csv",
|
||||
expect.stringMatching(/^达人连接用户画像_\d{8}_\d{4}\.csv$/)
|
||||
@@ -1790,56 +1796,149 @@ describe("market-content-entry", () => {
|
||||
"6866044569306267651",
|
||||
"7040323176106033165"
|
||||
]);
|
||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
record: expect.objectContaining({
|
||||
authorId: "6866044569306267651",
|
||||
authorName: "小九儿",
|
||||
backendMetrics: expect.objectContaining({
|
||||
a3IncreaseCount: "100",
|
||||
afterViewSearchCount: "300",
|
||||
afterViewSearchRate: "1.1%",
|
||||
cpSearch: "10",
|
||||
cpa3: "30",
|
||||
newA3Rate: "3.3%"
|
||||
}),
|
||||
exportFields: {
|
||||
达人ID: "6866044569306267651",
|
||||
达人名称: "小九儿",
|
||||
导出状态: "成功",
|
||||
失败原因: ""
|
||||
},
|
||||
rates: {
|
||||
personalVideoAfterSearchRate: "12.3%",
|
||||
singleVideoAfterSearchRate: "7.8%"
|
||||
}
|
||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
record: expect.objectContaining({
|
||||
authorId: "6866044569306267651",
|
||||
authorName: "小九儿",
|
||||
backendMetrics: expect.objectContaining({
|
||||
a3IncreaseCount: "100",
|
||||
afterViewSearchCount: "300",
|
||||
afterViewSearchRate: "1.1%",
|
||||
cpSearch: "10",
|
||||
cpa3: "30",
|
||||
newA3Rate: "3.3%"
|
||||
}),
|
||||
exportFields: {
|
||||
达人ID: "6866044569306267651",
|
||||
达人名称: "小九儿",
|
||||
导出状态: "成功",
|
||||
失败原因: ""
|
||||
},
|
||||
rates: {
|
||||
personalVideoAfterSearchRate: "12.3%",
|
||||
singleVideoAfterSearchRate: "7.8%"
|
||||
}
|
||||
})
|
||||
}),
|
||||
expect.objectContaining({
|
||||
record: expect.objectContaining({
|
||||
authorId: "7040323176106033165",
|
||||
authorName: "达人 B",
|
||||
backendMetrics: expect.objectContaining({
|
||||
a3IncreaseCount: "200",
|
||||
afterViewSearchCount: "400",
|
||||
afterViewSearchRate: "2.2%",
|
||||
cpSearch: "20",
|
||||
cpa3: "40",
|
||||
newA3Rate: "4.4%"
|
||||
}),
|
||||
rates: {
|
||||
personalVideoAfterSearchRate: "45.6%",
|
||||
singleVideoAfterSearchRate: "9.1%"
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
],
|
||||
expect.objectContaining({
|
||||
record: expect.objectContaining({
|
||||
authorId: "7040323176106033165",
|
||||
authorName: "达人 B",
|
||||
backendMetrics: expect.objectContaining({
|
||||
a3IncreaseCount: "200",
|
||||
afterViewSearchCount: "400",
|
||||
afterViewSearchRate: "2.2%",
|
||||
cpSearch: "20",
|
||||
cpa3: "40",
|
||||
newA3Rate: "4.4%"
|
||||
}),
|
||||
rates: {
|
||||
personalVideoAfterSearchRate: "45.6%",
|
||||
singleVideoAfterSearchRate: "9.1%"
|
||||
}
|
||||
})
|
||||
selectedHeaders: expect.arrayContaining(["秒思api-看后搜数"])
|
||||
})
|
||||
]);
|
||||
);
|
||||
expect(onCsvReady).toHaveBeenCalledWith(
|
||||
"profile-csv",
|
||||
expect.stringMatching(/^达人连接用户画像_按ID导出_\d{8}_\d{4}\.csv$/)
|
||||
);
|
||||
});
|
||||
|
||||
test("audience profile export uses persisted selected csv fields", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||
]);
|
||||
window.localStorage.setItem(
|
||||
"sces:audience-profile:selectedHeaders",
|
||||
JSON.stringify(["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"])
|
||||
);
|
||||
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||
const loadBusinessAbility = vi.fn(async () => ({
|
||||
estimates: {},
|
||||
status: "success" as const,
|
||||
videos: {}
|
||||
}));
|
||||
const loadAudienceProfile = vi.fn(async () => ({
|
||||
age: [],
|
||||
crowd: [],
|
||||
cityTier: [],
|
||||
gender: [],
|
||||
status: "success" as const
|
||||
}));
|
||||
const onCsvReady = vi.fn();
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
buildAudienceProfileCsv,
|
||||
document,
|
||||
loadBusinessAbility,
|
||||
loadAudienceProfile,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
onCsvReady,
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
clickSelectionCheckboxForAuthor("111");
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
|
||||
click('[data-plugin-export-audience-profile="button"]');
|
||||
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||
|
||||
expect(buildAudienceProfileCsv.mock.calls[0][1]).toEqual({
|
||||
selectedHeaders: ["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"]
|
||||
});
|
||||
});
|
||||
|
||||
test("audience profile field picker persists the next selected fields", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||
]);
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
document,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
click('[data-plugin-audience-profile-fields="button"]');
|
||||
|
||||
const afterSearchCountInput = document.querySelector(
|
||||
'input[data-audience-profile-field-dialog-field="checkbox"][value="秒思api-看后搜数"]'
|
||||
) as HTMLInputElement | null;
|
||||
expect(afterSearchCountInput).not.toBeNull();
|
||||
afterSearchCountInput!.checked = false;
|
||||
afterSearchCountInput!.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
click('[data-audience-profile-field-dialog-save="button"]');
|
||||
await flush();
|
||||
|
||||
const savedHeaders = JSON.parse(
|
||||
window.localStorage.getItem("sces:audience-profile:selectedHeaders") ?? "[]"
|
||||
) as string[];
|
||||
expect(savedHeaders).not.toContain("秒思api-看后搜数");
|
||||
expect(savedHeaders).toContain("内容数据-个人视频-播放量中位数");
|
||||
expect(
|
||||
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||
).toContain("画像字段已保存");
|
||||
});
|
||||
|
||||
test(
|
||||
"selected export keeps a generic loading status while exporting the default paged range",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user