I've been enjoying using draft-js; the approach to representing complex text editing states is great, and the API is pretty well documented.
Where I'm getting stuck is performing integration-level blackbox testing of components that I build with Draft.
Example: I've got a <RichTextEditor/> component, largely based on the rich example. The editor lives in an ecosystem of HTML in, HTML out, so I want to assert that the conversion is happening as expected.
I'd like to simulate a keypress with the end result of that character getting pushed onto the editorState, and the onChange callback being called. I can't figure out how to accomplish this.
I've tried grabbing the DOM node corresponding to the [role="textbox"] and simulating (with React TestUtils via Enzyme) a keyDown event with a payload of {which: [someKeyCode]}. I see this being handled in editOnKeyDown, but the onChange handler is never called.
I've tried generating a new editorState with something like this, but there's no obvious way to actually call update() on the <Editor/> instance.
/**
* Simulate typing a character into editor by generating a new editorState.
*
* @param {string} value Current editor state value serialized to string
* @param {string} char Character to type
* @returns {EditorState} A new editorState
*/
const simulateKeyPress = (currentEditorValue, charToAdd) => {
const editorState = currentEditorValue.trim().length ?
// Create internal editor state representation from serialized value
EditorState.createWithContent(
ContentState.createFromBlockArray(
processHTML(currentEditorValue)
)
) :
// Create empty editor state
EditorState.createEmpty();
// Create a new editorState by pushing the character onto the state.
return EditorState.push(
editorState,
ContentState.createFromText(charToAdd),
'insert-characters'
);
}
Is there an existing approach that I'm missing? If not, I'd be happy to discuss how blackbox testing could be made easier.
Thanks!
keydown and keypress events are not used for character insertion. Rather, the React polyfill for beforeInput is used for this: https://github.com/facebook/draft-js/blob/master/src/component/handlers/edit/editOnBeforeInput.js.
Within the polyfill, the keypress event _is_ used for browsers that do not have a native textInput event (Firefox, IE), since the keypress event is intended to indicate character insertion. keydown, however, is never used for input.
If you can generate an textInput event on the target node instead, you may find success.
I'd be cautious about using the role attribute here. There is a test ID prop that was added internally, and you may be able to use it to target your event: https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditor.react.js#L252. I haven't tried this myself, but it may solve things for you.
@coopy, did this help resolve the issue for you?
Thanks @hellendag. I spent a little bit of time trying to generate textInput events, without success. I decided to stop trying and spend the cycles on Selenium acceptance tests instead.
If I go back to this approach, I will definitely not use the role attribute but instead use the webDriverTestID prop. Thanks for the 馃惍 tip!
This JavaScript is working with ChromeDriver in Selenium to add text at the current cursor position:
function addTextToDraftJs(className, text) {
var components = document.getElementsByClassName(className);
if(components && components.length) {
var textarea = components[0].getElementsByClassName('public-DraftEditor-content')[0];
var textEvent = document.createEvent('TextEvent');
textEvent.initTextEvent ('textInput', true, true, null, text);
textarea.dispatchEvent(textEvent);
}
}
addTextToDraftJs('welcome-text-rte', 'Add this text at cursor position');
@skolmer
Yes, I can confirm that this works in chrome browser, but does anyone know how to do this on IE11? I just can't make it work, event seems to be dispatched, but nothing happens. Might be related with isTrusted property, but not sure.
@acikojevic - "Within the polyfill, the keypress event is used for browsers that do not have a native textInput event (Firefox, IE)" from hellendag on Apr 22 - what if you use keypress instead of the textInput event?
Actually, Internet Explorer has native textInput event from version 9+. But still not working, tried with keypress.
I'm tying to get this working in a test using enzyme, I've mounted the component if that matters.
React test utils doesn't have a mapping for the textInput event, so the typical enzyme approach of element.simulate('textInput', {data: 'abc'}) isn't available. Instead, I'm trying to trigger the event manually, which doesn't work either:
// setting both data & target.value, because I'm not sure which is used
const event = new Event('textInput', { data: 'abc', target: { value: 'abc' } });
// finding the draft-js editor & dispatching the event
wrapper.find('div[role="textbox"]').get(0).dispatchEvent(event);
Is there another way to go about this? Or a more manual way I can update EditorState as part of a test?
Is anyone still looking into getting this working with Enzyme?
I've had success with a few other custom React components by simulating a focus event, followed by keyPress or click.
So to get Draft working with Enzyme, I just need to know 2 things:
focuskeyPress or change@mikeislearning did you get anything to work?
I agree that this is too hard today - it would be great if there was a library for testing contenteditable implementations.
On 0.8.x we used
var draftEditorContent = field.find('.public-DraftEditor-content')
draftEditorContent.simulate('beforeInput', { data: value })
But this stopped working when we upgraded to 0.10 (and upgraded React to 15.5) so now we do this instead:
const editor = field.find(Editor)
let editorState = editor.props().editorState
var contentState = Modifier.insertText(editorState.getCurrentContent(),
editorState.getSelection(),
value, editorState.getCurrentInlineStyle(), null)
var newEditorState = EditorState.push(editorState, contentState, 'insert-characters')
editorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter())
editor.props().onChange(editorState)
It's not exactly black box but it simulates what we need to simulate :)
@mikeislearning
Hello, your solution seems interesting. Would you tell us more, hopefully with working codes?
Sorry for the late reply.
@ryanwmarsh - sadly haven't got anything to work yet with draft-js
@kenju - Sure thing, here are some examples from a test we're running:
// loadRoute is a function we're using to simulate the page going to this route, but is not necessary for this example
const components = await loadRoute('/projects/new');
const page = mount(components);
const form = page.find('form');
// Working with React Select (https://github.com/JedWatson/react-select)
const multiSelect = form.find('Select').find('input');
multiSelect.simulate('focus');
multiSelect.simulate('change', { target: { 'web developer' } });
multiSelect.simulate('keyDown', {keyCode: 13});
// Working with React DatePicker (https://github.com/Hacker0x01/react-datepicker)
const start_date = form.find('DatePicker').first();
start_date.find('input').simulate('focus');
start_date.find('Day').at(14).simulate('click');
const end_date = form.find('DatePicker').last();
end_date.find('input').simulate('focus');
end_date.find('Day').at(15).simulate('click');
@mikeislearning Thank you very much :)
@tarjei 's answer does work, only when what you want is text input. It sure things, but for the case we are encountering is to test command + S key input, that is to test keyCode with modifier key event.
For this case, manipulating EditorState manually fails to simulate the key event.
Here is a pseudo-code (be sure, it does not work):
import { shallow } from 'enzyme';
const wrapperComponent = shallow(EditorCore, props);
wrapperComponent.simulate('keyDown', {
keyCode: KeyCode.KEY_S,
altKey: true,
});
I am working on testing this keyboard input with modifier key to draft-js components, and will share if I can find the solution. But looking for the better/alternatives/workaround if you have ones :)
@kenju: Actually my example can be modified to do anything, but _it does not test draft-js_. I.e. the test is responsible for the output.
I agree that it is an inferior method, but it has the benefit of simpleness. I would love to see a draftjs-testing library :)
I have found a better way for testing draft.js with enzyme in much more declarative way.
In order to test keyboard event with draft.js,
mount API for Full DOM Renderingsimulate APIHere is a reproducible repository: https://github.com/kenju/enzyme-draftjs-sample/blob/master/src/Editor.jsx
import { mount } from 'enzyme';
...
describe('Editor', function () {
it('dispatch COMMAND_SAVE when command+s key pressesd', function () {
const dispatchSpy = sinon.spy();
dispatchSpy.withArgs(COMMAND_SAVE);
// 1. use `mount` API for Full DOM Rendering
const wrapper = mount(<Editor dispatch={dispatchSpy} />);
// 2. find a DOM element with '.public-DraftEditor-content' classname
const ed = wrapper.find('.public-DraftEditor-content');
// 3. dispatch the 'keyDown' event with `simulate` API
ed.simulate('keyDown', {
keyCode: KeyCode.KEY_S,
metaKey: false, // is IS_OSX=true, this should be true
ctrlKey: true,
altKey: false,
});
assert.equal(dispatchSpy.calledOnce, true);
assert.equal(dispatchSpy.withArgs(COMMAND_SAVE).calledOnce, true);
});
});
If you dive into the draft.js source code, you will find that the each events are attached to the inner div whose className is '.public-DraftEditor-content'.
See:
https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditor.react.js#L258-L276
@kenju I've been picking apart your example, but can't figure out how it translates to my own code. I don't need a onKeyCommand handler, so I'm just ensuring that onChange gets called and updates state. Have you found a reliable way for doing that? Thanks for posting a link to your repo too.
@nring
so I'm just ensuring that onChange gets called and updates state.
Um... what kind of changes do you want to watch, and what is supposed to trigger the change?
Unfortunately I have no idea about your problems until looking at the real code though.
@kenju What I'd like to have happen is that whenever a user makes a change in an Editor it triggers a callback to its parent component. For now that's just a keypress, but in the future that could could be richer content like inserting an image. That way I can change the parent's UI based on the Editor's content.
I'm designing it so that I'm wrapping the draft-js component in a more generic component that will have some common functions throughout my app. This wrapper will then be used in different scenarios (a comment, a post, a search input, etc).
This example is abbreviated, but this is my wrapper component.
class DraftEditor extends React.Component {
static propTypes = {
onChangeDraft: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
this.onChange = (editorState) => {
this.setState({ editorState }, () => {
this.props.onChangeDraft(this.state);
});
};
}
render() {
return (
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
/>
);
}
}
export default DraftEditor;
In my Enzyme tests, I'm trying to verify that the onChangeDraft function I pass in as a prop is executed when the content in the Editor component is changed. However, I've been unable to trigger this onChange event successfully.
The component looks fine.
I'm trying to verify that the onChangeDraft function I pass in as a prop is executed when the content in the Editor component is changed. However, I've been unable to trigger this onChange event successfully.
How do you test " the content in the Editor component is changed" ? I'd be glad to see the test code. Here is some suggestions:
onChangeDraft upon your component?@kenju Thanks for taking a look. This is a whittled down version of my test. I've commented where I'm running into problems.
describe('DraftEditor', () => {
let wrapper;
const defaultProps = {
className: 'test-class',
placeholder: 'Enter some text',
onChangeDraft: () => {},
};
const getMountComponent = (overrideProps) => {
const props = { ...defaultProps, ...overrideProps };
return mount(
<DraftEditor
className={props.className}
editingContent={props.editingContent}
placeholder={props.placeholder}
onChangeDraft={props.onChangeDraft}
/>
);
};
describe.only('onChangeDraft', () => {
it('calls props.onChange with JSON options', () => {
const onChangeSpy = sinon.spy();
wrapper = getMountComponent({ onChangeDraft: onChangeSpy });
// How to trigger the onChange event without calling it directly?
// None of these work:
// wrapper.find('.public-DraftEditor-content')...trigger keydown event
// wrapper.simulate('change')...
// wrapper.update();
expect(onChangeSpy.callCount).to.equal(1);
});
});
});
@nring
I have created a reproducible repo (https://github.com/kenju/draftjs-issues-35-sample ) and trying to figure out, still do not find any better solution. deleted since the issue get closed
However, I can tell you why your code does not work:
DraftEditor#render, onChange listener is NOT attached to the rendered React ComponentDraftEditor#onChange, _update private methods should be calledonChange event triggered is different from other events (e.g. onKeyPress, onFocus, ...), therefore simulate does not work hereTherefore, we should find another way to test on the onChangeSpy. Here is my suggestion:
onChange, set up another event triggers and use themBtw, this is just a small advice on OSS activity :)
At this point I see many potential answers to the original question, and some great discussion. This is a pretty old issue now though, so I'm closing it. Feel free to open a new issue if questions remain. Thanks to everyone who added information here!
I managed to get this working by creating fake paste events
function createPasteEvent(html: string) {
const text = html.replace('<[^>]*>', '');
return {
clipboardData: {
types: ['text/plain', 'text/html'],
getData: (type: string) => (type === 'text/plain' ? text : html),
},
};
}
test('should return markdown onChange', () => {
const onChange = mock().once();
const editor = mount(<RichTextEditor value="" onChange={onChange} />);
const textArea = editor.find('.public-DraftEditor-content');
textArea.simulate(
'paste',
createPasteEvent(`<b>bold</b> <i>italic</i> <br/> stuff`)
);
expect(onChange.args).toEqual([[`**bold** _italic_ \n stuff\n`]]);
});
where <RichTextEditor /> is just a in-house wrapper around draftjs Editor that handles markdown
The code has changed since it was originally linked, so here's the line of code with webDriverTestID prop: https://github.com/facebook/draft-js/blob/0a1f981a42ba665471bf35e3955560988de24c78/src/component/base/DraftEditor.react.js#L320
For anyone still looking for an answer to this problem, here is what worked for me:
import {
ContentState,
EditorState,
} from 'draft-js';
export default async function fillRTE(component, value) {
const input = component.find('DraftEditor');
input.instance().update(EditorState.createWithContent(ContentState.createFromText(value)));
}
Obviously this abandons the notion of using simulate, but it does manage to update the editor with whatever value is passed. I found this worked well for testing form submission code when inputs needed to be set ahead of time.
@skolmer Thanks for the snippet 馃憤 . It working for adding new text.
How do I clear the input before adding?
Most helpful comment
I managed to get this working by creating fake paste events
where
<RichTextEditor />is just a in-house wrapper around draftjs Editor that handles markdown