Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add some tests for the rich text editor #452

Merged
merged 1 commit into from
Sep 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/components/views/rooms/MessageComposerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';

import {stateToMarkdown} from 'draft-js-export-markdown';
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
import classNames from 'classnames';
import escape from 'lodash/escape';

Expand All @@ -51,6 +51,16 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;

const KEY_M = 77;

const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
function stateToMarkdown(state) {
return __stateToMarkdown(state)
.replace(
ZWS, // draft-js-export-markdown adds these
''); // this is *not* a zero width space, trust me :)
}


// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
function mdownToHtml(mdown: string): string {
let html = marked(mdown) || "";
Expand Down Expand Up @@ -480,7 +490,7 @@ export default class MessageComposerInput extends React.Component {
});
}
if (cmd.promise) {
cmd.promise.done(function() {
cmd.promise.then(function() {
console.log("Command success.");
}, function(err) {
console.error("Command failure: %s", err);
Expand Down Expand Up @@ -520,7 +530,7 @@ export default class MessageComposerInput extends React.Component {
this.sentHistory.push(contentHTML);
let sendMessagePromise = sendFn.call(this.client, this.props.room.roomId, contentText, contentHTML);

sendMessagePromise.done(() => {
sendMessagePromise.then(() => {
dis.dispatch({
action: 'message_sent'
});
Expand Down
144 changes: 144 additions & 0 deletions test/components/views/rooms/MessageComposerInput-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';
import ReactTestUtils from 'react-addons-test-utils';
import ReactDOM from 'react-dom';
import expect, {createSpy} from 'expect';
import sinon from 'sinon';
import Q from 'q';
import * as testUtils from '../../../test-utils';
import sdk from 'matrix-react-sdk';
import UserSettingsStore from '../../../../src/UserSettingsStore';
const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
import MatrixClientPeg from 'MatrixClientPeg';

function addTextToDraft(text) {
const components = document.getElementsByClassName('public-DraftEditor-content');
if (components && components.length) {
const textarea = components[0];
const textEvent = document.createEvent('TextEvent');
textEvent.initTextEvent('textInput', true, true, null, text);
textarea.dispatchEvent(textEvent);
}
}

describe('MessageComposerInput', () => {
let parentDiv = null,
sandbox = null,
client = null,
mci = null,
room = testUtils.mkStubRoom('!DdJkzRliezrwpNebLk:matrix.org');

// TODO Remove when RTE is out of labs.

beforeEach(() => {
sandbox = testUtils.stubClient(sandbox);
client = MatrixClientPeg.get();
UserSettingsStore.isFeatureEnabled = sinon.stub()
.withArgs('rich_text_editor').returns(true);

parentDiv = document.createElement('div');
document.body.appendChild(parentDiv);
mci = ReactDOM.render(
<MessageComposerInput
room={room}
client={client}
/>,
parentDiv);
});

afterEach(() => {
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
parentDiv = null;
}
sandbox.restore();
});

it('should change mode if indicator is clicked', () => {
mci.enableRichtext(true);

setTimeout(() => {
const indicator = ReactTestUtils.findRenderedDOMComponentWithClass(
mci,
'mx_MessageComposer_input_markdownIndicator');
ReactTestUtils.Simulate.click(indicator);

expect(mci.state.isRichtextEnabled).toEqual(false, 'should have changed mode');
});
});

it('should not send messages when composer is empty', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(true);
mci.handleReturn(sinon.stub());

expect(spy.calledOnce).toEqual(false, 'should not send message');
});

it('should not change content unnecessarily on RTE -> Markdown conversion', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(true);
addTextToDraft('a');
mci.handleKeyCommand('toggle-mode');
mci.handleReturn(sinon.stub());

expect(spy.calledOnce).toEqual(true);
expect(spy.args[0][1]).toEqual('a');
});

it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(false);
addTextToDraft('a');
mci.handleKeyCommand('toggle-mode');
mci.handleReturn(sinon.stub());
expect(spy.calledOnce).toEqual(true);
expect(spy.args[0][1]).toEqual('a');
});

it('should send emoji messages in rich text', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(true);
addTextToDraft('☹');
mci.handleReturn(sinon.stub());

expect(spy.calledOnce).toEqual(true, 'should send message');
});

it('should send emoji messages in Markdown', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(false);
addTextToDraft('☹');
mci.handleReturn(sinon.stub());

expect(spy.calledOnce).toEqual(true, 'should send message');
});

it('should convert basic Markdown to rich text correctly', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(false);
addTextToDraft('*abc*');
mci.handleKeyCommand('toggle-mode');
mci.handleReturn(sinon.stub());
expect(spy.args[0][2]).toContain('<em>abc');
});

it('should convert basic rich text to Markdown correctly', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(true);
mci.handleKeyCommand('italic');
addTextToDraft('abc');
mci.handleKeyCommand('toggle-mode');
mci.handleReturn(sinon.stub());
expect(['_abc_', '*abc*']).toContain(spy.args[0][1]);
});

it('should insert formatting characters in Markdown mode', () => {
const spy = sinon.spy(client, 'sendHtmlMessage');
mci.enableRichtext(false);
mci.handleKeyCommand('italic');
mci.handleReturn(sinon.stub());
expect(['__', '**']).toContain(spy.args[0][1]);
});

});
33 changes: 22 additions & 11 deletions test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var MatrixEvent = jssdk.MatrixEvent;
* name to stdout.
* @param {Mocha.Context} context The test context
*/
module.exports.beforeEach = function(context) {
export function beforeEach(context) {
var desc = context.currentTest.fullTitle();
console.log();
console.log(desc);
Expand All @@ -26,7 +26,7 @@ module.exports.beforeEach = function(context) {
*
* @returns {sinon.Sandbox}; remember to call sandbox.restore afterwards.
*/
module.exports.stubClient = function() {
export function stubClient() {
var sandbox = sinon.sandbox.create();

var client = {
Expand All @@ -44,6 +44,16 @@ module.exports.stubClient = function() {
sendReadReceipt: sinon.stub().returns(q()),
getRoomIdForAlias: sinon.stub().returns(q()),
getProfileInfo: sinon.stub().returns(q({})),
getAccountData: (type) => {
return mkEvent({
type,
event: true,
content: {},
});
},
setAccountData: sinon.stub(),
sendTyping: sinon.stub().returns(q({})),
sendHtmlMessage: () => q({}),
};

// stub out the methods in MatrixClientPeg
Expand Down Expand Up @@ -73,7 +83,7 @@ module.exports.stubClient = function() {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
module.exports.mkEvent = function(opts) {
export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
Expand Down Expand Up @@ -101,7 +111,7 @@ module.exports.mkEvent = function(opts) {
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkPresence = function(opts) {
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
Expand Down Expand Up @@ -132,7 +142,7 @@ module.exports.mkPresence = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMembership = function(opts) {
export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
Expand All @@ -145,7 +155,7 @@ module.exports.mkMembership = function(opts) {
};
if (opts.name) { opts.content.displayname = opts.name; }
if (opts.url) { opts.content.avatar_url = opts.url; }
return module.exports.mkEvent(opts);
return mkEvent(opts);
};

/**
Expand All @@ -157,7 +167,7 @@ module.exports.mkMembership = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMessage = function(opts) {
export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
Expand All @@ -169,11 +179,12 @@ module.exports.mkMessage = function(opts) {
msgtype: "m.text",
body: opts.msg
};
return module.exports.mkEvent(opts);
};
return mkEvent(opts);
}

module.exports.mkStubRoom = function() {
export function mkStubRoom(roomId = null) {
return {
roomId,
getReceiptsForEvent: sinon.stub().returns([]),
getMember: sinon.stub().returns({}),
getJoinedMembers: sinon.stub().returns([]),
Expand All @@ -182,4 +193,4 @@ module.exports.mkStubRoom = function() {
members: [],
},
};
};
}