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(); } });