Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Make blackbox testing easier #325

Closed
coopy opened this issue Apr 19, 2016 · 28 comments
Closed

Make blackbox testing easier #325

coopy opened this issue Apr 19, 2016 · 28 comments
Labels

Comments

@coopy
Copy link

coopy commented Apr 19, 2016

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!

@hellendag
Copy link

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.

@hellendag
Copy link

@coopy, did this help resolve the issue for you?

@coopy
Copy link
Author

coopy commented May 3, 2016

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!

@skolmer
Copy link

skolmer commented Jul 1, 2016

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');

@acikojevic
Copy link

@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.

@davidchang
Copy link
Contributor

@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?

@acikojevic
Copy link

Actually, Internet Explorer has native textInput event from version 9+. But still not working, tried with keypress.

@ndelage
Copy link

ndelage commented Jan 19, 2017

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?

@mikeislearning
Copy link

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:

  • which element to simulate focus
  • which element to either simulate keyPress or change

@ryan-mars
Copy link

@mikeislearning did you get anything to work?

@tarjei
Copy link

tarjei commented Apr 21, 2017

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 :)

@kenju
Copy link

kenju commented Apr 27, 2017

@mikeislearning

Hello, your solution seems interesting. Would you tell us more, hopefully with working codes?

@mikeislearning
Copy link

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');

@kenju
Copy link

kenju commented Apr 28, 2017

@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 :)

@tarjei
Copy link

tarjei commented Apr 28, 2017

@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 :)

@kenju
Copy link

kenju commented May 10, 2017

I have found a better way for testing draft.js with enzyme in much more declarative way.

tl;dr

In order to test keyboard event with draft.js,

  1. use mount API for Full DOM Rendering
  2. find a DOM element with '.public-DraftEditor-content' classname
  3. dispatch the 'keyDown' event with simulate API

Example

Here 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);
  });
});

Details

Why find a DOM element with '.public-DraftEditor-content' classname ?

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

@nring
Copy link

nring commented Jul 20, 2017

@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.

@kenju
Copy link

kenju commented Jul 21, 2017

@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.

@nring
Copy link

nring commented Jul 21, 2017

@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.

@kenju
Copy link

kenju commented Jul 22, 2017

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:

  • Do you correctly call onChangeDraft upon your component?
  • Do you successfully setup JSDOM ( or alternative ) environment for your Full DOM testing?

@nring
Copy link

nring commented Jul 24, 2017

@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);
    });
  });
});

@kenju
Copy link

kenju commented Jul 25, 2017

@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:

  • As you can see at DraftEditor#render, onChange listener is NOT attached to the rendered React Component
  • Instead, in order to invoke DraftEditor#onChange, _update private methods should be called
  • In conclusion, how the onChange event triggered is different from other events (e.g. onKeyPress, onFocus, ...), therefore simulate does not work here

Therefore, we should find another way to test on the onChangeSpy. Here is my suggestion:

  • How about raising the abstraction of your test code (that is, from a single spec test to an integration test) and look at how the Editor behaves?
  • Instead of invoking onChange, set up another event triggers and use them

Btw, this is just a small advice on OSS activity :)

  • If you tell others why your code does not work, creating a reproducible repository/gist/etc is a great help for others who want to help you 😉
  • If you find something does not work as you expect, looking at the library code itself is a good starting point

@flarnie
Copy link
Contributor

flarnie commented Oct 25, 2017

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!

@flarnie flarnie closed this as completed Oct 25, 2017
@cloudkite
Copy link

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

@WickyNilliams
Copy link

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

@jordanell
Copy link

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.

@harryi3t
Copy link

harryi3t commented Jul 6, 2018

@skolmer Thanks for the snippet 👍 . It working for adding new text.
How do I clear the input before adding?

@XxAmrIbrahimxX
Copy link

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.

Problem with this , is that you'll get an warning saying :

Warning: An update to KbSaveArticles inside a test was not wrapped in act(...).

  When testing, code that causes React state updates should be wrapped into act(...):
  
  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests