import { describe, expect, test } from "vitest"; import { buildMarketCsv } from "../src/content/market/csv-exporter"; import { buildSpreadInfoColumns } from "../src/content/market/spread-info"; import type { MarketRecord } from "../src/content/market/types"; describe("csv-exporter", () => { test("uses fallback header order when no page export fields are available", () => { const csv = buildMarketCsv([]); const [headerLine] = csv.split("\n"); expect(headerLine).toBe( [ "达人ID", "达人名称", "地区", "21-60s报价", "商单视频看后搜率", "个人视频看后搜率", "秒思api-看后搜率", "秒思api-看后搜数", "秒思api-新增A3数", "秒思api-新增A3率", "秒思api-CPA3", "秒思api-cp_search", ...buildSpreadInfoColumns() ].join(",") ); }); test("uses page export field order and appends the plugin columns", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", exportFields: { 达人信息: "Alice", 代表视频: "示例视频", 粉丝数: "100w", "21-60s报价": "¥450,000" }, status: "success", backendMetrics: { a3IncreaseCount: "78,366.22", afterViewSearchCount: "9,689.96", afterViewSearchRate: "0.36%", cpSearch: "14.46", cpa3: "1.79", newA3Rate: "3.44%" }, rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "1% - 3%" } } satisfies MarketRecord ]); const [headerLine, rowLine] = csv.split("\n"); expect(headerLine).toBe( [ "达人信息", "粉丝数", "21-60s报价", "商单视频看后搜率", "个人视频看后搜率", "秒思api-看后搜率", "秒思api-看后搜数", "秒思api-新增A3数", "秒思api-新增A3率", "秒思api-CPA3", "秒思api-cp_search", ...buildSpreadInfoColumns() ].join(",") ); expect(rowLine).toMatch( new RegExp( `^${escapeRegExp( 'Alice,100w,"¥450,000",0.5% - 1%,1% - 3%,0.36%,"9,689.96","78,366.22",3.44%,1.79,14.46' )}` ) ); }); test("omits the representative video column from exported page fields", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", exportFields: { 达人信息: "Alice", 代表视频: "示例视频", 粉丝数: "100w" }, status: "success" } satisfies MarketRecord ]); const [headerLine, rowLine] = csv.split("\n"); expect(headerLine).toBe( [ "达人信息", "粉丝数", "商单视频看后搜率", "个人视频看后搜率", "秒思api-看后搜率", "秒思api-看后搜数", "秒思api-新增A3数", "秒思api-新增A3率", "秒思api-CPA3", "秒思api-cp_search", ...buildSpreadInfoColumns() ].join(",") ); expect(rowLine.split(",").slice(0, 10).join(",")).toBe( "Alice,100w,,,,,,,," ); }); test("escapes commas and quotes", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice, \"A\"", location: "Hangzhou", price21To60s: "450000", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "1% - 3%" } } ]); const [, rowLine] = csv.split("\n"); expect(rowLine).toContain("\"Alice, \"\"A\"\"\""); }); test("emits empty rate fields plus failed status for failed rows", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", status: "failed", failureReason: "request-failed" } ]); const [, rowLine] = csv.split("\n"); expect(rowLine.split(",").slice(0, 12).join(",")).toBe( "123,Alice,,,,,,,,,," ); expect(rowLine.split(",").slice(12).every((cell) => cell === "")).toBe(true); }); test("uses normalized display values in export rows", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.02 - 0.1%" } } satisfies MarketRecord ]); const [, rowLine] = csv.split("\n"); expect(rowLine).toContain("0.5% - 1%"); expect(rowLine).toContain("0.02% - 0.1%"); }); test("emits empty backend metric cells when backend metrics are absent", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.02 - 0.1%" } } satisfies MarketRecord ]); const [, rowLine] = csv.split("\n"); expect(rowLine.split(",").slice(0, 12).join(",")).toBe( "123,Alice,,,0.5% - 1%,0.02% - 0.1%,,,,,," ); }); test("appends spread info metric columns after backend metrics", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", spreadMetrics: { "个人视频_近30天_完播率": "28.24%", "只看指派_排除营销流量_星图视频_近30天_互动率": "4.02%" }, status: "success" } satisfies MarketRecord ]); const [headerLine, rowLine] = csv.split("\n"); expect(headerLine).toContain( [ "秒思api-cp_search", "个人视频_近30天_完播率", "个人视频_近30天_播放量中位数" ].join(",") ); expect(headerLine).toContain( "只看指派_排除营销流量_星图视频_近30天_互动率" ); expect(rowLine).toContain("28.24%"); expect(rowLine).toContain("4.02%"); }); test("emits empty spread info cells when spread metrics are absent", () => { const csv = buildMarketCsv([ { authorId: "123", authorName: "Alice", status: "success" } satisfies MarketRecord ]); const [, rowLine] = csv.split("\n"); expect(rowLine.split(",").slice(-70).every((cell) => cell === "")).toBe(true); }); }); function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }