Files
star-chart-search-enhancer/tests/spread-info.test.ts
T

389 lines
10 KiB
TypeScript

import { describe, expect, test, vi } from "vitest";
import {
buildSpreadInfoConfigKey,
buildSpreadInfoColumns,
buildSpreadInfoUrl,
createSpreadInfoClient,
DEFAULT_SPREAD_INFO_CONFIGS,
filterUnloadedSpreadInfoConfigs,
matchesSpreadMetricRule,
normalizeSpreadInfoConfig,
mapSpreadInfoResponse,
selectSpreadInfoConfigsForHeaders
} from "../src/content/market/spread-info";
describe("spread-info", () => {
test("builds the spread info url with all request parameters", () => {
expect(
buildSpreadInfoUrl(
"7361012802036695050",
{
flowType: 1,
onlyAssign: true,
range: 2,
type: 2
},
"https://www.xingtu.cn"
)
).toBe(
"https://www.xingtu.cn/gw/api/data_sp/get_author_spread_info?o_author_id=7361012802036695050&platform_source=1&platform_channel=1&type=2&flow_type=1&only_assign=true&range=2"
);
});
test("defines personal video ranges with fixed non-prefix parameters", () => {
const personalConfigs = DEFAULT_SPREAD_INFO_CONFIGS.filter(
(config) => config.type === 1
);
expect(personalConfigs).toEqual([
{
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
{
flowType: 0,
onlyAssign: false,
range: 3,
type: 1
}
]);
expect(buildSpreadInfoColumns(personalConfigs).slice(0, 2)).toEqual([
"内容数据-个人视频-近30天-完播率",
"内容数据-个人视频-近30天-播放量中位数"
]);
});
test("defines all xingtu video assign flow and range combinations", () => {
const xingtuConfigs = DEFAULT_SPREAD_INFO_CONFIGS.filter(
(config) => config.type === 2
);
expect(xingtuConfigs).toHaveLength(8);
expect(xingtuConfigs).toContainEqual({
flowType: 1,
onlyAssign: true,
range: 2,
type: 2
});
expect(xingtuConfigs).toContainEqual({
flowType: 0,
onlyAssign: false,
range: 3,
type: 2
});
expect(buildSpreadInfoColumns([
{
flowType: 1,
onlyAssign: true,
range: 2,
type: 2
}
])).toEqual([
"内容数据-只看指派-排除营销流量-星图视频-近30天-完播率",
"内容数据-只看指派-排除营销流量-星图视频-近30天-播放量中位数",
"内容数据-只看指派-排除营销流量-星图视频-近30天-互动率",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均时长",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均评论数",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均点赞数",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均转发数"
]);
});
test("selects only unique request configs required by chosen columns", () => {
expect(
selectSpreadInfoConfigsForHeaders([
"内容数据-个人视频-近30天-完播率",
"内容数据-个人视频-近30天-作品平均评论数",
"内容数据-只看指派-排除营销流量-星图视频-近90天-互动率",
"效果预估-20-60s视频-预期CPM"
])
).toEqual([
{
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
{
flowType: 1,
onlyAssign: true,
range: 3,
type: 2
}
]);
});
test("keeps newly selected configs when other content configs are cached", () => {
const personalConfig = DEFAULT_SPREAD_INFO_CONFIGS[0];
const xingtuConfig = DEFAULT_SPREAD_INFO_CONFIGS.find(
(config) =>
config.type === 2 &&
config.onlyAssign &&
config.flowType === 1 &&
config.range === 3
);
expect(xingtuConfig).toBeDefined();
expect(
filterUnloadedSpreadInfoConfigs(
[personalConfig, xingtuConfig!],
{ "内容数据-个人视频-近30天-完播率": "28%" }
)
).toEqual([xingtuConfig]);
});
test("maps spread info response values into display values", () => {
expect(
mapSpreadInfoResponse({
avg_duration: "5600",
comment_avg: "7502",
interact_rate: {
value: 402
},
item_rate: {
play_mid: {
value: 10913233
}
},
like_avg: "494458",
play_over_rate: {
value: 2824
},
share_avg: "188267"
})
).toEqual({
averageCommentCount: "7502",
averageDuration: "56",
averageLikeCount: "494458",
averageShareCount: "188267",
finishRate: "28.24%",
interactionRate: "4.02%",
playMedian: "10913233"
});
});
test("loads each configured spread metric column for one author", async () => {
const fetchImpl = vi.fn(async () => ({
json: async () => ({
avg_duration: "5600",
comment_avg: "7502",
interact_rate: {
value: 402
},
like_avg: "494458",
play_mid: "10913233",
play_over_rate: {
value: 2824
},
share_avg: "188267"
}),
ok: true
}));
const client = createSpreadInfoClient({
configs: [
{
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
}
],
fetchImpl
});
await expect(
client.loadAuthorSpreadMetrics("7361012802036695050")
).resolves.toEqual({
"内容数据-个人视频-近30天-互动率": "4.02%",
"内容数据-个人视频-近30天-作品平均点赞数": "494458",
"内容数据-个人视频-近30天-作品平均评论数": "7502",
"内容数据-个人视频-近30天-作品平均时长": "56",
"内容数据-个人视频-近30天-作品平均转发数": "188267",
"内容数据-个人视频-近30天-完播率": "28.24%",
"内容数据-个人视频-近30天-播放量中位数": "10913233"
});
expect(fetchImpl).toHaveBeenCalledWith(
"https://www.xingtu.cn/gw/api/data_sp/get_author_spread_info?o_author_id=7361012802036695050&platform_source=1&platform_channel=1&type=1&flow_type=0&only_assign=false&range=2",
expect.objectContaining({
credentials: "include",
method: "GET"
})
);
});
test("requests only the explicitly selected spread configs", async () => {
const fetchImpl = vi.fn(async () => ({
json: async () => ({ play_over_rate: { value: 2824 } }),
ok: true
}));
const client = createSpreadInfoClient({ fetchImpl });
const selectedConfig = {
flowType: 1 as const,
onlyAssign: true,
range: 3 as const,
type: 2 as const
};
await client.loadAuthorSpreadMetrics("7361012802036695050", [selectedConfig]);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(fetchImpl.mock.calls[0][0]).toContain(
"type=2&flow_type=1&only_assign=true&range=3"
);
});
test("throws an HTTP error instead of returning empty metrics", async () => {
const client = createSpreadInfoClient({
fetchImpl: async () => ({
json: async () => ({}),
ok: false,
status: 429,
statusText: "Too Many Requests"
})
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
).rejects.toThrow("HTTP 429: Too Many Requests");
});
test("throws an explicit error when Xingtu reports business rate limiting", async () => {
const client = createSpreadInfoClient({
fetchImpl: async () => ({
json: async () => ({
base_resp: {
status_code: 31157,
status_message: "您访问过于频繁,请24小时后重试"
}
}),
ok: true
})
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
).rejects.toThrow(
"星图传播数据接口被限流 (status_code=31157): 您访问过于频繁,请24小时后重试"
);
});
test("propagates network errors instead of returning empty metrics", async () => {
const client = createSpreadInfoClient({
fetchImpl: async () => {
throw new Error("network unavailable");
}
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
).rejects.toThrow("network unavailable");
});
test("normalizes fixed personal-video parameters before grouping", () => {
expect(
normalizeSpreadInfoConfig({
flowType: 1,
onlyAssign: true,
range: 3,
type: 1
})
).toEqual({
flowType: 0,
onlyAssign: false,
range: 3,
type: 1
});
});
test("uses all normalized video dimensions in the config key", () => {
expect(
buildSpreadInfoConfigKey({
flowType: 1,
onlyAssign: true,
range: 3,
type: 1
})
).toBe(
buildSpreadInfoConfigKey({
flowType: 0,
onlyAssign: false,
range: 3,
type: 1
})
);
expect(
buildSpreadInfoConfigKey({
flowType: 0,
onlyAssign: false,
range: 2,
type: 2
})
).not.toBe(
buildSpreadInfoConfigKey({
flowType: 0,
onlyAssign: false,
range: 3,
type: 2
})
);
});
test("matches only the metric named by one filter rule", () => {
expect(
matchesSpreadMetricRule(
{
finishRate: "28.24%",
interactionRate: "4.02%"
},
{
config: {
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
metric: "finishRate",
threshold: 28
}
)
).toBe(true);
expect(
matchesSpreadMetricRule(
{
finishRate: "28.24%"
},
{
config: {
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
metric: "interactionRate",
threshold: 1
}
)
).toBe(false);
});
});