feat: KOC详情弹窗 + 任务中心单入口改版 + 资源池默认合作方/采集兜底
KOC 详情弹窗(合作方/数据回收/工作台三处入口): - 新增 KocDetailModal:基本信息 / 绑定KOC账号画像 / 参与任务记录 / 履约漏斗 - 工作台最近分发、回收队列账号名、截图回收合作方名、合作方页名称、分发详情头部合作方均可点开 - 分发表行点击仍打开分发详情(stopPropagation 隔离) 任务中心单入口改版(对齐"单任务双阶段"模型): - 任务卡发布/回收面板整块可点,全卡仅保留一个「查看明细」,回收面板重复按钮移除 - 详情页自述身份:动态 label(任务详情·分发与回收一体/仅分发)+ 阶段徽章(含回收·截止/未开通回收) - 内容分发表新增阶段筛选 chips(全部/待发布/已发布待数据/待收截图,未开通回收不显示截图项),筛选落空给专用空态文案 - 数据回收页入口文案改为「选择一个任务进入回收队列」,全站不再区分发布/回收两种任务类型 资源池 KOC 默认加入合作方 + 采集兜底: - partners 新增 released_account_id(mysql/0023):显式解绑/被顶替的账号不再被默认合作方同步回填 - 任务未设置采集计划时,默认按发布时间起连续采集 7 天,等待文案显示「等待D1 · 默认7天」
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import { isCollectionScheduleDue } from "../lib/collection-service.ts";
|
||||
import {
|
||||
DEFAULT_COLLECTION_DAYS,
|
||||
effectiveCollectionDays,
|
||||
isCollectionScheduleDue,
|
||||
parseCollectionDays,
|
||||
runDueScheduledCollections,
|
||||
} from "../lib/collection-service.ts";
|
||||
import { sortWithNullsLast } from "../lib/sort-utils.ts";
|
||||
|
||||
test("runs the Beijing collection schedule from 09:00", () => {
|
||||
@@ -78,3 +84,176 @@ test("a failed collection keeps historically successful rows on success", async
|
||||
// run 行照实记录失败,保留可观测性
|
||||
assert.match(source, /UPDATE collection_runs\s+SET status = 'failed'/);
|
||||
});
|
||||
|
||||
// —— 未设置采集计划时默认连采 7 天 ——
|
||||
|
||||
test("effective collection days fall back to 7 days when unset", () => {
|
||||
assert.deepEqual(effectiveCollectionDays(null), [1, 2, 3, 4, 5, 6, 7]);
|
||||
assert.deepEqual(effectiveCollectionDays("[]"), DEFAULT_COLLECTION_DAYS);
|
||||
assert.deepEqual(effectiveCollectionDays('{"bad":1}'), DEFAULT_COLLECTION_DAYS);
|
||||
assert.deepEqual(effectiveCollectionDays("[0,9]"), DEFAULT_COLLECTION_DAYS);
|
||||
assert.deepEqual(effectiveCollectionDays("[3,1,2,2]"), [1, 2, 3]);
|
||||
assert.deepEqual(parseCollectionDays("[7,2]"), [2, 7]);
|
||||
});
|
||||
|
||||
function fakeSchedulerDb({ tasks = [], published = [] } = {}) {
|
||||
const executed = [];
|
||||
const record = (sql, values) => executed.push({ sql, values });
|
||||
const db = {
|
||||
prepare(sql) {
|
||||
return {
|
||||
bind(...values) {
|
||||
return {
|
||||
async first() {
|
||||
record(sql, values);
|
||||
return null;
|
||||
},
|
||||
async run() {
|
||||
record(sql, values);
|
||||
return { meta: { changes: 0 } };
|
||||
},
|
||||
async all() {
|
||||
record(sql, values);
|
||||
if (/FROM tasks/.test(sql)) {
|
||||
const scoped = tasks.filter(
|
||||
(task) => !values[0] || task.id === values[0],
|
||||
);
|
||||
return {
|
||||
results: scoped.filter(
|
||||
(task) =>
|
||||
(task.task_type ?? "content_publish") !==
|
||||
"screenshot_collect",
|
||||
),
|
||||
};
|
||||
}
|
||||
if (/COALESCE\(d\.publish_time, d\.claimed_at\)/.test(sql)) {
|
||||
return {
|
||||
results: published.filter((row) => row.task_id === values[0]),
|
||||
};
|
||||
}
|
||||
// 无待采集行,避免触发真实 MCP 采集
|
||||
return { results: [] };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
async batch() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
return { db, executed };
|
||||
}
|
||||
|
||||
test("collects unplanned tasks on the default 7-day schedule from publish date", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-a",
|
||||
task_type: "content_publish",
|
||||
collection_start_date: null,
|
||||
collection_days: "[]",
|
||||
},
|
||||
],
|
||||
published: [
|
||||
// UTC 2026-10-09 01:30 → 上海 2026-10-09,D1–D4 在 10-12 09:00 前到期
|
||||
{ id: "dist-a", task_id: "task-a", publish_anchor: "2026-10-09 01:30:00" },
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 4);
|
||||
const anchorQuery = executed.find((entry) =>
|
||||
/COALESCE\(d\.publish_time/.test(entry.sql),
|
||||
);
|
||||
assert.ok(anchorQuery, "未设置计划时应查询每篇的发布时间");
|
||||
assert.equal(anchorQuery.values[0], "task-a");
|
||||
const scoped = executed.filter((entry) =>
|
||||
/SELECT d\.id FROM distributions d/.test(entry.sql),
|
||||
);
|
||||
assert.equal(scoped.length, 4);
|
||||
assert.ok(scoped.every((entry) => entry.values.includes("dist-a")));
|
||||
});
|
||||
|
||||
test("skips the default window once 7 days have passed since publish", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-old",
|
||||
task_type: "content_publish",
|
||||
collection_start_date: null,
|
||||
collection_days: "[]",
|
||||
},
|
||||
],
|
||||
published: [
|
||||
{ id: "dist-old", task_id: "task-old", publish_anchor: "2026-09-01 00:00:00" },
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 0);
|
||||
assert.ok(
|
||||
!executed.some((entry) =>
|
||||
/SELECT d\.id FROM distributions d/.test(entry.sql),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the explicit task-level schedule when a plan is saved", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-b",
|
||||
task_type: "content_publish",
|
||||
collection_start_date: "2026-10-10",
|
||||
collection_days: "[2]",
|
||||
},
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 1);
|
||||
const needing = executed.filter((entry) =>
|
||||
/SELECT d\.id FROM distributions d/.test(entry.sql),
|
||||
);
|
||||
assert.deepEqual(needing.map((entry) => entry.values), [
|
||||
["task-b", "2026-10-11"],
|
||||
]);
|
||||
assert.ok(
|
||||
!executed.some((entry) => /COALESCE\(d\.publish_time/.test(entry.sql)),
|
||||
);
|
||||
});
|
||||
|
||||
test("ignores screenshot collection tasks in the scheduler", async () => {
|
||||
const { db, executed } = fakeSchedulerDb({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-shot",
|
||||
task_type: "screenshot_collect",
|
||||
collection_start_date: null,
|
||||
collection_days: "[]",
|
||||
},
|
||||
],
|
||||
published: [
|
||||
{ id: "dist-shot", task_id: "task-shot", publish_anchor: "2026-10-11 00:00:00" },
|
||||
],
|
||||
});
|
||||
const summary = await runDueScheduledCollections(
|
||||
db,
|
||||
Date.parse("2026-10-12T01:00:00Z"),
|
||||
{},
|
||||
);
|
||||
assert.equal(summary.dueTasks, 0);
|
||||
assert.ok(
|
||||
!executed.some((entry) => /COALESCE\(d\.publish_time/.test(entry.sql)),
|
||||
);
|
||||
});
|
||||
|
||||
90
tests/koc-detail.test.mjs
Normal file
90
tests/koc-detail.test.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const source = readFileSync(new URL("../app/admin-app.tsx", import.meta.url), "utf8");
|
||||
|
||||
test("KOC 详情弹窗组件:三段式(基本信息 / 绑定账号 / 参与记录)", () => {
|
||||
const start = source.indexOf("function KocDetailModal");
|
||||
assert.ok(start > 0, "应存在 KocDetailModal 组件");
|
||||
const body = source.slice(start, source.indexOf("function RecoveryPage"));
|
||||
assert.match(body, /KOC详情:\$\{partner\.name\}/);
|
||||
assert.match(body, /① 基本信息/);
|
||||
assert.match(body, /② 绑定 KOC 账号(资源)/);
|
||||
assert.match(body, /③ 参与任务记录/);
|
||||
assert.match(body, /暂无领取记录/);
|
||||
// 概览漏斗:领取 → 发布 → 回收完成
|
||||
assert.match(body, /领取 <b>\{history\.length\}<\/b>/);
|
||||
assert.match(body, /发布 <b>\{publishedCount\}<\/b>/);
|
||||
assert.match(body, /回收完成 <b>\{completedCount\}<\/b>/);
|
||||
// 绑定账号缺失时的兜底文案区分群主与外部KOC
|
||||
assert.match(body, /群主合作方无需绑定 KOC 资源账号/);
|
||||
});
|
||||
|
||||
test("工作台:按 id 挂载 KOC 详情,操作刷新后内容自动更新", () => {
|
||||
assert.match(source, /const \[detailPartnerId, setDetailPartnerId\] = useState<string \| null>\(null\);/);
|
||||
assert.match(
|
||||
source,
|
||||
/data\.partners\.find\(\(partner\) => partner\.id === detailPartnerId\)/,
|
||||
);
|
||||
assert.match(source, /item\.partner_id === partnerRow\.id/);
|
||||
assert.match(source, /<KocDetailModal/);
|
||||
});
|
||||
|
||||
test("工作台最近分发:行可点开分发详情,合作方可点开 KOC 详情", () => {
|
||||
const start = source.indexOf("function OverviewPage");
|
||||
const body = source.slice(start, source.indexOf("function OwnerLinkRow"));
|
||||
assert.match(body, /onOpenDetail=\{onOpenDetail\}/);
|
||||
assert.match(body, /onOpenPartner=\{onOpenPartner\}/);
|
||||
});
|
||||
|
||||
test("分发表/截图回收表:合作方名为 KOC 详情入口且不触发行点击", () => {
|
||||
const tableStart = source.indexOf("function DistributionTable");
|
||||
const tableBody = source.slice(tableStart, source.indexOf("type AdminPreviewImage"));
|
||||
assert.match(tableBody, /onOpenPartner\?: \(partnerId: string\) => void;/);
|
||||
assert.match(tableBody, /className="koc-link"/);
|
||||
assert.match(tableBody, /event\.stopPropagation\(\);\s*onOpenPartner\(item\.partner_id\)/);
|
||||
|
||||
const shotStart = source.indexOf("function ScreenshotTaskTable");
|
||||
const shotBody = source.slice(shotStart, source.indexOf("const RESOURCE_PAGE_SIZE"));
|
||||
assert.match(shotBody, /onOpenPartner\?: \(partnerId: string\) => void;/);
|
||||
assert.match(shotBody, /onOpenPartner\(item\.partner_id\)/);
|
||||
});
|
||||
|
||||
test("数据回收页:回收队列账号名与截图回收合作方均可点开 KOC 详情", () => {
|
||||
const start = source.indexOf("function RecoveryPage");
|
||||
const body = source.slice(start);
|
||||
assert.match(body, /onOpenPartner: \(partnerId: string\) => void;/);
|
||||
// 回收队列:账号名(待识别账号兜底)为入口,且阻断行点击冒泡
|
||||
assert.match(body, /\{item\.account_nickname \|\| "待识别账号"\}\s*<\/button>/);
|
||||
assert.match(
|
||||
body,
|
||||
/event\.stopPropagation\(\);\s*onOpenPartner\(item\.partner_id\)/,
|
||||
);
|
||||
// 截图回收表透传
|
||||
assert.match(body, /<ScreenshotTaskTable\s+distributions=\{taskDistributions\}\s+onOpenPartner=\{onOpenPartner\}/);
|
||||
});
|
||||
|
||||
test("合作方页:合作方名称单元格可点开 KOC 详情", () => {
|
||||
const start = source.indexOf("function PartnersPage");
|
||||
const body = source.slice(start, source.indexOf("function ResourcesPage"));
|
||||
assert.match(body, /onOpenPartner: \(partner: Partner\) => void;/);
|
||||
assert.match(body, /className="partner-name-cell"/);
|
||||
assert.match(body, /onClick=\{\(\) => onOpenPartner\(partner\)\}/);
|
||||
});
|
||||
|
||||
test("分发详情弹窗:头部合作方可跳转 KOC 详情", () => {
|
||||
const start = source.indexOf("function DistributionDetailModal");
|
||||
const body = source.slice(start, source.indexOf("function KocDetailModal"));
|
||||
assert.match(body, /onOpenPartner\?: \(partnerId: string\) => void;/);
|
||||
assert.match(body, /detail-meta-koc/);
|
||||
assert.match(body, /onOpenPartner\(item\.partner_id\)/);
|
||||
});
|
||||
|
||||
test("KOC 详情样式:入口链接与历史记录列表", () => {
|
||||
const css = readFileSync(new URL("../app/globals.css", import.meta.url), "utf8");
|
||||
assert.match(css, /^\.koc-link \{/m);
|
||||
assert.match(css, /^\.partner-name-cell \{/m);
|
||||
assert.match(css, /^\.koc-history-list \{/m);
|
||||
assert.match(css, /^\.koc-summary-strip \{/m);
|
||||
});
|
||||
@@ -30,14 +30,14 @@ test("partner_bind_account enforces one partner per resource account", () => {
|
||||
const body = section(actionRoute, "partner_bind_account");
|
||||
assert.match(
|
||||
body,
|
||||
/SELECT id, name FROM partners WHERE account_id = \? AND id != \? LIMIT 1/,
|
||||
/SELECT id, name, partner_type, wecom_external_user_id, wecom_userid\s+FROM partners WHERE account_id = \? AND id != \? LIMIT 1/,
|
||||
"绑定前必须检查资源账号是否已被其它合作方占用",
|
||||
);
|
||||
assert.match(body, /该 KOC 资源已绑定合作方/, "占用时返回可读错误");
|
||||
assert.match(
|
||||
body,
|
||||
/UPDATE partners SET account_id = '' WHERE id = \?/,
|
||||
"空 accountId 表示解绑",
|
||||
/UPDATE partners SET account_id = '', released_account_id = \? WHERE id = \?/,
|
||||
"空 accountId 表示解绑,并记录释放的资源账号避免默认合作方同步回填",
|
||||
);
|
||||
assert.match(body, /群主合作方不绑定 KOC 资源/);
|
||||
});
|
||||
|
||||
126
tests/pool-partner-default.test.mjs
Normal file
126
tests/pool-partner-default.test.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const resourceWrite = readFileSync(
|
||||
new URL("../lib/resource-write.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const bootstrapRoute = readFileSync(
|
||||
new URL("../app/api/bootstrap/route.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const resourcesImport = readFileSync(
|
||||
new URL("../app/api/resources-import/route.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const resourcesInsert = readFileSync(
|
||||
new URL("../app/api/resources-insert/route.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const actionRoute = readFileSync(
|
||||
new URL("../app/api/action/route.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const migration = readFileSync(
|
||||
new URL("../mysql/0023_partner_released_account.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
function section(source, action) {
|
||||
const start = source.indexOf(`"${action}"`);
|
||||
assert.ok(start >= 0, `action ${action} 不存在于路由源码`);
|
||||
const next = source.indexOf("} else if (body.action ===", start);
|
||||
return source.slice(start, next === -1 ? undefined : next);
|
||||
}
|
||||
|
||||
test("ensurePartnersForPoolAccounts 为资源池未绑定账号创建默认合作方", () => {
|
||||
assert.match(resourceWrite, /export async function ensurePartnersForPoolAccounts/);
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/NOT EXISTS \(SELECT 1 FROM partners p WHERE p\.account_id = a\.id\)/,
|
||||
"只为尚未绑定合作方的资源账号建档",
|
||||
);
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/INSERT OR IGNORE INTO partners/,
|
||||
"并发同步幂等:主键冲突直接忽略",
|
||||
);
|
||||
assert.match(resourceWrite, /'external_koc'/, "默认合作方为外部KOC类型");
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/partner-koc-\$\{hashText/,
|
||||
"合作方 id 由账号身份派生,与领取流程的前缀约定一致",
|
||||
);
|
||||
});
|
||||
|
||||
test("默认合作方同步尊重运营显式解绑(released_account_id 不回填)", () => {
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/NOT EXISTS \(\s*SELECT 1 FROM partners r WHERE r\.released_account_id = a\.id\s*\)/,
|
||||
"被显式解绑/顶替过的资源账号不再自动加入合作方",
|
||||
);
|
||||
assert.match(migration, /ADD COLUMN `released_account_id`/);
|
||||
assert.match(migration, /partners_released_account_idx/);
|
||||
});
|
||||
|
||||
test("空壳默认合作方的名称随昵称补全刷新且不碰有业绩合作方", () => {
|
||||
assert.match(resourceWrite, /async function refreshPoolPartnerNames/);
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/NOT EXISTS \(SELECT 1 FROM claims c WHERE c\.partner_id = p\.id\)/,
|
||||
"有领取记录的合作方不刷新名称",
|
||||
);
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/NOT EXISTS \(SELECT 1 FROM distributions d WHERE d\.partner_id = p\.id\)/,
|
||||
"有分发记录的合作方不刷新名称",
|
||||
);
|
||||
assert.match(
|
||||
resourceWrite,
|
||||
/p\.wecom_external_user_id IS NULL OR p\.wecom_external_user_id = ''/,
|
||||
"已绑企微的合作方不刷新名称",
|
||||
);
|
||||
});
|
||||
|
||||
test("bootstrap 在返回面板数据前同步默认合作方", () => {
|
||||
const syncIndex = bootstrapRoute.indexOf("await ensurePartnersForPoolAccounts");
|
||||
const dashboardIndex = bootstrapRoute.indexOf("await getDashboardData(");
|
||||
assert.ok(syncIndex >= 0, "bootstrap 需要调用默认合作方同步");
|
||||
assert.ok(
|
||||
dashboardIndex > syncIndex,
|
||||
"同步必须发生在 getDashboardData 之前,首次加载即可见",
|
||||
);
|
||||
});
|
||||
|
||||
test("资源导入与手动新增后都会触发默认合作方同步", () => {
|
||||
assert.match(
|
||||
resourcesImport,
|
||||
/runInBackground\(\s*ensurePartnersForPoolAccounts\(\)/,
|
||||
"批量导入 commit 后同步",
|
||||
);
|
||||
assert.match(
|
||||
resourcesInsert,
|
||||
/runInBackground\(\s*ensurePartnersForPoolAccounts\(\)/,
|
||||
"手动新增后同步",
|
||||
);
|
||||
});
|
||||
|
||||
test("partner_bind_account 换绑时可顶替空壳占用方", () => {
|
||||
const body = section(actionRoute, "partner_bind_account");
|
||||
assert.match(body, /isShellAccountHolder/, "占用方为空壳时判断是否可顶替");
|
||||
assert.match(
|
||||
body,
|
||||
/UPDATE partners SET account_id = \?, released_account_id = '' WHERE id = \?/,
|
||||
"绑定目标时清掉自身的释放标记",
|
||||
);
|
||||
const helper = actionRoute.slice(
|
||||
actionRoute.indexOf("async function isShellAccountHolder"),
|
||||
);
|
||||
assert.match(helper, /SELECT 1 FROM claims WHERE partner_id = \? LIMIT 1/);
|
||||
assert.match(
|
||||
helper,
|
||||
/SELECT 1 FROM distributions WHERE partner_id = \? LIMIT 1/,
|
||||
"有领取或分发记录的占用方不可顶替,仍走人工解绑",
|
||||
);
|
||||
});
|
||||
@@ -31,7 +31,7 @@ test("merges distribution detail into the task center", async () => {
|
||||
|
||||
assert.match(adminApp, /查看明细/);
|
||||
assert.match(adminApp, /function TaskDistributionDetail/);
|
||||
assert.match(adminApp, /选择一个数据回收任务/);
|
||||
assert.match(adminApp, /选择一个任务进入回收队列/);
|
||||
assert.doesNotMatch(adminApp, /"distributions"/);
|
||||
assert.doesNotMatch(adminApp, /选择一个分发任务/);
|
||||
assert.doesNotMatch(adminApp, /distribution-task-/);
|
||||
@@ -87,7 +87,7 @@ test("organizes distribution and recovery by task and imports the verified Feish
|
||||
]);
|
||||
const snapshot = JSON.parse(snapshotText);
|
||||
|
||||
assert.match(adminApp, /选择一个数据回收任务/);
|
||||
assert.match(adminApp, /选择一个任务进入回收队列/);
|
||||
assert.match(adminApp, /读取表格/);
|
||||
assert.match(adminApp, /当前仅展示/);
|
||||
assert.match(adminApp, /release_distribution/);
|
||||
|
||||
57
tests/task-center-stage.test.mjs
Normal file
57
tests/task-center-stage.test.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const adminApp = readFileSync(new URL("../app/admin-app.tsx", import.meta.url), "utf8");
|
||||
const adminCss = readFileSync(new URL("../app/globals.css", import.meta.url), "utf8");
|
||||
|
||||
test("任务卡单一入口:发布/回收面板整块可点,全卡只有一个「查看明细」", () => {
|
||||
const start = adminApp.indexOf("function TaskRowCard");
|
||||
const end = adminApp.indexOf("function TasksPage");
|
||||
const body = adminApp.slice(start, end);
|
||||
// 两块阶段面板都可整块点击进入任务明细
|
||||
assert.match(body, /className="master-panel publish clickable"/);
|
||||
assert.match(body, /className="master-panel recovery clickable"/);
|
||||
// 回收面板不再有独立的「查看明细」按钮:全卡仅一处
|
||||
assert.equal(body.match(/查看明细/g)?.length, 1);
|
||||
// 未开通回收的面板保持不可点、保留开通入口
|
||||
assert.match(body, /className="master-panel ghost"/);
|
||||
assert.match(body, /开通回收 ›/);
|
||||
});
|
||||
|
||||
test("详情页自述身份:动态 label 说明任务阶段构成", () => {
|
||||
assert.match(adminApp, /任务详情 · 分发与回收一体/);
|
||||
assert.match(adminApp, /任务详情 · 仅分发/);
|
||||
// 头部阶段徽章:含回收(带截止日或同日说明)/ 未开通回收
|
||||
assert.match(adminApp, /未开通回收"\s*: task\.recovery_due_at/);
|
||||
assert.match(adminApp, /含回收 · 回收截止 \$\{formatDate\(task\.recovery_due_at\)\}/);
|
||||
assert.match(adminApp, /含回收 · 与发布同日截止/);
|
||||
});
|
||||
|
||||
test("单一列表按阶段筛选:不拆两套列表,用筛选 chips 过滤生命周期阶段", () => {
|
||||
const start = adminApp.indexOf("function TaskDistributionDetail");
|
||||
const end = adminApp.indexOf("\nfunction ", start + 20);
|
||||
const body = adminApp.slice(start, end);
|
||||
assert.match(body, /const \[stageFilter, setStageFilter\] = useState<StageFilterKey>\("all"\);/);
|
||||
assert.match(body, /key: "pending_publish",\s*\n\s*label: "待发布"/);
|
||||
assert.match(body, /label: "已发布待数据"/);
|
||||
assert.match(body, /label: "待收截图"/);
|
||||
// 待收截图仅在开通回收的任务出现
|
||||
assert.match(body, /\.\.\.\(recoveryEnabled\s*\?\s*\[\{/);
|
||||
// 表格使用过滤后的列表;筛选落空时给出专用空态文案
|
||||
assert.match(body, /distributions=\{visibleDistributions\}/);
|
||||
assert.match(body, /当前筛选下没有分发记录/);
|
||||
assert.match(body, /stage-filter-tabs/);
|
||||
});
|
||||
|
||||
test("阶段面板与筛选 chips 的样式", () => {
|
||||
assert.match(adminCss, /^\.master-panel\.clickable \{/m);
|
||||
assert.match(adminCss, /^\.master-panel\.clickable:hover \{/m);
|
||||
assert.match(adminCss, /^\.stage-filter-tabs \{/m);
|
||||
assert.match(adminCss, /^\.stage-filter-tabs button\.active \{/m);
|
||||
});
|
||||
|
||||
test("数据回收页入口不再使用「数据回收任务」的说法", () => {
|
||||
assert.match(adminApp, /选择一个任务进入回收队列/);
|
||||
assert.doesNotMatch(adminApp, /选择一个数据回收任务/);
|
||||
});
|
||||
@@ -46,7 +46,8 @@ test("admin waiting label follows the task collection plan", async () => {
|
||||
readFile(new URL("../lib/mvp-db.ts", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(adminApp, /waitingCollectionLabel/);
|
||||
assert.match(adminApp, /待设置采集计划/);
|
||||
// 未设置计划时走默认 7 天兜底,等待文案随之说明而不是停在“待设置”
|
||||
assert.match(adminApp, /等待D1 · 默认7天/);
|
||||
assert.match(adminApp, /等待D\$\{Math\.min\(\.\.\.days\)\}/);
|
||||
assert.doesNotMatch(adminApp, /"等待D2"/);
|
||||
// 分发查询带上任务级采集计划字段
|
||||
|
||||
Reference in New Issue
Block a user