594 lines
14 KiB
TypeScript
594 lines
14 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { assert } from 'chai';
|
|
|
|
import {
|
|
getDeltaToRemoveStaleMentions,
|
|
getTextAndRangesFromOps,
|
|
getDeltaToRestartMention,
|
|
} from '../../quill/util';
|
|
import { BodyRange } from '../../types/BodyRange';
|
|
import { generateAci } from '../../types/ServiceId';
|
|
|
|
const SERVICE_ID_1 = generateAci();
|
|
const SERVICE_ID_2 = generateAci();
|
|
const SERVICE_ID_3 = generateAci();
|
|
const SERVICE_ID_4 = generateAci();
|
|
|
|
describe('getDeltaToRemoveStaleMentions', () => {
|
|
const memberUuids = [SERVICE_ID_1, SERVICE_ID_2];
|
|
|
|
describe('given text', () => {
|
|
it('retains the text', () => {
|
|
const originalOps = [
|
|
{
|
|
insert: 'whoa, nobody here',
|
|
},
|
|
];
|
|
|
|
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
|
|
|
assert.deepEqual(ops, [{ retain: 17 }]);
|
|
});
|
|
});
|
|
|
|
describe('given stale and valid mentions', () => {
|
|
it('retains the valid and replaces the stale', () => {
|
|
const originalOps = [
|
|
{
|
|
insert: {
|
|
mention: { aci: SERVICE_ID_3, title: 'Klaus' },
|
|
},
|
|
},
|
|
{ insert: { mention: { aci: SERVICE_ID_1, title: 'Werner' } } },
|
|
];
|
|
|
|
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
|
|
|
assert.deepEqual(ops, [
|
|
{ delete: 1 },
|
|
{ insert: '@Klaus' },
|
|
{ retain: 1 },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('given emoji embeds', () => {
|
|
it('retains the embeds', () => {
|
|
const originalOps = [
|
|
{
|
|
insert: {
|
|
emoji: '😂',
|
|
},
|
|
},
|
|
{
|
|
insert: {
|
|
emoji: '🍋',
|
|
},
|
|
},
|
|
];
|
|
|
|
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
|
|
|
assert.deepEqual(ops, [{ retain: 1 }, { retain: 1 }]);
|
|
});
|
|
});
|
|
|
|
describe('given other ops', () => {
|
|
it('passes them through', () => {
|
|
const originalOps = [
|
|
{
|
|
delete: 5,
|
|
},
|
|
];
|
|
|
|
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
|
|
|
assert.deepEqual(ops, originalOps);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getTextAndRangesFromOps', () => {
|
|
describe('given only text', () => {
|
|
it('returns only text trimmed', () => {
|
|
const ops = [{ insert: ' The ' }, { insert: ' text \n' }];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'The text');
|
|
assert.equal(bodyRanges.length, 0);
|
|
});
|
|
|
|
it('returns trimmed of trailing newlines', () => {
|
|
const ops = [{ insert: ' The\ntext\n\n\n' }];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'The\ntext');
|
|
assert.equal(bodyRanges.length, 0);
|
|
});
|
|
});
|
|
|
|
describe('given formatting', () => {
|
|
it('handles trimming with simple ops', () => {
|
|
const ops = [
|
|
{
|
|
insert: 'test test ',
|
|
attributes: { bold: true },
|
|
},
|
|
// This is something Quill does for some reason
|
|
{
|
|
insert: '\n',
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'test test');
|
|
assert.equal(bodyRanges.length, 1);
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 9,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles trimming with no-formatting single- and multi-newline ops', () => {
|
|
const ops = [
|
|
{
|
|
insert: 'line1',
|
|
attributes: { bold: true },
|
|
},
|
|
// quill doesn't put formatting on all-newline ops
|
|
{
|
|
insert: '\n',
|
|
},
|
|
{
|
|
insert: 'line2',
|
|
attributes: { bold: true },
|
|
},
|
|
{
|
|
insert: '\n\n',
|
|
},
|
|
{
|
|
insert: 'line4',
|
|
attributes: { bold: true },
|
|
},
|
|
{
|
|
insert: '\n\n',
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'line1\nline2\n\nline4');
|
|
assert.equal(bodyRanges.length, 1);
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 18,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles trimming at the end of the message', () => {
|
|
const ops = [
|
|
{
|
|
insert: 'Text with trailing ',
|
|
attributes: { bold: true },
|
|
},
|
|
{
|
|
insert: 'whitespace ',
|
|
attributes: { bold: true, italic: true },
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'Text with trailing whitespace');
|
|
assert.equal(bodyRanges.length, 2);
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 29,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
{
|
|
start: 19,
|
|
length: 10,
|
|
style: BodyRange.Style.ITALIC,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles trimming at beginning of the message', () => {
|
|
const ops = [
|
|
{
|
|
insert: ' Text with leading ',
|
|
attributes: { bold: true },
|
|
},
|
|
{
|
|
insert: 'whitespace!!',
|
|
attributes: { bold: true, italic: true },
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'Text with leading whitespace!!');
|
|
assert.equal(bodyRanges.length, 2);
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 30,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
{
|
|
start: 18,
|
|
length: 12,
|
|
style: BodyRange.Style.ITALIC,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles formatting of whitespace at beginning/ending of message', () => {
|
|
const ops = [
|
|
{
|
|
insert: ' ',
|
|
attributes: { bold: true, italic: true, strike: true },
|
|
},
|
|
{
|
|
insert: ' ',
|
|
attributes: { italic: true, strike: true, spoiler: true },
|
|
},
|
|
{
|
|
insert: 'so much whitespace',
|
|
attributes: { strike: true, spoiler: true, monospace: true },
|
|
},
|
|
{
|
|
insert: ' ',
|
|
attributes: { spoiler: true, monospace: true, italic: true },
|
|
},
|
|
{
|
|
insert: ' ',
|
|
attributes: { monospace: true, italic: true, bold: true },
|
|
},
|
|
{ insert: '\n' },
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'so much whitespace');
|
|
assert.equal(bodyRanges.length, 3);
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 18,
|
|
style: BodyRange.Style.STRIKETHROUGH,
|
|
},
|
|
{
|
|
start: 0,
|
|
length: 18,
|
|
style: BodyRange.Style.SPOILER,
|
|
},
|
|
{
|
|
start: 0,
|
|
length: 18,
|
|
style: BodyRange.Style.MONOSPACE,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('given text, emoji, and mentions', () => {
|
|
it('returns the trimmed text with placeholders and mentions', () => {
|
|
const ops = [
|
|
{
|
|
insert: {
|
|
emoji: '😂',
|
|
},
|
|
},
|
|
{
|
|
insert: ' wow, funny, ',
|
|
},
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_1,
|
|
title: '@fred',
|
|
},
|
|
},
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, '😂 wow, funny, \uFFFC');
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
length: 1,
|
|
mentionAci: SERVICE_ID_1,
|
|
replacementText: '@fred',
|
|
start: 15,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('given only mentions', () => {
|
|
it('returns the trimmed text with placeholders and mentions', () => {
|
|
const ops = [
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_1,
|
|
title: '@fred',
|
|
},
|
|
},
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, '\uFFFC');
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
length: 1,
|
|
mentionAci: SERVICE_ID_1,
|
|
replacementText: '@fred',
|
|
start: 0,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('does not trim newlines padding mentions', () => {
|
|
const ops = [
|
|
{ insert: 'test \n' },
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_1,
|
|
title: '@fred',
|
|
},
|
|
},
|
|
},
|
|
{ insert: '\n test' },
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'test \n\uFFFC\n test');
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
length: 1,
|
|
mentionAci: SERVICE_ID_1,
|
|
replacementText: '@fred',
|
|
start: 6,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('given formatting on text, with emoji and mentions', () => {
|
|
it('handles overlapping and contiguous format sections properly', () => {
|
|
const ops = [
|
|
{
|
|
insert: 'Hey, ',
|
|
attributes: {
|
|
spoiler: true,
|
|
},
|
|
},
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_4,
|
|
title: '@alice',
|
|
},
|
|
},
|
|
attributes: {
|
|
spoiler: true,
|
|
},
|
|
},
|
|
{
|
|
insert: ': this is ',
|
|
attributes: {
|
|
spoiler: true,
|
|
},
|
|
},
|
|
{
|
|
insert: 'bold',
|
|
attributes: {
|
|
bold: true,
|
|
spoiler: true,
|
|
},
|
|
},
|
|
{
|
|
insert: ' and',
|
|
attributes: {
|
|
bold: true,
|
|
italic: true,
|
|
spoiler: true,
|
|
},
|
|
},
|
|
{
|
|
insert: ' italic',
|
|
attributes: {
|
|
italic: true,
|
|
spoiler: true,
|
|
},
|
|
},
|
|
{
|
|
insert: ' and strikethrough',
|
|
attributes: {
|
|
strike: true,
|
|
},
|
|
},
|
|
{ insert: ' ' },
|
|
{
|
|
insert: 'and monospace',
|
|
attributes: {
|
|
monospace: true,
|
|
},
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(
|
|
text,
|
|
'Hey, \uFFFC: this is bold and italic and strikethrough and monospace'
|
|
);
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 5,
|
|
length: 1,
|
|
mentionAci: SERVICE_ID_4,
|
|
replacementText: '@alice',
|
|
},
|
|
{
|
|
start: 16,
|
|
length: 8,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
{
|
|
start: 20,
|
|
length: 11,
|
|
style: BodyRange.Style.ITALIC,
|
|
},
|
|
{
|
|
start: 0,
|
|
length: 31,
|
|
style: BodyRange.Style.SPOILER,
|
|
},
|
|
{
|
|
start: 31,
|
|
length: 18,
|
|
style: BodyRange.Style.STRIKETHROUGH,
|
|
},
|
|
{
|
|
start: 50,
|
|
length: 13,
|
|
style: BodyRange.Style.MONOSPACE,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles lots of the same format', () => {
|
|
const ops = [
|
|
{
|
|
insert: 'Every',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
{
|
|
insert: ' other ',
|
|
},
|
|
{
|
|
insert: 'word',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
{
|
|
insert: ' is ',
|
|
},
|
|
{
|
|
insert: 'bold!',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, 'Every other word is bold!');
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 5,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
{
|
|
start: 12,
|
|
length: 4,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
{
|
|
start: 20,
|
|
length: 5,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles formatting on mentions', () => {
|
|
const ops = [
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_4,
|
|
title: '@alice',
|
|
},
|
|
},
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
];
|
|
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
|
assert.equal(text, '\uFFFC');
|
|
assert.deepEqual(bodyRanges, [
|
|
{
|
|
start: 0,
|
|
length: 1,
|
|
mentionAci: SERVICE_ID_4,
|
|
replacementText: '@alice',
|
|
},
|
|
{
|
|
start: 0,
|
|
length: 1,
|
|
style: BodyRange.Style.BOLD,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getDeltaToRestartMention', () => {
|
|
describe('given text and emoji', () => {
|
|
it('returns the correct retains, a delete, and an @', () => {
|
|
const originalOps = [
|
|
{
|
|
insert: {
|
|
emoji: '😂',
|
|
},
|
|
},
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_2,
|
|
title: '@sam',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
insert: ' wow, funny, ',
|
|
},
|
|
{
|
|
insert: {
|
|
mention: {
|
|
aci: SERVICE_ID_1,
|
|
title: '@fred',
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const { ops } = getDeltaToRestartMention(originalOps);
|
|
|
|
assert.deepEqual(ops, [
|
|
{
|
|
retain: 1,
|
|
},
|
|
{
|
|
retain: 1,
|
|
},
|
|
{
|
|
retain: 13,
|
|
},
|
|
{
|
|
retain: 1,
|
|
},
|
|
{
|
|
delete: 1,
|
|
},
|
|
{
|
|
insert: '@',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|