waybetter-for-x/apps/desktop/tests/draftsPopover.test.ts
2026-10-01 06:01:13 +07:00

110 lines
5.8 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { Draft } from '../src/core/types.ts';
import { anchorRequest, isPostPath, withAlpha } from '../src/inject/drafts.ts';
import { charCount, contextLabel, highlight, isPostUrl, matchesQuery, popoverSections, relativeTime, rowText, step } from '../src/popover/popoverModel.ts';
const draft = (patch: Partial<Draft>): Draft => ({
id: 'd', accountId: 'unassigned-x', kind: 'post', title: '', text: 'hello', contextUrl: '', revision: 1,
status: 'active', createdAt: '2026-09-01T00:00:00Z', updatedAt: '2026-09-01T00:00:00Z', source: 'capture', ...patch,
});
const POST = 'https://x.com/jack/status/20';
test('popoverSections: this conversation first, then recent, each newest first', () => {
const drafts = [
draft({ id: 'old', updatedAt: '2026-09-01T00:00:00Z' }),
draft({ id: 'new', updatedAt: '2026-09-05T00:00:00Z' }),
draft({ id: 'here-old', contextUrl: `${POST}?s=20`, updatedAt: '2026-08-01T00:00:00Z' }),
draft({ id: 'here-new', contextUrl: `${POST}/`, updatedAt: '2026-08-02T00:00:00Z' }),
draft({ id: 'archived', status: 'archived', updatedAt: '2026-09-09T00:00:00Z' }),
draft({ id: 'blank', text: ' \n ', updatedAt: '2026-09-09T00:00:00Z' }),
];
const { sections, items } = popoverSections(drafts, POST, '', 'reply');
assert.deepEqual(sections.map(s => [s.id, s.label, s.items.map(d => d.id)]), [
['here', 'This conversation', ['here-new', 'here-old']],
['recent', 'Recent', ['new', 'old']],
]);
assert.deepEqual(items.map(d => d.id), ['here-new', 'here-old', 'new', 'old']);
});
test('popoverSections: no "here" section without matches; query relabels the rest', () => {
const drafts = [draft({ id: 'a', text: 'Ship the launch thread' }), draft({ id: 'b', text: 'nope' })];
const { sections } = popoverSections(drafts, 'https://x.com/home', 'launch');
assert.deepEqual(sections.map(s => [s.id, s.label, s.items.map(d => d.id)]), [['recent', 'Matches', ['a']]]);
assert.equal(popoverSections(drafts, 'https://x.com/home', 'zzz').items.length, 0);
assert.equal(popoverSections([draft({ id: 'h', contextUrl: 'https://x.com/home' })], 'https://x.com/home').sections[0]?.label, 'This page');
});
test('matchesQuery: every word, any order, title or text, case-insensitive', () => {
const d = draft({ title: 'Launch', text: 'Better Twttr ships today' });
assert.ok(matchesQuery(d, 'today launch'));
assert.ok(matchesQuery(d, ' TWTTR '));
assert.ok(matchesQuery(d, ''));
assert.ok(!matchesQuery(d, 'launch tomorrow'));
});
test('relativeTime', () => {
const now = Date.parse('2026-09-27T12:00:00Z');
assert.equal(relativeTime('2026-09-27T11:59:30Z', now), 'now');
assert.equal(relativeTime('2026-09-27T11:56:00Z', now), '4m');
assert.equal(relativeTime('2026-09-27T09:00:00Z', now), '3h');
assert.equal(relativeTime('2026-09-26T09:00:00Z', now), 'Yesterday');
assert.equal(relativeTime('2026-09-22T12:00:00Z', now), '5d');
assert.equal(relativeTime('2026-09-03T12:00:00Z', now), 'Sep 3');
assert.equal(relativeTime('2025-09-03T12:00:00Z', now), 'Sep 3, 2025');
assert.equal(relativeTime('2026-09-28T12:00:00Z', now), 'now');
assert.equal(relativeTime('garbage', now), '');
});
test('rowText: title + text, short first line as heading, long single lines as snippet only', () => {
assert.deepEqual(rowText(draft({ title: 'Launch', text: 'a\n\nb' })), { title: 'Launch', snippet: 'a b', untitled: false });
assert.deepEqual(rowText(draft({ text: 'first line\nsecond\nthird' })), { title: 'first line', snippet: 'second third', untitled: true });
assert.deepEqual(rowText(draft({ text: 'only one line' })), { title: 'only one line', snippet: '', untitled: true });
const text = 'Congrats on 2.9! The child webview APIs are what made our tab strip possible, thanks';
assert.deepEqual(rowText(draft({ text })), { title: '', snippet: text, untitled: true });
});
test('contextLabel and isPostUrl', () => {
assert.equal(contextLabel(POST), "On @jack's post");
assert.equal(contextLabel('https://x.com/home'), 'Home');
assert.equal(contextLabel('https://x.com/explore/'), 'x.com/explore');
assert.equal(contextLabel(''), '');
assert.ok(isPostUrl(`${POST}?s=1`));
assert.ok(!isPostUrl('https://x.com/i/status/1'));
assert.ok(!isPostUrl('not a url'));
});
test('highlight marks query words and escapes regex characters', () => {
assert.deepEqual(highlight('Ship it (today)', 'ship (today'), [
{ text: 'Ship', hit: true }, { text: ' it ', hit: false }, { text: '(today', hit: true }, { text: ')', hit: false },
]);
assert.deepEqual(highlight('abc', ''), [{ text: 'abc', hit: false }]);
});
test('step wraps and survives empty lists', () => {
assert.equal(step(0, -1, 3), 2);
assert.equal(step(2, 1, 3), 0);
assert.equal(step(4, 1, 0), 0);
});
test('charCount counts code points', () => {
assert.equal(charCount('héllo 👋'), 7);
});
test('anchorRequest reports the button as left/bottom plus size, rounded', () => {
assert.deepEqual(anchorRequest({ left: 412.4, top: 300.6, width: 34, height: 34 }, 'reply'), { kind: 'reply', x: 412, y: 335, width: 34, height: 34 });
});
test('isPostPath only matches post pages', () => {
assert.ok(isPostPath('/jack/status/20'));
assert.ok(isPostPath('/jack/status/20/photo/1'));
assert.ok(!isPostPath('/home'));
assert.ok(!isPostPath('/i/status/20'));
assert.ok(!isPostPath('/jack/status/abc'));
});
test('withAlpha keeps X accent colors and falls back to blue', () => {
assert.equal(withAlpha('rgb(249, 24, 128)', 0.1), 'rgba(249, 24, 128, 0.1)');
assert.equal(withAlpha('rgba(0, 186, 124, 1)', 0.35), 'rgba(0, 186, 124, 0.35)');
assert.equal(withAlpha('currentColor', 0.1), 'rgba(29, 155, 240, 0.1)');
});