Add some tests for the rich text editor
This commit is contained in:
parent
b0a4b017c3
commit
fec1e4d4c1
3 changed files with 179 additions and 14 deletions
|
@ -31,7 +31,7 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||||
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
||||||
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';
|
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 classNames from 'classnames';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
|
@ -51,6 +51,16 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
const KEY_M = 77;
|
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>
|
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
|
||||||
function mdownToHtml(mdown: string): string {
|
function mdownToHtml(mdown: string): string {
|
||||||
let html = marked(mdown) || "";
|
let html = marked(mdown) || "";
|
||||||
|
@ -480,7 +490,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (cmd.promise) {
|
if (cmd.promise) {
|
||||||
cmd.promise.done(function() {
|
cmd.promise.then(function() {
|
||||||
console.log("Command success.");
|
console.log("Command success.");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Command failure: %s", err);
|
console.error("Command failure: %s", err);
|
||||||
|
@ -520,7 +530,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.sentHistory.push(contentHTML);
|
this.sentHistory.push(contentHTML);
|
||||||
let sendMessagePromise = sendFn.call(this.client, this.props.room.roomId, contentText, contentHTML);
|
let sendMessagePromise = sendFn.call(this.client, this.props.room.roomId, contentText, contentHTML);
|
||||||
|
|
||||||
sendMessagePromise.done(() => {
|
sendMessagePromise.then(() => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent'
|
action: 'message_sent'
|
||||||
});
|
});
|
||||||
|
|
144
test/components/views/rooms/MessageComposerInput-test.js
Normal file
144
test/components/views/rooms/MessageComposerInput-test.js
Normal file
|
@ -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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -12,7 +12,7 @@ var MatrixEvent = jssdk.MatrixEvent;
|
||||||
* name to stdout.
|
* name to stdout.
|
||||||
* @param {Mocha.Context} context The test context
|
* @param {Mocha.Context} context The test context
|
||||||
*/
|
*/
|
||||||
module.exports.beforeEach = function(context) {
|
export function beforeEach(context) {
|
||||||
var desc = context.currentTest.fullTitle();
|
var desc = context.currentTest.fullTitle();
|
||||||
console.log();
|
console.log();
|
||||||
console.log(desc);
|
console.log(desc);
|
||||||
|
@ -26,7 +26,7 @@ module.exports.beforeEach = function(context) {
|
||||||
*
|
*
|
||||||
* @returns {sinon.Sandbox}; remember to call sandbox.restore afterwards.
|
* @returns {sinon.Sandbox}; remember to call sandbox.restore afterwards.
|
||||||
*/
|
*/
|
||||||
module.exports.stubClient = function() {
|
export function stubClient() {
|
||||||
var sandbox = sinon.sandbox.create();
|
var sandbox = sinon.sandbox.create();
|
||||||
|
|
||||||
var client = {
|
var client = {
|
||||||
|
@ -44,6 +44,16 @@ module.exports.stubClient = function() {
|
||||||
sendReadReceipt: sinon.stub().returns(q()),
|
sendReadReceipt: sinon.stub().returns(q()),
|
||||||
getRoomIdForAlias: sinon.stub().returns(q()),
|
getRoomIdForAlias: sinon.stub().returns(q()),
|
||||||
getProfileInfo: 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
|
// stub out the methods in MatrixClientPeg
|
||||||
|
@ -73,7 +83,7 @@ module.exports.stubClient = function() {
|
||||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
* @return {Object} a JSON object representing this event.
|
* @return {Object} a JSON object representing this event.
|
||||||
*/
|
*/
|
||||||
module.exports.mkEvent = function(opts) {
|
export function mkEvent(opts) {
|
||||||
if (!opts.type || !opts.content) {
|
if (!opts.type || !opts.content) {
|
||||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
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.
|
* @param {Object} opts Values for the presence.
|
||||||
* @return {Object|MatrixEvent} The event
|
* @return {Object|MatrixEvent} The event
|
||||||
*/
|
*/
|
||||||
module.exports.mkPresence = function(opts) {
|
export function mkPresence(opts) {
|
||||||
if (!opts.user) {
|
if (!opts.user) {
|
||||||
throw new Error("Missing user");
|
throw new Error("Missing user");
|
||||||
}
|
}
|
||||||
|
@ -132,7 +142,7 @@ module.exports.mkPresence = function(opts) {
|
||||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
* @return {Object|MatrixEvent} The event
|
* @return {Object|MatrixEvent} The event
|
||||||
*/
|
*/
|
||||||
module.exports.mkMembership = function(opts) {
|
export function mkMembership(opts) {
|
||||||
opts.type = "m.room.member";
|
opts.type = "m.room.member";
|
||||||
if (!opts.skey) {
|
if (!opts.skey) {
|
||||||
opts.skey = opts.user;
|
opts.skey = opts.user;
|
||||||
|
@ -145,7 +155,7 @@ module.exports.mkMembership = function(opts) {
|
||||||
};
|
};
|
||||||
if (opts.name) { opts.content.displayname = opts.name; }
|
if (opts.name) { opts.content.displayname = opts.name; }
|
||||||
if (opts.url) { opts.content.avatar_url = opts.url; }
|
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.
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
* @return {Object|MatrixEvent} The event
|
* @return {Object|MatrixEvent} The event
|
||||||
*/
|
*/
|
||||||
module.exports.mkMessage = function(opts) {
|
export function mkMessage(opts) {
|
||||||
opts.type = "m.room.message";
|
opts.type = "m.room.message";
|
||||||
if (!opts.msg) {
|
if (!opts.msg) {
|
||||||
opts.msg = "Random->" + Math.random();
|
opts.msg = "Random->" + Math.random();
|
||||||
|
@ -169,11 +179,12 @@ module.exports.mkMessage = function(opts) {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: opts.msg
|
body: opts.msg
|
||||||
};
|
};
|
||||||
return module.exports.mkEvent(opts);
|
return mkEvent(opts);
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports.mkStubRoom = function() {
|
export function mkStubRoom(roomId = null) {
|
||||||
return {
|
return {
|
||||||
|
roomId,
|
||||||
getReceiptsForEvent: sinon.stub().returns([]),
|
getReceiptsForEvent: sinon.stub().returns([]),
|
||||||
getMember: sinon.stub().returns({}),
|
getMember: sinon.stub().returns({}),
|
||||||
getJoinedMembers: sinon.stub().returns([]),
|
getJoinedMembers: sinon.stub().returns([]),
|
||||||
|
@ -182,4 +193,4 @@ module.exports.mkStubRoom = function() {
|
||||||
members: [],
|
members: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
Loading…
Reference in a new issue