Files
creator-hub/web/tests/comment-aggregation.test.cjs
T

469 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const assert = require("node:assert/strict");
const fs = require("node:fs");
const Module = require("node:module");
const path = require("node:path");
const test = require("node:test");
const ts = require("../node_modules/typescript");
const target = path.resolve(
__dirname,
"../src/pages/creator/comments/index.tsx",
);
const originalLoad = Module._load;
const createElement = (type, props, ...children) => ({
type,
props: { ...props, children: children.length === 1 ? children[0] : children },
});
const names = [
"Alert",
"Button",
"Card",
"Flex",
"Form",
"Select",
"Table",
"Tabs",
"Typography",
];
const components = Object.fromEntries(names.map((name) => [name, name]));
components.Form = { Item: "Form.Item" };
components.Typography = { Text: "Text", Link: "Link" };
const same = (a, b) =>
a &&
b &&
a.length === b.length &&
a.every((value, index) => Object.is(value, b[index]));
let harness;
Module._load = function (request, parent, isMain) {
if (parent?.filename === target) {
if (request === "react") return harness.react;
if (request === "antd") return components;
if (request === "@ant-design/icons")
return { ReloadOutlined: "ReloadOutlined" };
if (request === "@/services/api")
return {
getList: ({ resource, ...query }) =>
harness.listResource(resource, query),
creatorGet: (...args) => harness.creatorGet(...args),
};
if (request === "@/components/creator/WorksGallery")
return { WorkCover: "WorkCover" };
if (request === "@/utils/helpers")
return {
conflictMessage: (error, fallback) => error?.message || fallback,
};
}
return originalLoad.call(this, request, parent, isMain);
};
function load() {
const source = ts.transpileModule(
"import React from 'react';\n" + fs.readFileSync(target, "utf8"),
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
jsx: ts.JsxEmit.React,
esModuleInterop: true,
},
},
).outputText;
const mod = new Module(target, module);
mod.filename = target;
mod.paths = module.paths;
mod._compile(source, target);
return mod.exports;
}
function makeHarness() {
const state = [];
const effects = [];
const pending = [];
const calls = [];
const queries = [];
let cursor = 0;
const h = {
state,
effects,
calls,
queries,
react: {
createElement,
useState(initial) {
const index = cursor++;
if (!(index in state)) state[index] = initial;
return [
state[index],
(value) => {
state[index] =
typeof value === "function" ? value(state[index]) : value;
},
];
},
useEffect(fn, deps) {
const index = cursor++;
if (!same(effects[index]?.deps, deps))
pending.push(() => {
effects[index]?.cleanup?.();
effects[index] = { deps, cleanup: fn() };
});
},
},
creatorGet: async () => [
{
id: "account-1",
name: "我的账号",
nickname: "竞品账号",
platform_account_key: "uid-1",
},
],
listResource: async (resource, query) => {
calls.push(resource);
queries.push(query);
return { data: [], total: 0 };
},
render(Component, props) {
cursor = 0;
const tree = Component(props);
while (pending.length) pending.shift()();
return tree;
},
cleanup() {
effects.forEach((effect) => effect?.cleanup?.());
},
};
harness = h;
return h;
}
const tick = async () => {
for (let i = 0; i < 8; i++) await Promise.resolve();
};
function nodes(tree, type) {
const matches = [];
function walk(node) {
if (!node) return;
if (Array.isArray(node)) {
node.forEach(walk);
return;
}
if (typeof node !== "object") return;
if (node.type === type) matches.push(node);
walk(node.props?.children);
}
walk(tree);
return matches;
}
function deferred() {
let resolve, reject;
const promise = new Promise((yes, no) => {
resolve = yes;
reject = no;
});
return { promise, resolve, reject };
}
test("two independently retained tabs and seven relative publication windows", async () => {
const h = makeHarness();
const page = load();
const root = page.default();
const tabs = nodes(root, "Tabs")[0];
assert.deepEqual(
tabs.props.items.map((item) => [item.key, item.label]),
[
["owned", "我的作品评论"],
["competitor", "竞品作品评论"],
],
);
assert.equal(tabs.props.destroyOnHidden, false);
const props = { sourceType: "owned" };
h.render(page.CommentAggregationTab, props);
await tick();
const tree = h.render(page.CommentAggregationTab, props);
assert.deepEqual(h.calls, ["creator-comment-aggregation"]);
assert.deepEqual(h.queries[0], {
page: 1,
pageSize: 25,
filters: { source_type: "owned", source_id: undefined, hours: 24 },
});
const select = nodes(tree, "Select").find(
(node) => node.props["aria-label"] === "评论发布时间",
);
assert.deepEqual(
select.props.options.map((option) => option.value),
[1, 6, 12, 24, 72, 120, 168],
);
const accounts = nodes(tree, "Select").find(
(node) => node.props["aria-label"] === "所属账号",
);
assert.deepEqual(accounts.props.options, [
{ value: "account-1", label: "我的账号" },
]);
h.cleanup();
});
test("account dropdown never uses UID or internal IDs as names in either tab", async () => {
for (const sourceType of ["owned", "competitor"]) {
const h = makeHarness();
const page = load();
h.creatorGet = async () => [
{
id: "account-1",
name: "我的账号",
nickname: "竞品账号",
platform_account_key: "uid-1",
},
{
id: "account-2",
name: "",
nickname: "",
platform_account_key: "uid-2",
},
];
h.render(page.CommentAggregationTab, { sourceType });
await tick();
const tree = h.render(page.CommentAggregationTab, { sourceType });
const select = nodes(tree, "Select").find(
(node) => node.props["aria-label"] === "所属账号",
);
assert.deepEqual(select.props.options, [
{
value: "account-1",
label: sourceType === "owned" ? "我的账号" : "竞品账号",
},
{ value: "account-2", label: "未命名账号" },
]);
h.cleanup();
}
});
test("account and time changes reset pagination and preserve page size", async () => {
const h = makeHarness();
const page = load();
const props = { sourceType: "competitor" };
h.render(page.CommentAggregationTab, props);
await tick();
let tree = h.render(page.CommentAggregationTab, props);
nodes(tree, "Table")[0].props.onChange({ current: 1, pageSize: 50 });
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
nodes(tree, "Table")[0].props.onChange({ current: 3, pageSize: 50 });
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.equal(h.queries.at(-1).page, 3);
assert.equal(h.queries.at(-1).pageSize, 50);
nodes(tree, "Select")
.find((node) => node.props["aria-label"] === "所属账号")
.props.onChange("account-1");
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.deepEqual(h.queries.at(-1), {
page: 1,
pageSize: 50,
filters: { source_type: "competitor", source_id: "account-1", hours: 24 },
});
nodes(tree, "Select")
.find((node) => node.props["aria-label"] === "评论发布时间")
.props.onChange(6);
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.equal(h.queries.at(-1).filters.hours, 6);
assert.equal(h.queries.at(-1).page, 1);
nodes(tree, "Button")
.find((node) => node.props.children === "重置")
.props.onClick();
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.equal(h.queries.at(-1).filters.hours, 24);
assert.equal(h.queries.at(-1).filters.source_id, undefined);
nodes(tree, "Table")[0].props.onChange({ current: 2, pageSize: 25 });
h.render(page.CommentAggregationTab, props);
await tick();
assert.equal(h.queries.at(-1).page, 1);
assert.equal(h.queries.at(-1).pageSize, 25);
h.cleanup();
});
test("renders commenter, account and work metadata with server pagination", async () => {
const h = makeHarness();
const page = load();
const props = { sourceType: "owned" };
const item = {
id: "comment-1",
content: "评论内容",
author_name: "读者",
author_uid: "reader-uid",
published_at: "2026-09-20T04:00:00Z",
source_name: "账号",
source_account_key: "uid-1",
source_id: "account-1",
work_title: "作品标题",
work_id: "work-1",
work_key: "work-key",
work_url: "https://www.douyin.com/video/work-key",
};
h.listResource = async () => ({ data: [item], total: 101 });
h.render(page.CommentAggregationTab, props);
await tick();
const tree = h.render(page.CommentAggregationTab, props);
const table = nodes(tree, "Table")[0];
assert.deepEqual(table.props.dataSource, [item]);
assert.equal(table.props.pagination.total, 101);
assert.equal(table.props.pagination.current, 1);
assert.deepEqual(
table.props.columns.map((column) => column.title),
["评论发布时间", "评论者", "评论内容", "所属账号", "对应作品"],
);
const account = table.props.columns
.find((column) => column.title === "所属账号")
.render(undefined, item);
assert.equal(nodes(account, "Text")[0].props.children, "账号");
assert.equal(nodes(account, "Text").length, 1);
const work = table.props.columns
.find((column) => column.title === "对应作品")
.render(undefined, item);
const link = nodes(work, "Link")[0];
assert.equal(link.props.href, item.work_url);
assert.equal(link.props.target, "_blank");
assert.equal(link.props["aria-label"], "查看作品:作品标题");
assert.equal(link.props.style.width, 48);
assert.equal(nodes(work, "Text").length, 0);
assert.deepEqual(nodes(work, "WorkCover")[0].props, {
workID: "work-1",
title: "作品标题",
children: [],
});
const author = table.props.columns
.find((column) => column.title === "评论者")
.render(undefined, item);
assert.equal(nodes(author, "Text")[1].props.children, "UID:reader-uid");
const empty = {
...item,
source_name: "",
source_account_key: "",
author_name: "",
author_uid: "",
work_title: "",
work_url: "",
};
assert.equal(
nodes(
table.props.columns
.find((column) => column.title === "所属账号")
.render(undefined, empty),
"Text",
)[0].props.children,
"未命名账号",
);
const untitledWork = table.props.columns
.find((column) => column.title === "对应作品")
.render(undefined, empty);
assert.equal(
nodes(untitledWork, "Link")[0].props.href,
"https://www.douyin.com/video/work-key",
);
assert.equal(
nodes(untitledWork, "Link")[0].props["aria-label"],
"查看作品:work-key",
);
assert.equal(nodes(untitledWork, "WorkCover")[0].props.title, "作品封面");
h.cleanup();
});
test("obsolete responses do not replace current filters and refresh retries errors", async () => {
const h = makeHarness();
const page = load();
const props = { sourceType: "owned" };
const old = deferred();
const fresh = deferred();
let count = 0;
h.listResource = () => (++count === 1 ? old.promise : fresh.promise);
h.render(page.CommentAggregationTab, props);
await tick();
let tree = h.render(page.CommentAggregationTab, props);
nodes(tree, "Select")
.find((node) => node.props["aria-label"] === "评论发布时间")
.props.onChange(1);
h.render(page.CommentAggregationTab, props);
fresh.resolve({ data: [{ id: "fresh" }], total: 1 });
await tick();
old.resolve({ data: [{ id: "old" }], total: 9 });
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.deepEqual(nodes(tree, "Table")[0].props.dataSource, [{ id: "fresh" }]);
assert.equal(nodes(tree, "Table")[0].props.pagination.total, 1);
const errors = [];
const previous = console.error;
console.error = (...args) => errors.push(args);
try {
h.listResource = async () => {
throw new Error("读取失败");
};
nodes(tree, "Button")
.find((node) => node.props.children === "刷新")
.props.onClick();
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.equal(nodes(tree, "Alert")[0].props.title, "评论读取失败");
assert.equal(nodes(tree, "Alert")[0].props.description, "读取失败");
assert.equal(errors.length, 1);
assert.deepEqual(nodes(tree, "Table")[0].props.dataSource, []);
h.listResource = async () => ({ data: [{ id: "retried" }], total: 1 });
nodes(tree, "Button")
.find((node) => node.props.children === "刷新")
.props.onClick();
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.equal(nodes(tree, "Alert").length, 0);
assert.deepEqual(nodes(tree, "Table")[0].props.dataSource, [
{ id: "retried" },
]);
} finally {
console.error = previous;
h.cleanup();
}
});
test("account errors remain visible and disable account filter until retry", async () => {
const h = makeHarness();
const page = load();
const props = { sourceType: "competitor" };
const previous = console.error;
console.error = () => {};
try {
h.creatorGet = async () => {
throw new Error("账号接口失败");
};
h.render(page.CommentAggregationTab, props);
await tick();
let tree = h.render(page.CommentAggregationTab, props);
assert.equal(nodes(tree, "Alert")[0].props.title, "账号读取失败");
assert.equal(
nodes(tree, "Select").find(
(node) => node.props["aria-label"] === "所属账号",
).props.disabled,
true,
);
h.creatorGet = async () => [];
nodes(tree, "Button")
.find((node) => node.props.children === "刷新")
.props.onClick();
h.render(page.CommentAggregationTab, props);
await tick();
tree = h.render(page.CommentAggregationTab, props);
assert.equal(nodes(tree, "Alert").length, 0);
assert.equal(
nodes(tree, "Select").find(
(node) => node.props["aria-label"] === "所属账号",
).props.disabled,
false,
);
} finally {
console.error = previous;
h.cleanup();
}
});