feat: add selected audience profile csv export
This commit is contained in:
@@ -209,6 +209,46 @@ describe("market-content-entry", () => {
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:test-url");
|
||||
});
|
||||
|
||||
test("booted export callback can download a custom csv filename", async () => {
|
||||
const createMarketController = vi.fn(() => ({
|
||||
ready: Promise.resolve()
|
||||
}));
|
||||
let clickedDownload: { download: string; href: string } | null = null;
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function (
|
||||
this: HTMLAnchorElement
|
||||
) {
|
||||
clickedDownload = {
|
||||
download: this.download,
|
||||
href: this.href
|
||||
};
|
||||
});
|
||||
|
||||
window.history.replaceState({}, "", "/ad/creator/market");
|
||||
Object.defineProperty(window.URL, "createObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn(() => "blob:test-url")
|
||||
});
|
||||
Object.defineProperty(window.URL, "revokeObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn()
|
||||
});
|
||||
|
||||
const { bootContentScript } = await import("../src/content/index");
|
||||
await bootContentScript({
|
||||
createMarketController,
|
||||
sendAuthMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:state",
|
||||
value: { isAuthenticated: true }
|
||||
}))
|
||||
});
|
||||
|
||||
const controllerOptions = createMarketController.mock.calls[0]?.[0];
|
||||
controllerOptions.onCsvReady("列1,列2\n值1,值2", "达人连接用户画像_20260518_1530.csv");
|
||||
|
||||
expect(clickedDownload?.download).toBe("达人连接用户画像_20260518_1530.csv");
|
||||
});
|
||||
|
||||
test("booted export callback sends the csv to extension runtime when available", async () => {
|
||||
const createMarketController = vi.fn(() => ({
|
||||
ready: Promise.resolve()
|
||||
@@ -245,6 +285,7 @@ describe("market-content-entry", () => {
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
csv: "列1,列2\n值1,值2",
|
||||
filename: expect.stringMatching(/^star-chart-search-enhancer-/),
|
||||
type: "download-market-csv"
|
||||
})
|
||||
);
|
||||
@@ -284,6 +325,9 @@ describe("market-content-entry", () => {
|
||||
expect(document.body.firstElementChild).not.toBe(toolbar);
|
||||
expect(document.querySelector('[data-plugin-export-range="select"]')).not.toBeNull();
|
||||
expect(document.querySelector('[data-plugin-export="button"]')).not.toBeNull();
|
||||
expect(
|
||||
document.querySelector('[data-plugin-export-audience-profile="button"]')
|
||||
).not.toBeNull();
|
||||
expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull();
|
||||
expect(document.querySelector('[data-plugin-export-status="text"]')).not.toBeNull();
|
||||
|
||||
@@ -293,10 +337,15 @@ describe("market-content-entry", () => {
|
||||
const batchSubmitButton = document.querySelector(
|
||||
'[data-plugin-batch-submit="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const audienceProfileExportButton = document.querySelector(
|
||||
'[data-plugin-export-audience-profile="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
expect(exportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
|
||||
expect(batchSubmitButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
|
||||
expect(audienceProfileExportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
|
||||
expect(exportButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
expect(batchSubmitButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
expect(audienceProfileExportButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
});
|
||||
|
||||
test("remounts the plugin action bar when the native market action row appears later", async () => {
|
||||
@@ -1535,6 +1584,92 @@ describe("market-content-entry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("audience profile export requires selected creators", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||
]);
|
||||
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||
const loadAudienceProfile = vi.fn();
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
buildAudienceProfileCsv,
|
||||
document,
|
||||
loadAudienceProfile,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
onCsvReady: vi.fn(),
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
|
||||
click('[data-plugin-export-audience-profile="button"]');
|
||||
await flush();
|
||||
|
||||
expect(loadAudienceProfile).not.toHaveBeenCalled();
|
||||
expect(buildAudienceProfileCsv).not.toHaveBeenCalled();
|
||||
expect(
|
||||
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||
).toContain("请先勾选需要导出画像的达人");
|
||||
});
|
||||
|
||||
test("audience profile export loads profiles only for selected creators", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||
]);
|
||||
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||
const loadAudienceProfile = vi.fn(async () => ({
|
||||
gender: [{ label: "男性", value: "60%" }],
|
||||
status: "success" as const
|
||||
}));
|
||||
const onCsvReady = vi.fn();
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
buildAudienceProfileCsv,
|
||||
document,
|
||||
loadAudienceProfile,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
onCsvReady,
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
clickSelectionCheckboxForAuthor("222");
|
||||
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(loadAudienceProfile).toHaveBeenCalledTimes(1);
|
||||
expect(loadAudienceProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ authorId: "222" })
|
||||
);
|
||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
||||
{
|
||||
profile: {
|
||||
gender: [{ label: "男性", value: "60%" }],
|
||||
status: "success"
|
||||
},
|
||||
record: expect.objectContaining({ authorId: "222" })
|
||||
}
|
||||
]);
|
||||
expect(onCsvReady).toHaveBeenCalledWith(
|
||||
"profile-csv",
|
||||
expect.stringMatching(/^达人连接用户画像_\d{8}_\d{4}\.csv$/)
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
"selected export keeps a generic loading status while exporting the default paged range",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user