Check leaks in Mocha

This commit is contained in:
Evan Hahn 2021-01-11 14:17:09 -06:00 committed by GitHub
parent 7543d8b60d
commit dc918aea1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 3 additions and 38 deletions

View file

@ -1,442 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import sinon from 'sinon';
import { EmojiCompletion } from '../../../quill/emoji/completion';
import { EmojiData } from '../../../components/emoji/lib';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalAsAny = global as any;
describe('emojiCompletion', () => {
let emojiCompletion: EmojiCompletion;
const mockOnPickEmoji = sinon.spy();
const mockSetEmojiPickerElement = sinon.spy();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mockQuill: any;
beforeEach(function beforeEach() {
this.oldDocument = globalAsAny.document;
globalAsAny.document = {
body: {
appendChild: () => null,
},
createElement: () => null,
};
mockQuill = {
getLeaf: sinon.stub(),
getSelection: sinon.stub(),
keyboard: {
addBinding: sinon.stub(),
},
on: sinon.stub(),
setSelection: sinon.stub(),
updateContents: sinon.stub(),
};
const options = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onPickEmoji: mockOnPickEmoji as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setEmojiPickerElement: mockSetEmojiPickerElement as any,
skinTone: 0,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emojiCompletion = new EmojiCompletion(mockQuill as any, options);
// Stub rendering to avoid missing DOM until we bring in Enzyme
emojiCompletion.render = sinon.stub();
});
afterEach(function afterEach() {
mockOnPickEmoji.resetHistory();
mockSetEmojiPickerElement.resetHistory();
(emojiCompletion.render as sinon.SinonStub).resetHistory();
if (this.oldDocument === undefined) {
delete globalAsAny.document;
} else {
globalAsAny.document = this.oldDocument;
}
});
describe('getCurrentLeafTextPartitions', () => {
it('returns left and right text', () => {
mockQuill.getSelection.returns({ index: 0, length: 0 });
const blot = {
text: ':smile:',
};
mockQuill.getLeaf.returns([blot, 2]);
const [
leftLeafText,
rightLeafText,
] = emojiCompletion.getCurrentLeafTextPartitions();
assert.equal(leftLeafText, ':s');
assert.equal(rightLeafText, 'mile:');
});
});
describe('onTextChange', () => {
let insertEmojiStub: sinon.SinonStub<
[EmojiData, number, number, (boolean | undefined)?],
void
>;
beforeEach(function beforeEach() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emojiCompletion.results = [{ short_name: 'joy' } as any];
emojiCompletion.index = 5;
insertEmojiStub = sinon
.stub(emojiCompletion, 'insertEmoji')
.callThrough();
});
afterEach(function afterEach() {
insertEmojiStub.restore();
});
describe('given an emoji is not starting (no colon)', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 3,
length: 0,
});
const blot = {
text: 'smi',
};
mockQuill.getLeaf.returns([blot, 3]);
emojiCompletion.onTextChange();
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
describe('given a colon in a string (but not an emoji)', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 5,
length: 0,
});
const blot = {
text: '10:30',
};
mockQuill.getLeaf.returns([blot, 5]);
emojiCompletion.onTextChange();
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
describe('given an emoji is starting but does not have 2 characters', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 2,
length: 0,
});
const blot = {
text: ':s',
};
mockQuill.getLeaf.returns([blot, 2]);
emojiCompletion.onTextChange();
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
describe('given an emoji is starting but does not match a short name', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 4,
length: 0,
});
const blot = {
text: ':smy',
};
mockQuill.getLeaf.returns([blot, 4]);
emojiCompletion.onTextChange();
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
describe('given an emoji is starting and matches short names', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 4,
length: 0,
});
const blot = {
text: ':smi',
};
mockQuill.getLeaf.returns([blot, 4]);
emojiCompletion.onTextChange();
});
it('stores the results and renders', () => {
assert.equal(emojiCompletion.results.length, 10);
assert.equal((emojiCompletion.render as sinon.SinonStub).called, true);
});
});
describe('given an emoji was just completed', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 7,
length: 0,
});
});
describe('and given it matches a short name', () => {
const text = ':smile:';
beforeEach(function beforeEach() {
const blot = {
text,
};
mockQuill.getLeaf.returns([blot, 7]);
emojiCompletion.onTextChange();
});
it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0];
assert.equal(emoji.short_name, 'smile');
assert.equal(index, 0);
assert.equal(range, 7);
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
describe('and given it matches a short name inside a larger string', () => {
const text = 'have a :smile: nice day';
beforeEach(function beforeEach() {
const blot = {
text,
};
mockQuill.getSelection.returns({
index: 13,
length: 0,
});
mockQuill.getLeaf.returns([blot, 13]);
emojiCompletion.onTextChange();
});
it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0];
assert.equal(emoji.short_name, 'smile');
assert.equal(index, 7);
assert.equal(range, 7);
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
it('sets the quill selection to the right cursor position', () => {
const [index, range] = mockQuill.setSelection.args[0];
assert.equal(index, 8);
assert.equal(range, 0);
});
});
describe('and given it does not match a short name', () => {
const text = ':smyle:';
beforeEach(function beforeEach() {
const blot = {
text,
};
mockQuill.getLeaf.returns([blot, 7]);
emojiCompletion.onTextChange();
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
});
describe('given an emoji was just completed from inside the colons', () => {
const validEmoji = ':smile:';
const invalidEmoji = ':smyle:';
const middleCursorIndex = validEmoji.length - 3;
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: middleCursorIndex,
length: 0,
});
});
describe('and given it matches a short name', () => {
beforeEach(function beforeEach() {
const blot = {
text: validEmoji,
};
mockQuill.getLeaf.returns([blot, middleCursorIndex]);
emojiCompletion.onTextChange();
});
it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0];
assert.equal(emoji.short_name, 'smile');
assert.equal(index, 0);
assert.equal(range, validEmoji.length);
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
describe('and given it does not match a short name', () => {
beforeEach(function beforeEach() {
const blot = {
text: invalidEmoji,
};
mockQuill.getLeaf.returns([blot, middleCursorIndex]);
emojiCompletion.onTextChange();
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
});
describe('given a completeable emoji and colon was just pressed', () => {
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index: 6,
length: 0,
});
});
describe('and given it matches a short name', () => {
const text = ':smile';
beforeEach(function beforeEach() {
const blot = {
text,
};
mockQuill.getLeaf.returns([blot, 6]);
emojiCompletion.onTextChange(true);
});
it('inserts the emoji at the current cursor position', () => {
const [emoji, index, range] = insertEmojiStub.args[0];
assert.equal(emoji.short_name, 'smile');
assert.equal(index, 0);
assert.equal(range, 6);
});
it('does not show results', () => {
assert.equal(emojiCompletion.results.length, 0);
});
});
});
});
describe('completeEmoji', () => {
let insertEmojiStub: sinon.SinonStub<
[EmojiData, number, number, (boolean | undefined)?],
void
>;
beforeEach(function beforeEach() {
emojiCompletion.results = [
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ short_name: 'smile' } as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ short_name: 'smile_cat' } as any,
];
emojiCompletion.index = 1;
insertEmojiStub = sinon.stub(emojiCompletion, 'insertEmoji');
});
describe('given a valid token', () => {
const text = ':smi';
const index = text.length;
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index,
length: 0,
});
const blot = {
text,
};
mockQuill.getLeaf.returns([blot, index]);
emojiCompletion.completeEmoji();
});
it('inserts the currently selected emoji at the current cursor position', () => {
const [emoji, insertIndex, range] = insertEmojiStub.args[0];
assert.equal(emoji.short_name, 'smile_cat');
assert.equal(insertIndex, 0);
assert.equal(range, text.length);
});
});
describe('given a valid token is not present', () => {
const text = 'smi';
const index = text.length;
beforeEach(function beforeEach() {
mockQuill.getSelection.returns({
index,
length: 0,
});
const blot = {
text,
};
mockQuill.getLeaf.returns([blot, index]);
emojiCompletion.completeEmoji();
});
it('does not insert anything', () => {
assert.equal(insertEmojiStub.called, false);
});
});
});
});

