diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 1f5b303fe0..c326a7c4f5 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -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'; @@ -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

function mdownToHtml(mdown: string): string { let html = marked(mdown) || ""; @@ -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); @@ -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' }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js new file mode 100644 index 0000000000..89f838ba87 --- /dev/null +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -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( + , + 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('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]); + }); + +}); diff --git a/test/test-utils.js b/test/test-utils.js index 799f04ce54..78349b7824 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -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); @@ -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 = { @@ -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 @@ -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)); } @@ -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"); } @@ -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; @@ -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); }; /** @@ -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(); @@ -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([]), @@ -182,4 +193,4 @@ module.exports.mkStubRoom = function() { members: [], }, }; -}; +}