feat: export audience profiles by author ids
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
buildAuthorBaseInfoUrl,
|
||||
createAuthorBaseClient,
|
||||
mapAuthorBaseInfoResponse
|
||||
} from "../src/content/market/author-base-client";
|
||||
|
||||
describe("author-base-client", () => {
|
||||
test("builds Xingtu author base info url", () => {
|
||||
expect(
|
||||
buildAuthorBaseInfoUrl("6866044569306267651", "https://www.xingtu.cn")
|
||||
).toBe(
|
||||
"https://www.xingtu.cn/gw/api/author/get_author_base_info?o_author_id=6866044569306267651&platform_source=1&platform_channel=1&recommend=true&need_sec_uid=true&need_linkage_info=true"
|
||||
);
|
||||
});
|
||||
|
||||
test("maps author nickname into a market record", () => {
|
||||
expect(mapAuthorBaseInfoResponse("6866044569306267651", {
|
||||
base_resp: { status_code: 0, status_message: "Success" },
|
||||
nick_name: "小九儿"
|
||||
})).toEqual({
|
||||
authorId: "6866044569306267651",
|
||||
authorName: "小九儿",
|
||||
status: "success"
|
||||
});
|
||||
});
|
||||
|
||||
test("loads author base info from Xingtu", async () => {
|
||||
const fetchImpl = vi.fn(async () => ({
|
||||
json: async () => ({
|
||||
base_resp: { status_code: 0, status_message: "Success" },
|
||||
nick_name: "小九儿"
|
||||
}),
|
||||
ok: true
|
||||
}));
|
||||
const client = createAuthorBaseClient({
|
||||
baseUrl: "https://www.xingtu.cn",
|
||||
fetchImpl,
|
||||
timeoutMs: 1000
|
||||
});
|
||||
|
||||
await expect(client.loadAuthorBaseInfo("6866044569306267651")).resolves.toEqual({
|
||||
authorId: "6866044569306267651",
|
||||
authorName: "小九儿",
|
||||
status: "success"
|
||||
});
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
"https://www.xingtu.cn/gw/api/author/get_author_base_info?o_author_id=6866044569306267651&platform_source=1&platform_channel=1&recommend=true&need_sec_uid=true&need_linkage_info=true",
|
||||
expect.objectContaining({
|
||||
credentials: "include",
|
||||
method: "GET"
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { parseAuthorIds } from "../src/content/market/author-id-input";
|
||||
|
||||
describe("author-id-input", () => {
|
||||
test("parses newline comma and space separated Xingtu author ids", () => {
|
||||
expect(parseAuthorIds(`
|
||||
6866044569306267651
|
||||
7040323176106033165,7088592143119286285
|
||||
7222310247979810854
|
||||
`)).toEqual({
|
||||
duplicates: [],
|
||||
ids: [
|
||||
"6866044569306267651",
|
||||
"7040323176106033165",
|
||||
"7088592143119286285",
|
||||
"7222310247979810854"
|
||||
],
|
||||
invalidTokens: []
|
||||
});
|
||||
});
|
||||
|
||||
test("deduplicates ids and reports invalid tokens", () => {
|
||||
expect(parseAuthorIds(`
|
||||
6866044569306267651
|
||||
bad-id
|
||||
123
|
||||
6866044569306267651
|
||||
`)).toEqual({
|
||||
duplicates: ["6866044569306267651"],
|
||||
ids: ["6866044569306267651"],
|
||||
invalidTokens: ["bad-id", "123"]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -328,6 +328,9 @@ describe("market-content-entry", () => {
|
||||
expect(
|
||||
document.querySelector('[data-plugin-export-audience-profile="button"]')
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
document.querySelector('[data-plugin-export-audience-profile-by-id="button"]')
|
||||
).not.toBeNull();
|
||||
expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull();
|
||||
expect(document.querySelector('[data-plugin-export-status="text"]')).not.toBeNull();
|
||||
|
||||
@@ -340,12 +343,17 @@ describe("market-content-entry", () => {
|
||||
const audienceProfileExportButton = document.querySelector(
|
||||
'[data-plugin-export-audience-profile="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const audienceProfileByIdExportButton = document.querySelector(
|
||||
'[data-plugin-export-audience-profile-by-id="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(audienceProfileByIdExportButton?.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)");
|
||||
expect(audienceProfileByIdExportButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
});
|
||||
|
||||
test("remounts the plugin action bar when the native market action row appears later", async () => {
|
||||
@@ -1698,6 +1706,87 @@ describe("market-content-entry", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("audience profile export by id loads pasted creators without page selection", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||
]);
|
||||
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||
const loadAuthorBaseInfo = vi.fn(async (authorId: string) => ({
|
||||
authorId,
|
||||
authorName: authorId === "6866044569306267651" ? "小九儿" : "达人 B",
|
||||
status: "success" as const
|
||||
}));
|
||||
const loadBusinessAbility = vi.fn(async () => ({
|
||||
estimates: {},
|
||||
status: "success" as const,
|
||||
videos: {}
|
||||
}));
|
||||
const loadAudienceProfile = vi.fn(async () => ({
|
||||
age: [{ label: "31-40", value: "60%" }],
|
||||
crowd: [{ label: "都市蓝领", value: "80%" }],
|
||||
cityTier: [{ label: "一线城市", value: "90%" }],
|
||||
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,
|
||||
loadAuthorBaseInfo,
|
||||
loadBusinessAbility,
|
||||
loadAudienceProfile,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
onCsvReady,
|
||||
promptAuthorIds: () => `
|
||||
6866044569306267651
|
||||
7040323176106033165
|
||||
6866044569306267651
|
||||
bad-id
|
||||
`,
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
click('[data-plugin-export-audience-profile-by-id="button"]');
|
||||
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||
|
||||
expect(loadAuthorBaseInfo.mock.calls.map(([authorId]) => authorId)).toEqual([
|
||||
"6866044569306267651",
|
||||
"7040323176106033165"
|
||||
]);
|
||||
expect(loadAudienceProfile).toHaveBeenCalledTimes(6);
|
||||
expect(loadBusinessAbility).toHaveBeenCalledTimes(2);
|
||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
record: expect.objectContaining({
|
||||
authorId: "6866044569306267651",
|
||||
authorName: "小九儿",
|
||||
exportFields: {
|
||||
达人ID: "6866044569306267651",
|
||||
达人名称: "小九儿",
|
||||
导出状态: "成功",
|
||||
失败原因: ""
|
||||
}
|
||||
})
|
||||
}),
|
||||
expect.objectContaining({
|
||||
record: expect.objectContaining({
|
||||
authorId: "7040323176106033165",
|
||||
authorName: "达人 B"
|
||||
})
|
||||
})
|
||||
]);
|
||||
expect(onCsvReady).toHaveBeenCalledWith(
|
||||
"profile-csv",
|
||||
expect.stringMatching(/^达人连接用户画像_按ID导出_\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