feat: allow selecting audience export fields

This commit is contained in:
2026-05-19 16:26:04 +08:00
parent db95a1f565
commit 50933af0a6
11 changed files with 1273 additions and 110 deletions
+151 -52
View File
@@ -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 () => {