Files
Haven/test/messageGrouping.test.js

100 lines
5.5 KiB
JavaScript

'use strict';
// One grouping rule for channels, DMs, the pop-out DM and threads: the same
// author within five minutes folds under the message before it.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const { pathToFileURL } = require('node:url');
const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8');
let groupsWithPrevious, groupInfoFromElement, stampGroupSource, GROUP_WINDOW_MS;
test.before(async () => {
({ groupsWithPrevious, groupInfoFromElement, stampGroupSource, GROUP_WINDOW_MS } =
await import(pathToFileURL(path.join(__dirname, '..', 'public/js/modules/message-grouping.js')).href));
});
// Midday local time, so a few minutes either way stay on the same day.
const NOON = new Date(2026, 9, 16, 12, 0, 0).getTime();
const at = (ms) => new Date(ms).toISOString();
const msg = (over = {}) => ({ user_id: 2, username: 'bob', created_at: at(NOON), ...over });
test('the same author within five minutes groups', () => {
assert.equal(GROUP_WINDOW_MS, 5 * 60 * 1000);
assert.equal(groupsWithPrevious(msg(), msg({ created_at: at(NOON + 1000) })), true);
assert.equal(groupsWithPrevious(msg(), msg({ created_at: at(NOON + 4 * 60 * 1000 + 59000) })), true);
});
test('a different author breaks the group', () => {
assert.equal(groupsWithPrevious(msg(), msg({ user_id: 3, username: 'carol' })), false);
});
test('five minutes or more breaks the group', () => {
assert.equal(groupsWithPrevious(msg(), msg({ created_at: at(NOON + GROUP_WINDOW_MS) })), false);
assert.equal(groupsWithPrevious(msg(), msg({ created_at: at(NOON + 20 * 60 * 1000) })), false);
});
test('a reply starts its own group', () => {
assert.equal(groupsWithPrevious(msg(), msg({ created_at: at(NOON + 1000), reply_to: 7 })), false);
});
test('a new day starts its own group, even a minute later', () => {
const late = new Date(2026, 9, 16, 23, 59, 30).getTime();
const prev = msg({ created_at: at(late) });
assert.equal(groupsWithPrevious(prev, msg({ created_at: at(late + 60 * 1000) })), false);
assert.equal(groupsWithPrevious(prev, msg({ created_at: at(late + 20 * 1000) })), true);
});
test('system lines, personas, webhooks, imports and /break never fold', () => {
const next = { created_at: at(NOON + 1000) };
assert.equal(groupsWithPrevious(msg({ type: 'notice' }), msg(next)), false);
assert.equal(groupsWithPrevious(msg(), msg({ ...next, persona_id: 4, persona_username: 'Wizard' })), false);
assert.equal(groupsWithPrevious(msg({ persona_id: 4, persona_username: 'Wizard' }), msg({ ...next, persona_id: 4, persona_username: 'Wizard' })), true);
assert.equal(groupsWithPrevious(msg(), msg({ ...next, is_webhook: 1, webhook_username: 'Hook' })), false);
assert.equal(groupsWithPrevious(msg(), msg({ ...next, imported_from: 'discord' })), false);
assert.equal(groupsWithPrevious(msg(), msg({ ...next, break_chain: 1 })), false);
assert.equal(groupsWithPrevious(null, msg(next)), false);
assert.equal(groupsWithPrevious(msg({ created_at: 'not a date' }), msg(next)), false);
});
test('a message rebuilt from its element groups like the message itself', () => {
const el = { dataset: { userId: '2', msgId: '10', username: 'bob', time: at(NOON) } };
stampGroupSource(el, { is_webhook: 1, webhook_username: 'Hook', imported_from: 'discord' });
const prev = groupInfoFromElement(el);
assert.equal(prev.user_id, 2);
assert.equal(prev.is_webhook, 1);
assert.equal(groupsWithPrevious(prev, msg({ created_at: at(NOON + 1000), is_webhook: 1, webhook_username: 'Hook', imported_from: 'discord' })), true);
assert.equal(groupsWithPrevious(prev, msg({ created_at: at(NOON + 1000) })), false);
// A divider or system line has no author or id, so nothing groups with it.
assert.equal(groupInfoFromElement({ dataset: {} }), null);
assert.equal(groupInfoFromElement(null), null);
});
test('channels, DMs, the pop-out DM and threads all use the shared rule', () => {
const messages = read('public/js/modules/app-messages.js');
assert.match(messages, /this\._groupsWithPrevious\(prevMsg, msg\)/);
assert.equal((messages.match(/this\._groupInfoFromElement\(/g) || []).length, 2);
assert.equal((messages.match(/this\._stampGroupSource\(el, msg\);/g) || []).length, 2);
assert.doesNotMatch(messages, /5 \* 60 \* 1000/);
// The pop-out DM goes by its own channel, not the one open behind it.
assert.match(messages, /const viewCode = \(msg && msg\._isDmRender && this\._activeDMPip\) \|\| this\.currentChannel;/);
const pip = read('public/js/modules/app-pip-panels.js');
assert.match(pip, /const prevMsg = this\._groupInfoFromElement\(list\.lastElementChild\);/);
const threads = read('public/js/modules/app-threads.js');
assert.match(threads, /threadCompact = this\._groupsWithPrevious\(this\._groupInfoFromElement\(prevEl\)/);
assert.match(threads, /this\._stampGroupSource\(el, msg\);/);
const app = read('public/js/app.js');
assert.match(app, /import MessageGroupingMethods from '\.\/modules\/message-grouping\.js\?v=[\d.]+';/);
assert.match(app, /^ MessageGroupingMethods,$/m);
});
test('grouped thread replies sit as close as the channel\'s', () => {
const css = read('public/css/style-emoji-reactions-threads.css');
assert.match(css, /\.thread-message:has\(\+ \.thread-message\.thread-compact\) \{\s*margin-bottom: 0\.125rem;/);
assert.match(css, /\.thread-message\.thread-compact \.thread-msg-row \{\s*padding-top: 0\.0625rem;\s*padding-bottom: 0\.0625rem;/);
assert.match(css, /\.thread-msg-avatar\.thread-msg-compact-spacer \{\s*height: auto;/);
});