View file

@ -1,269 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import Delta from 'quill-delta';
import sinon, { SinonStub } from 'sinon';
import Quill, { KeyboardStatic } from 'quill';
import { MutableRefObject } from 'react';
import {
MentionCompletion,
MentionCompletionOptions,
} from '../../../quill/mentions/completion';
import { ConversationType } from '../../../state/ducks/conversations';
import { MemberRepository } from '../../../quill/memberRepository';
const me: ConversationType = {
id: '666777',
uuid: 'pqrstuv',
title: 'Fred Savage',
firstName: 'Fred',
profileName: 'Fred S.',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
areWeAdmin: false,
};
const members: Array<ConversationType> = [
{
id: '555444',
uuid: 'abcdefg',
title: 'Mahershala Ali',
firstName: 'Mahershala',
profileName: 'Mahershala A.',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
areWeAdmin: false,
},
{
id: '333222',
uuid: 'hijklmno',
title: 'Shia LaBeouf',
firstName: 'Shia',
profileName: 'Shia L.',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
areWeAdmin: false,
},
me,
];
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
document: {
body: {
appendChild: unknown;
};
createElement: unknown;
};
}
}
}
describe('MentionCompletion', () => {
const mockSetMentionPickerElement = sinon.spy();
let mockQuill: Omit<
Partial<{ [K in keyof Quill]: SinonStub }>,
'keyboard'
> & {
keyboard: Partial<{ [K in keyof KeyboardStatic]: SinonStub }>;
};
let mentionCompletion: MentionCompletion;
beforeEach(function beforeEach() {
global.document = {
body: {
appendChild: sinon.spy(),
},
createElement: sinon.spy(),
};
const memberRepositoryRef: MutableRefObject<MemberRepository> = {
current: new MemberRepository(members),
};
const options: MentionCompletionOptions = {
i18n: sinon.stub(),
me,
memberRepositoryRef,
setMentionPickerElement: mockSetMentionPickerElement,
};
mockQuill = {
getContents: sinon.stub(),
getLeaf: sinon.stub(),
getSelection: sinon.stub(),
keyboard: { addBinding: sinon.stub() },
on: sinon.stub(),
setSelection: sinon.stub(),
updateContents: sinon.stub(),
};
mentionCompletion = new MentionCompletion(
(mockQuill as unknown) as Quill,
options
);
sinon.stub(mentionCompletion, 'render');
});
describe('onTextChange', () => {
let possiblyShowMemberResultsStub: sinon.SinonStub<[], ConversationType[]>;
beforeEach(() => {
possiblyShowMemberResultsStub = sinon.stub(
mentionCompletion,
'possiblyShowMemberResults'
);
});
describe('given a change that should show members', () => {
const newContents = new Delta().insert('@a');
beforeEach(() => {
mockQuill.getContents?.returns(newContents);
possiblyShowMemberResultsStub.returns(members);
});
it('shows member results', () => {
mentionCompletion.onTextChange();
assert.equal(mentionCompletion.results, members);
assert.equal(mentionCompletion.index, 0);
});
});
describe('given a change that should clear results', () => {
const newContents = new Delta().insert('foo ');
let clearResultsStub: SinonStub<[], void>;
beforeEach(() => {
mentionCompletion.results = members;
mockQuill.getContents?.returns(newContents);
possiblyShowMemberResultsStub.returns([]);
clearResultsStub = sinon.stub(mentionCompletion, 'clearResults');
});
it('clears member results', () => {
mentionCompletion.onTextChange();
assert.equal(clearResultsStub.called, true);
});
});
});
describe('completeMention', () => {
describe('given a completable mention', () => {
let insertMentionStub: SinonStub<
[ConversationType, number, number, (boolean | undefined)?],
void
>;
beforeEach(() => {
mentionCompletion.results = members;
mockQuill.getSelection?.returns({ index: 5 });
mockQuill.getLeaf?.returns([{ text: '@shia' }, 5]);
insertMentionStub = sinon.stub(mentionCompletion, 'insertMention');
});
it('inserts the currently selected mention at the current cursor position', () => {
mentionCompletion.completeMention(1);
const [
member,
distanceFromCursor,
adjustCursorAfterBy,
withTrailingSpace,
] = insertMentionStub.getCall(0).args;
assert.equal(member, members[1]);
assert.equal(distanceFromCursor, 0);
assert.equal(adjustCursorAfterBy, 5);
assert.equal(withTrailingSpace, true);
});
it('can infer the member to complete with', () => {
mentionCompletion.index = 1;
mentionCompletion.completeMention();
const [
member,
distanceFromCursor,
adjustCursorAfterBy,
withTrailingSpace,
] = insertMentionStub.getCall(0).args;
assert.equal(member, members[1]);
assert.equal(distanceFromCursor, 0);
assert.equal(adjustCursorAfterBy, 5);
assert.equal(withTrailingSpace, true);
});
describe('from the middle of a string', () => {
beforeEach(() => {
mockQuill.getSelection?.returns({ index: 9 });
mockQuill.getLeaf?.returns([{ text: 'foo @shia bar' }, 9]);
});
it('inserts correctly', () => {
mentionCompletion.completeMention(1);
const [
member,
distanceFromCursor,
adjustCursorAfterBy,
withTrailingSpace,
] = insertMentionStub.getCall(0).args;
assert.equal(member, members[1]);
assert.equal(distanceFromCursor, 4);
assert.equal(adjustCursorAfterBy, 5);
assert.equal(withTrailingSpace, true);
});
});
describe('given a completable mention starting with a capital letter', () => {
const text = '@Sh';
const index = text.length;
beforeEach(function beforeEach() {
mockQuill.getSelection?.returns({ index });
const blot = {
text,
};
mockQuill.getLeaf?.returns([blot, index]);
mentionCompletion.completeMention(1);
});
it('inserts the currently selected mention at the current cursor position', () => {
const [
member,
distanceFromCursor,
adjustCursorAfterBy,
withTrailingSpace,
] = insertMentionStub.getCall(0).args;
assert.equal(member, members[1]);
assert.equal(distanceFromCursor, 0);
assert.equal(adjustCursorAfterBy, 3);
assert.equal(withTrailingSpace, true);
});
});
});
});
});