const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const vm = require('node:vm') const ts = require('typescript') const vue = require('vue') const { parse, compileTemplate } = require('vue/compiler-sfc') const root = path.join(__dirname, '../src/views/chat/list') const plain = (value) => JSON.parse(JSON.stringify(value)) const transpile = (source) => ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText const utilsContext = { exports: {}, URL, Intl, Date } vm.runInNewContext( transpile(fs.readFileSync(path.join(root, 'chat-utils.ts'), 'utf8')), utilsContext ) const utils = utilsContext.exports function createModerationState() { const context = { exports: {}, require: (name) => { if (name === 'vue') return vue throw new Error(name) } } vm.runInNewContext( transpile(fs.readFileSync(path.join(root, '../components/message-moderation.ts'), 'utf8')), context ) return context.exports } const deferred = () => { let resolve, reject const promise = new Promise((yes, no) => { resolve = yes reject = no }) return { promise, resolve, reject } } const conversation = (first = 1, second = 2) => ({ userA: { id: first, nickname: `用户${first}` }, userB: { id: second, nickname: `用户${second}` }, messageCount: 4, lastMessage: { id: 4, sendId: second, recvId: first, type: 0, status: 3, content: '你好', sendTime: 1793664000000 } }) function createHarness(component, overrides = {}, moderation = createModerationState()) { const calls = [] const lifecycle = {} const context = { exports: {}, defineOptions() {}, defineExpose(value) { context.exposed = value }, document: { activeElement: null }, HTMLElement: class {}, require(name) { if (name === 'vue') return { ...vue, onMounted(callback) { lifecycle.mounted = callback }, onBeforeUnmount(callback) { lifecycle.unmount = callback }, onDeactivated(callback) { lifecycle.deactivate = callback } } if (name === '@/api/chat/private-msg') return { async conversationPage(params) { calls.push(plain(params)) return { list: [], total: 0 } }, async page(params) { calls.push(plain(params)) return { list: [], total: 0 } }, ...overrides } if (name.endsWith('chat-utils')) return utils if (name.endsWith('message-moderation')) return moderation if (name.endsWith('.vue')) return {} throw new Error(`Unexpected import ${name}`) } } const source = fs.readFileSync(path.join(root, component), 'utf8') const { descriptor } = parse(source) const exports = component === 'index.vue' ? 'filters, applied, pagination, list, total, loading, error, getList, handleQuery, resetQuery, handleSizeChange, drawerRef, openConversation' : 'filters, applied, pagination, messages, total, loading, error, visible, getList, handleQuery, resetQuery, handleSizeChange, open, handleClose, restoreFocus, scrollRef, searchRef, focusSearch' const scope = vue.effectScope() scope.run(() => vm.runInNewContext( transpile(descriptor.scriptSetup.content) + `\nglobalThis.state = { ${exports} };`, context ) ) const unmount = lifecycle.unmount lifecycle.unmount = () => { unmount?.() scope.stop() } return { state: context.state, exposed: context.exposed, calls, lifecycle, moderation } } test('China date ranges include the entire final calendar day independently of machine timezone', () => { assert.deepEqual(plain(utils.toChinaDateRange(['2026-11-03', '2026-11-03'])), [ Date.parse('2026-11-02T16:00:00.000Z'), Date.parse('2026-11-03T15:59:59.999Z') ]) assert.equal(utils.toChinaDateRange(null), undefined) assert.equal(utils.toChinaDateRange(['2026-11-04', '2026-11-03']), undefined) assert.equal(utils.formatChatTime('2026-11-03 08:01:02'), '2026-11-03 08:01:02') assert.equal(utils.formatChatTime('2026-11-03T00:01:02Z'), '2026-11-03 08:01:02') }) test('media payload failures and dangerous protocols have safe fallbacks', () => { for (const value of [undefined, null, '', '{broken', 'null', '[]', '1']) assert.equal(utils.parseMedia(value), null) for (const url of [ 'javascript:alert(1)', 'data:text/html,hello', 'file:///etc/passwd', '//evil.example/x', 'https://user:pass@example.com/file', 'https://x.example/\nfile', '\\evil.example\\x' ]) { assert.equal(utils.safeResourceUrl(url, 'https://cdn.example/media/'), '') } assert.equal( utils.safeResourceUrl('a.png', 'https://cdn.example/media'), 'https://cdn.example/media/a.png' ) assert.equal(utils.safeResourceUrl('https://cdn.example/a.png'), 'https://cdn.example/a.png') assert.equal(utils.messagePreview({ type: 1, status: 2 }), '[已撤回/屏蔽] [图片]') assert.equal(utils.messagePreview({ type: 10, content: '1234', status: 0 }), '撤回了一条消息') assert.equal( utils.messagePreview({ type: 40, content: '通话时长 01:23', status: 3 }), '[语音通话] 通话时长 01:23' ) }) test('text links preserve text, keep HTML escaped, and support uppercase HTTP', () => { const input = ' HTTP://example.com?a=1&b=2, www.example.com。' const parts = utils.messageTextParts(input) assert.equal(parts.map((part) => part.text).join(''), input) assert.deepEqual(plain(parts.filter((part) => part.href).map((part) => part.href)), [ 'http://example.com/?a=1&b=2', 'https://www.example.com/' ]) assert.equal(utils.escapeMessageText('