feat: 复盘报表视图与 CSV 导出(manager 专属)
This commit is contained in:
164
tests/task-report.test.mjs
Normal file
164
tests/task-report.test.mjs
Normal file
@@ -0,0 +1,164 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { buildTaskReportCsv } from "../lib/task-report-csv.ts";
|
||||
import { getTaskReportData } from "../lib/mvp-db.ts";
|
||||
|
||||
const reportRow = {
|
||||
id: "task-A",
|
||||
name: "夏日种草计划",
|
||||
task_type: "content_publish",
|
||||
brand: "轻食品牌",
|
||||
platform: "小红书",
|
||||
status: "active",
|
||||
due_at: "2026-08-20",
|
||||
created_at: "2026-08-01 10:00:00",
|
||||
quantity: 20,
|
||||
project_name: "八月投放",
|
||||
claimed_count: 10,
|
||||
published_count: 8,
|
||||
completed_count: 5,
|
||||
exposure_total: 12000,
|
||||
views_total: 9000,
|
||||
d2_likes: 100,
|
||||
d2_comments: 20,
|
||||
d2_collects: 30,
|
||||
d5_likes: 150,
|
||||
d5_comments: 25,
|
||||
d5_collects: 40,
|
||||
d7_likes: 180,
|
||||
d7_comments: 30,
|
||||
d7_collects: 50,
|
||||
latest_likes: 200,
|
||||
latest_comments: 40,
|
||||
latest_collects: 60,
|
||||
latest_shares: 10,
|
||||
};
|
||||
|
||||
test("buildTaskReportCsv writes BOM, headers and mapped rows", () => {
|
||||
const csv = buildTaskReportCsv([reportRow]);
|
||||
assert.ok(csv.startsWith(""), "starts with UTF-8 BOM");
|
||||
const lines = csv.slice(1).split("\r\n");
|
||||
assert.equal(lines.length, 2);
|
||||
assert.equal(
|
||||
lines[0],
|
||||
[
|
||||
"任务ID",
|
||||
"任务名称",
|
||||
"任务类型",
|
||||
"品牌",
|
||||
"平台",
|
||||
"所属项目",
|
||||
"任务状态",
|
||||
"计划数量",
|
||||
"领取数",
|
||||
"发布数",
|
||||
"完成数",
|
||||
"完成率",
|
||||
"截止日期",
|
||||
"创建时间",
|
||||
"曝光量合计",
|
||||
"阅读量合计",
|
||||
"D2点赞",
|
||||
"D2评论",
|
||||
"D2收藏",
|
||||
"D5点赞",
|
||||
"D5评论",
|
||||
"D5收藏",
|
||||
"D7点赞",
|
||||
"D7评论",
|
||||
"D7收藏",
|
||||
"最新点赞",
|
||||
"最新评论",
|
||||
"最新收藏",
|
||||
"最新转发",
|
||||
].join(","),
|
||||
);
|
||||
assert.equal(
|
||||
lines[1],
|
||||
[
|
||||
"task-A",
|
||||
"夏日种草计划",
|
||||
"内容发布",
|
||||
"轻食品牌",
|
||||
"小红书",
|
||||
"八月投放",
|
||||
"进行中",
|
||||
20,
|
||||
10,
|
||||
8,
|
||||
5,
|
||||
"50%",
|
||||
"2026-08-20",
|
||||
"2026-08-01 10:00:00",
|
||||
12000,
|
||||
9000,
|
||||
100,
|
||||
20,
|
||||
30,
|
||||
150,
|
||||
25,
|
||||
40,
|
||||
180,
|
||||
30,
|
||||
50,
|
||||
200,
|
||||
40,
|
||||
60,
|
||||
10,
|
||||
].join(","),
|
||||
);
|
||||
});
|
||||
|
||||
test("buildTaskReportCsv escapes commas and quotes, blanks null metrics", () => {
|
||||
const csv = buildTaskReportCsv([
|
||||
{
|
||||
...reportRow,
|
||||
name: '含,逗号"引号"任务',
|
||||
project_name: null,
|
||||
claimed_count: 0,
|
||||
exposure_total: null,
|
||||
views_total: null,
|
||||
d2_likes: null,
|
||||
d2_comments: null,
|
||||
d2_collects: null,
|
||||
d5_likes: null,
|
||||
d5_comments: null,
|
||||
d5_collects: null,
|
||||
d7_likes: null,
|
||||
d7_comments: null,
|
||||
d7_collects: null,
|
||||
latest_likes: null,
|
||||
latest_comments: null,
|
||||
latest_collects: null,
|
||||
latest_shares: null,
|
||||
},
|
||||
]);
|
||||
const rowLine = csv.slice(1).split("\r\n")[1];
|
||||
assert.ok(rowLine.includes('"含,逗号""引号""任务"'));
|
||||
const cells = rowLine.split(",");
|
||||
assert.equal(cells.at(-1), "", "null latest_shares becomes empty");
|
||||
assert.equal(cells.at(-18), "", "no claims means empty completion rate");
|
||||
assert.equal(cells.at(-16), "2026-08-01 10:00:00");
|
||||
});
|
||||
|
||||
test("getTaskReportData aggregates per task via distributions join", async () => {
|
||||
let capturedSql = "";
|
||||
const db = {
|
||||
prepare(sql) {
|
||||
capturedSql = sql;
|
||||
return {
|
||||
async all() {
|
||||
return { results: [reportRow] };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const rows = await getTaskReportData(db);
|
||||
assert.deepEqual(rows, [reportRow]);
|
||||
assert.match(capturedSql, /FROM tasks t/);
|
||||
assert.match(capturedSql, /LEFT JOIN distributions d ON d\.task_id = t\.id/);
|
||||
assert.match(capturedSql, /LEFT JOIN projects p ON p\.id = t\.project_id/);
|
||||
assert.match(capturedSql, /GROUP BY t\.id/);
|
||||
assert.match(capturedSql, /publish_url/);
|
||||
assert.match(capturedSql, /latest_shares/);
|
||||
});
|
||||
Reference in New Issue
Block a user