Upgrade to QuillV2
This commit is contained in:
parent
fb04b1ede3
commit
7575bda35b
32 changed files with 910 additions and 1227 deletions
|
@ -1,170 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import { MemberRepository, _toMembers } from '../../quill/memberRepository';
|
||||
import { getDefaultConversationWithServiceId } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const UNKNOWN_SERVICE_ID = generateAci();
|
||||
|
||||
const memberMahershala: ConversationType = getDefaultConversationWithServiceId({
|
||||
id: '555444',
|
||||
title: 'Pal',
|
||||
firstName: 'Mahershala',
|
||||
profileName: 'Mr Ali',
|
||||
name: 'Friend',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
const memberShia: ConversationType = getDefaultConversationWithServiceId({
|
||||
id: '333222',
|
||||
title: 'Buddy',
|
||||
firstName: 'Shia',
|
||||
profileName: 'Sr LaBeouf',
|
||||
name: 'Duder',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
const conversations: Array<ConversationType> = [memberMahershala, memberShia];
|
||||
|
||||
const singleMember: ConversationType = getDefaultConversationWithServiceId({
|
||||
id: '666777',
|
||||
title: 'The Guy',
|
||||
firstName: 'Jeff',
|
||||
profileName: 'Jr Klaus',
|
||||
name: 'Him',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
describe('MemberRepository', () => {
|
||||
describe('#updateMembers', () => {
|
||||
it('updates with given members', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
assert.deepEqual(
|
||||
memberRepository.getMembers(),
|
||||
_toMembers(conversations)
|
||||
);
|
||||
|
||||
const updatedConversations = [...conversations, singleMember];
|
||||
memberRepository.updateMembers(updatedConversations);
|
||||
assert.deepEqual(
|
||||
memberRepository.getMembers(),
|
||||
_toMembers(updatedConversations)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMemberById', () => {
|
||||
it('returns undefined when there is no search id', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
assert.isUndefined(memberRepository.getMemberById());
|
||||
});
|
||||
|
||||
it('returns a matched member', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
assert.isDefined(memberRepository.getMemberById('555444'));
|
||||
});
|
||||
|
||||
it('returns undefined when it does not have the member', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
assert.isUndefined(memberRepository.getMemberById(UNKNOWN_SERVICE_ID));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMemberByAci', () => {
|
||||
it('returns undefined when there is no search serviceId', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
assert.isUndefined(memberRepository.getMemberByAci());
|
||||
});
|
||||
|
||||
it('returns a matched member', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const aci = memberMahershala.serviceId;
|
||||
if (!isAciString(aci)) {
|
||||
throw new Error('Service id not ACI');
|
||||
}
|
||||
assert.isDefined(memberRepository.getMemberByAci(aci));
|
||||
});
|
||||
|
||||
it('returns undefined when it does not have the member', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
assert.isUndefined(memberRepository.getMemberByAci(UNKNOWN_SERVICE_ID));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#search', () => {
|
||||
describe('given a prefix-matching string on last name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const results = memberRepository.search('a');
|
||||
assert.deepEqual(results, _toMembers([memberMahershala]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on first name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const results = memberRepository.search('ma');
|
||||
assert.deepEqual(results, _toMembers([memberMahershala]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on profile name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const results = memberRepository.search('sr');
|
||||
assert.deepEqual(results, _toMembers([memberShia]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const results = memberRepository.search('dude');
|
||||
assert.deepEqual(results, _toMembers([memberShia]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on title', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const results = memberRepository.search('bud');
|
||||
assert.deepEqual(results, _toMembers([memberShia]));
|
||||
});
|
||||
|
||||
it('handles titles with Unicode bidi characters, which some contacts have', () => {
|
||||
const memberShiaBidi: ConversationType = {
|
||||
...memberShia,
|
||||
title: '\u2086Buddyo\u2069',
|
||||
};
|
||||
const memberRepository = new MemberRepository([
|
||||
memberMahershala,
|
||||
memberShiaBidi,
|
||||
]);
|
||||
const results = memberRepository.search('bud');
|
||||
assert.deepEqual(results, _toMembers([memberShiaBidi]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a match in the middle of a name', () => {
|
||||
it('returns zero matches', () => {
|
||||
const memberRepository = new MemberRepository(conversations);
|
||||
const results = memberRepository.search('e');
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,201 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import type { RefObject } from 'react';
|
||||
import Delta from 'quill-delta';
|
||||
|
||||
import type { AciString } from '../../../types/ServiceId';
|
||||
import { generateAci } from '../../../types/ServiceId';
|
||||
import { matchMention } from '../../../quill/mentions/matchers';
|
||||
import { MemberRepository } from '../../../quill/memberRepository';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { getDefaultConversationWithServiceId } from '../../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const ACI_1 = generateAci();
|
||||
|
||||
class FakeTokenList<T> extends Array<T> {
|
||||
constructor(elements: Array<T>) {
|
||||
super();
|
||||
elements.forEach(element => this.push(element));
|
||||
}
|
||||
|
||||
contains(searchElement: T) {
|
||||
return this.includes(searchElement);
|
||||
}
|
||||
}
|
||||
|
||||
const createMockElement = (
|
||||
className: string,
|
||||
dataset: Record<string, string>
|
||||
): HTMLElement =>
|
||||
({
|
||||
classList: new FakeTokenList([className]),
|
||||
dataset,
|
||||
}) as unknown as HTMLElement;
|
||||
|
||||
const createMockAtMentionElement = (
|
||||
dataset: Record<string, string>
|
||||
): HTMLElement => createMockElement('MessageBody__at-mention', dataset);
|
||||
|
||||
const createMockMentionBlotElement = (
|
||||
dataset: Record<string, string>
|
||||
): HTMLElement => createMockElement('mention-blot', dataset);
|
||||
|
||||
const memberMahershala: ConversationType = getDefaultConversationWithServiceId({
|
||||
id: '555444',
|
||||
title: 'Mahershala Ali',
|
||||
firstName: 'Mahershala',
|
||||
profileName: 'Mahershala A.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
const memberShia: ConversationType = getDefaultConversationWithServiceId({
|
||||
id: '333222',
|
||||
title: 'Shia LaBeouf',
|
||||
firstName: 'Shia',
|
||||
profileName: 'Shia L.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
const members: Array<ConversationType> = [memberMahershala, memberShia];
|
||||
|
||||
const memberRepositoryRef: RefObject<MemberRepository> = {
|
||||
current: new MemberRepository(members),
|
||||
};
|
||||
|
||||
const matcher = matchMention(memberRepositoryRef);
|
||||
|
||||
type Mention = {
|
||||
aci: AciString;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type MentionInsert = {
|
||||
mention: Mention;
|
||||
};
|
||||
|
||||
const isMention = (insert?: unknown): insert is MentionInsert => {
|
||||
if (insert) {
|
||||
if (Object.getOwnPropertyNames(insert).includes('mention')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const EMPTY_DELTA = new Delta();
|
||||
|
||||
describe('matchMention', () => {
|
||||
it('handles an AtMentionify from clipboard', () => {
|
||||
const existingAttributes = { italic: true };
|
||||
const result = matcher(
|
||||
createMockAtMentionElement({
|
||||
id: memberMahershala.id,
|
||||
title: memberMahershala.title,
|
||||
}),
|
||||
EMPTY_DELTA,
|
||||
existingAttributes
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert, attributes } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
const { title, aci } = insert.mention;
|
||||
|
||||
assert.equal(title, memberMahershala.title);
|
||||
assert.equal(aci, memberMahershala.serviceId);
|
||||
|
||||
assert.deepEqual(existingAttributes, attributes, 'attributes');
|
||||
} else {
|
||||
assert.fail('insert is invalid');
|
||||
}
|
||||
});
|
||||
|
||||
it('handles an MentionBlot from clipboard', () => {
|
||||
const result = matcher(
|
||||
createMockMentionBlotElement({
|
||||
aci: memberMahershala.serviceId || '',
|
||||
title: memberMahershala.title,
|
||||
}),
|
||||
EMPTY_DELTA,
|
||||
{}
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
const { title, aci } = insert.mention;
|
||||
|
||||
assert.equal(title, memberMahershala.title);
|
||||
assert.equal(aci, memberMahershala.serviceId);
|
||||
} else {
|
||||
assert.fail('insert is invalid');
|
||||
}
|
||||
});
|
||||
|
||||
it('converts a missing AtMentionify to string', () => {
|
||||
const result = matcher(
|
||||
createMockAtMentionElement({
|
||||
id: 'florp',
|
||||
title: 'Nonexistent',
|
||||
}),
|
||||
EMPTY_DELTA,
|
||||
{}
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
assert.fail('insert is invalid');
|
||||
} else {
|
||||
assert.equal(insert, '@Nonexistent');
|
||||
}
|
||||
});
|
||||
|
||||
it('converts a missing MentionBlot to string', () => {
|
||||
const result = matcher(
|
||||
createMockMentionBlotElement({
|
||||
aci: ACI_1,
|
||||
title: 'Nonexistent',
|
||||
}),
|
||||
EMPTY_DELTA,
|
||||
{}
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
assert.fail('insert is invalid');
|
||||
} else {
|
||||
assert.equal(insert, '@Nonexistent');
|
||||
}
|
||||
});
|
||||
|
||||
it('passes other clipboard elements through', () => {
|
||||
const result = matcher(createMockElement('ignore', {}), EMPTY_DELTA, {});
|
||||
assert.equal(result, EMPTY_DELTA);
|
||||
});
|
||||
});
|
|
@ -1,630 +0,0 @@
|
|||
// 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: { value: '😂' },
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: {
|
||||
emoji: { value: '🍋' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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('does not trim at beginning of the message if monospace', () => {
|
||||
const ops = [
|
||||
{
|
||||
insert: ' ',
|
||||
},
|
||||
{
|
||||
insert: ' Text with leading ',
|
||||
attributes: { monospace: true },
|
||||
},
|
||||
{
|
||||
insert: 'whitespace!!',
|
||||
attributes: { bold: true, italic: true },
|
||||
},
|
||||
];
|
||||
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
|
||||
assert.equal(text, ' Text with leading whitespace!!');
|
||||
assert.equal(bodyRanges.length, 3);
|
||||
assert.deepEqual(bodyRanges, [
|
||||
{
|
||||
start: 0,
|
||||
length: 20,
|
||||
style: BodyRange.Style.MONOSPACE,
|
||||
},
|
||||
{
|
||||
start: 20,
|
||||
length: 12,
|
||||
style: BodyRange.Style.BOLD,
|
||||
},
|
||||
{
|
||||
start: 20,
|
||||
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: { value: '😂' },
|
||||
},
|
||||
},
|
||||
{
|
||||
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: { value: '😂' },
|
||||
},
|
||||
},
|
||||
{
|
||||
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: '@',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue