import { describe, expect, test, vi } from "vitest"; import { buildSpreadInfoColumns, buildSpreadInfoUrl, createSpreadInfoClient, DEFAULT_SPREAD_INFO_CONFIGS, filterUnloadedSpreadInfoConfigs, 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.loadAuthorSpreadMetrics("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.loadAuthorSpreadMetrics("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.loadAuthorSpreadMetrics("7361012802036695050", [ { flowType: 0, onlyAssign: true, range: 2, type: 2 } ]) ).rejects.toThrow("network unavailable"); }); });