112 lines
7.5 KiB
JavaScript
112 lines
7.5 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { readFileSync } = require('node:fs');
|
|
const { resolve } = require('node:path');
|
|
const Module = require('node:module');
|
|
const ts = require('typescript');
|
|
const file = resolve(__dirname, '../src/utils/private-messages.ts');
|
|
const loaded = new Module(file, module);
|
|
loaded._compile(ts.transpileModule(readFileSync(file, 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText, file);
|
|
const util = loaded.exports;
|
|
|
|
const contact = (account_id, peer_uid, time = '', last_text = '') => ({ account_id, peer_uid, account_name: '', peer_name: '', last_text, last_message_at: time, message_count: 0 });
|
|
const message = (id, time = '', overrides = {}) => ({ id, account_id: 'a', peer_uid: '100', direction: 'inbound', message_type: 'text', text: '', state: 'succeeded', error: '', message_at: time, created_at: '', ...overrides });
|
|
|
|
test('conversation and message API queries preserve pagination, source and encoded peers', () => {
|
|
assert.equal(util.conversationQuery({}, 2, 25), 'page=2&page_size=25');
|
|
const query = new URLSearchParams(util.conversationQuery({ accountId: 'a/b' }, 1, 25));
|
|
assert.equal(query.get('account_id'), 'a/b');
|
|
const messages = new URLSearchParams(util.messagesQuery('a/b', '100+20', 3, 20));
|
|
assert.equal(messages.get('account_id'), 'a/b'); assert.equal(messages.get('peer_uid'), '100+20');
|
|
assert.equal(messages.get('page'), '3'); assert.equal(messages.get('page_size'), '20');
|
|
});
|
|
|
|
test('conversation identity includes account and peer, never peer alone', () => {
|
|
assert.equal(util.conversationKey(contact('a', '100')), 'a:100');
|
|
assert.notEqual(util.conversationKey(contact('a', '100')), util.conversationKey(contact('b', '100')));
|
|
assert.notEqual(util.conversationKey(contact('a', '100')), util.conversationKey(contact('a', '200')));
|
|
});
|
|
|
|
test('contact pages merge by compound key, update summaries, sort newest first and retain older rows', () => {
|
|
const old = contact('a', '100', '2026-06-01T10:00:00Z');
|
|
const updated = { ...old, last_text: '最新摘要', last_message_at: '2026-06-01T12:00:00Z' };
|
|
const previous = [old, contact('b', '100', '2026-06-01T11:00:00Z')];
|
|
const result = util.mergeConversations(previous, [updated, updated]);
|
|
assert.deepEqual(result.map(util.conversationKey), ['a:100', 'b:100']);
|
|
assert.equal(result[0].last_text, '最新摘要'); assert.equal(previous[0].last_text, '');
|
|
assert.equal(util.mergeConversations(result, []).length, 2);
|
|
});
|
|
|
|
test('missing and invalid contact times stay behind known times, equal-time ordering is deterministic', () => {
|
|
const rows = [contact('b', '100'), contact('c', '100', 'invalid'), contact('a', '100', '2026-06-01T12:00:00Z')];
|
|
assert.deepEqual(util.mergeConversations([], rows).map(util.conversationKey), ['a:100', 'b:100', 'c:100']);
|
|
});
|
|
|
|
test('late conversation snapshots never roll back newer real message times or summaries', () => {
|
|
const fresh = contact('b', '100', '2026-06-01T12:00:10Z', '最新收发消息');
|
|
const old = contact('b', '100', '2026-06-01T12:00:01Z', '旧分页摘要');
|
|
const other = contact('a', '100', '2026-06-01T12:00:05Z');
|
|
const result = util.mergeConversations([fresh, other], [old]);
|
|
assert.deepEqual(result.map(util.conversationKey), ['b:100', 'a:100']);
|
|
assert.equal(result[0].last_text, '最新收发消息');
|
|
assert.equal(result[0].last_message_at, fresh.last_message_at);
|
|
const duplicates = util.mergeConversations([], [old, fresh, other]);
|
|
assert.equal(duplicates[0].last_text, '最新收发消息');
|
|
assert.equal(duplicates.length, 2);
|
|
const incomplete = util.mergeConversations([fresh], [contact('b', '100', '', '尚无真实时间')]);
|
|
assert.equal(incomplete[0].last_message_at, fresh.last_message_at);
|
|
const renamed = util.mergeConversations([fresh], [{ ...fresh, peer_name: '实时昵称' }, { ...fresh, peer_name: '过期别名' }]);
|
|
assert.equal(renamed[0].peer_name, '实时昵称');
|
|
});
|
|
|
|
test('unknown conversation times stay last even before the Unix epoch, and ties use stable compound keys', () => {
|
|
const rows = [contact('a', '2'), contact('z', '1', '1970-01-01T00:00:00Z'), contact('y', '1', '1969-12-31T23:59:59Z'), contact('a', '10'), contact('a0', '1'), contact('b', '1', 'invalid')];
|
|
const expected = ['z:1', 'y:1', 'a:10', 'a:2', 'a0:1', 'b:1'];
|
|
assert.deepEqual(util.mergeConversations([], rows).map(util.conversationKey), expected);
|
|
assert.deepEqual(util.mergeConversations([], rows.slice().reverse()).map(util.conversationKey), expected);
|
|
});
|
|
|
|
test('older message pages merge without duplicates, ascending times and updated send results', () => {
|
|
const latest = [message('3', '2026-06-01T12:00:03Z'), message('2', '2026-06-01T12:00:02Z', { state: 'sending' })];
|
|
const updated = message('2', '2026-06-01T12:00:02Z', { state: 'succeeded' });
|
|
const result = util.mergeMessages(latest, [updated, message('1', '2026-06-01T12:00:01Z'), updated]);
|
|
assert.deepEqual(result.map((m) => m.id), ['1', '2', '3']); assert.equal(result[1].state, 'succeeded');
|
|
assert.equal(latest[1].state, 'sending'); assert.equal(util.mergeMessages(result, []).length, 3);
|
|
});
|
|
|
|
test('message ordering handles real storage times and exact large numeric IDs without number rounding', () => {
|
|
const rows = [message('9007199254740993', '', { created_at: '2026-06-01T12:00:00Z' }), message('9007199254740992', '', { created_at: '2026-06-01T12:00:00Z' }), message('2'), message('10')];
|
|
assert.deepEqual(util.mergeMessages([], rows).map((m) => m.id), ['2', '10', '9007199254740992', '9007199254740993']);
|
|
});
|
|
|
|
test('draft updates isolate sessions and success only clears the exact submitted revision', () => {
|
|
let drafts = util.updateDraft({}, 'a:100', '甲');
|
|
drafts = util.updateDraft(drafts, 'b:100', '乙');
|
|
const original = drafts;
|
|
const cleared = util.clearSentDraft(drafts, 'a:100', drafts['a:100'].version);
|
|
assert.equal(cleared['a:100'].text, ''); assert.equal(cleared['b:100'].text, '乙');
|
|
assert.equal(original['a:100'].text, '甲');
|
|
const newer = util.updateDraft(drafts, 'a:100', '新输入');
|
|
assert.strictEqual(util.clearSentDraft(newer, 'a:100', drafts['a:100'].version), newer);
|
|
assert.strictEqual(util.clearSentDraft(drafts, 'missing', 1), drafts);
|
|
});
|
|
|
|
test('retyping the same value counts as new input and cannot be erased by an old success', () => {
|
|
const original = util.updateDraft({}, 'a:100', '相同文本');
|
|
const changed = util.updateDraft(util.updateDraft(original, 'a:100', '其他输入'), 'a:100', '相同文本');
|
|
assert.strictEqual(util.clearSentDraft(changed, 'a:100', original['a:100'].version), changed);
|
|
});
|
|
|
|
test('direction, state and type labels preserve unknown server values without faking delivery or reads', () => {
|
|
assert.equal(util.messageDirectionLabel('inbound'), '收到'); assert.equal(util.messageDirectionLabel('outbound'), '发出');
|
|
assert.equal(util.messageDirectionLabel('other'), 'other');
|
|
for (const [state, label, tone] of [['succeeded', '已发送', 'success'], ['sending', '发送中', 'processing'], ['failed', '发送失败', 'error'], ['unknown', '结果未确认', 'warning']]) {
|
|
assert.equal(util.messageStateLabel(state), label); assert.equal(util.messageStateTone(state), tone);
|
|
}
|
|
assert.equal(util.messageStateLabel('other'), 'other'); assert.equal(util.messageStateTone('other'), 'default');
|
|
assert.equal(util.messageTypeLabel('text'), '文字'); assert.equal(util.messageTypeLabel('image'), '图片');
|
|
assert.equal(util.messageTypeLabel('unknown'), '未知类型'); assert.equal(util.messageTypeLabel('video'), 'video');
|
|
});
|