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: [],
},
};
-};
+}