165 lines
3.8 KiB
JavaScript
165 lines
3.8 KiB
JavaScript
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/);
|
||
});
|