Skip to content

Commit

Permalink
Merge bdef1df into 829b5da
Browse files Browse the repository at this point in the history
  • Loading branch information
pomek committed Mar 22, 2019
2 parents 829b5da + bdef1df commit 520f7b3
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 64 deletions.
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -25,6 +25,9 @@ npm install

### Executing tests

Before starting tests execution, you need to build the package. You can use `npm run build` in order to build the production-ready version
or `npm run develop` which produces a development version with attached watcher for all sources files.

```bash
npm run test -- [additional options]
# or
Expand All @@ -46,8 +49,6 @@ an environment variable, e.g.:
BROWSER_STACK_USERNAME=[...] BROWSER_STACK_ACCESS_KEY=[...] npm t -- -b BrowserStack_Edge,BrowserStack_Safari -c
```

If you are going to change the source (`src/ckeditor.jsx`) file, remember about rebuilding the package. You can use `npm run develop` in order to do it automatically.

### Building the package

Build a minified version of the package that is ready to publish:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -24,7 +24,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"@ckeditor/ckeditor5-build-classic": "^11.0.1",
"@ckeditor/ckeditor5-build-classic": "^12.0.0",
"@ckeditor/ckeditor5-dev-env": "^13.0.2",
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
"babel-loader": "^8.0.5",
Expand Down
32 changes: 24 additions & 8 deletions src/ckeditor.jsx
Expand Up @@ -17,18 +17,17 @@ export default class CKEditor extends React.Component {
this.domContainer = React.createRef();
}

componentDidUpdate() {
if ( !this.editor ) {
return;
// This component should never be updated by React itself.
shouldComponentUpdate( nextProps ) {
if ( this._shouldUpdateContent( nextProps ) ) {
this.editor.setData( nextProps.data );
}

if ( 'data' in this.props && this.props.data !== this.editor.getData() ) {
this.editor.setData( this.props.data );
if ( 'disabled' in nextProps ) {
this.editor.isReadOnly = nextProps.disabled;
}

if ( 'disabled' in this.props ) {
this.editor.isReadOnly = this.props.disabled;
}
return false;
}

// Initialize the editor when the component is mounted.
Expand Down Expand Up @@ -102,6 +101,23 @@ export default class CKEditor extends React.Component {
} );
}
}

_shouldUpdateContent( nextProps ) {
// Check whether `nextProps.data` is equal to `this.props.data` is required if somebody defined the `#data`
// property as a static string and updated a state of component when the editor's content has been changed.
// If we avoid checking those properties, the editor's content will back to the initial value because
// the state has been changed and React will call this method.
if ( this.props.data === nextProps.data ) {
return false;
}

// We should not change data if the editor's content is equal to the `#data` property.
if ( this.editor.getData() === nextProps.data ) {
return false;
}

return true;
}
}

// Properties definition.
Expand Down
1 change: 1 addition & 0 deletions tests/39/1.jsx
Expand Up @@ -2,6 +2,7 @@
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { configure, mount } from 'enzyme';
Expand Down
153 changes: 153 additions & 0 deletions tests/ckeditor-integration.jsx
@@ -0,0 +1,153 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import CKEditor from '../src/ckeditor.jsx';

configure( { adapter: new Adapter() } );

const Editor = ( props ) => {
return (
<CKEditor editor={ ClassicEditor } { ...props } />
)
};

class AppUsingState extends React.Component {
constructor( props ) {
super( props );

this.state = {
content: ''
};

this.editor = null;
}

render() {
return (
<Editor
data={ this.state.content }
onChange={ (evt, editor) => this.setState( { content: editor.getData() } ) }
onInit={ _editor => this.editor = _editor }
/>
)
}
}

class AppUsingStaticString extends AppUsingState {
render() {
return (
<Editor
data={ '<p>Initial data.</p>' }
onChange={ (evt, editor) => this.setState( { content: editor.getData() } ) }
onInit={ _editor => this.editor = _editor }
/>
)
}
}

describe( 'CKEditor Component - integration', () => {
describe('update the editor\'s content', () => {
// Common usage of the component - a component's state is passed to the <CKEditor> component.
describe( '#data is connected with the state', () => {
let div, component;

beforeEach( () => {
div = document.createElement( 'div' );
document.body.appendChild( div );

return new Promise( resolve => {
component = ReactDOM.render( <AppUsingState />, div );

setTimeout( resolve );
} );
} );

afterEach( () => {
div.remove();
} );

it( 'returns initial state', () => {
const editor = component.editor;

expect( editor.getData() ).to.equal( '' );
expect( component.state.content ).to.equal( '' );
} );

it( 'updates the editor\'s content when state has changed', () => {
component.setState( { content: 'Foo.' } );

const editor = component.editor;

// State has been updated because we attached the `onChange` callback.
expect( component.state.content ).to.equal( '<p>Foo.</p>' );
expect( editor.getData() ).to.equal( '<p>Foo.</p>' );
} );

it( 'updates state when a user typed something', () => {
const editor = component.editor;

editor.model.change( writer => {
writer.insertText( 'Plain text.', editor.model.document.selection.getFirstPosition() );
} );

expect( component.state.content ).to.equal( '<p>Plain text.</p>' );
expect( editor.getData() ).to.equal( '<p>Plain text.</p>' );
} );
} );

// Non-common usage but it shouldn't blow or freeze the editor.
describe( '#data is a static string', () => {
let div, component;

beforeEach( () => {
div = document.createElement( 'div' );
document.body.appendChild( div );

return new Promise( resolve => {
component = ReactDOM.render( <AppUsingStaticString />, div );

setTimeout( resolve );
} );
} );

afterEach( () => {
div.remove();
} );

it( 'returns initial state', () => {
const editor = component.editor;

expect( component.state.content ).to.equal( '' );
expect( editor.getData() ).to.equal( '<p>Initial data.</p>' );
} );

it( 'updates the editor\'s content when state has changed', () => {
component.setState( { content: 'Foo.' } );

const editor = component.editor;

// The editor's content has not been updated because the component's `[data]` property isn't connected with it.
expect( editor.getData() ).to.equal( '<p>Initial data.</p>' );
expect( component.state.content ).to.equal( 'Foo.' );
} );

it( 'updates state when a user typed something', () => {
const editor = component.editor;

editor.model.change( writer => {
writer.insertText( 'Plain text. ', editor.model.document.selection.getFirstPosition() );
} );

expect( component.state.content ).to.equal( '<p>Plain text. Initial data.</p>' );
expect( editor.getData() ).to.equal( '<p>Plain text. Initial data.</p>' );
} );
} );
} );
} );
68 changes: 15 additions & 53 deletions tests/ckeditor.jsx
Expand Up @@ -87,6 +87,21 @@ describe( 'CKEditor Component', () => {
} );
} );

it( 'must not update the component by React itself', done => {
sandbox.stub( Editor, 'create' ).resolves( new Editor() );

wrapper = mount( <CKEditor editor={ Editor }/> );

setTimeout( () => {
const component = wrapper.instance();

// This method always is called with an object with component's properties.
expect( component.shouldComponentUpdate( {} ) ).to.equal( false );

done();
} );
} );

it( 'displays an error if something went wrong', done => {
const error = new Error( 'Something went wrong.' );
const consoleErrorStub = sandbox.stub( console, 'error' );
Expand All @@ -108,59 +123,6 @@ describe( 'CKEditor Component', () => {
} );

describe( 'properties', () => {
it( 'sets editor\'s data if properties have changed and contain the "data" key', done => {
const editorInstance = new Editor();

sandbox.stub( Editor, 'create' ).resolves( editorInstance );
sandbox.stub( editorInstance, 'setData' );
sandbox.stub( editorInstance, 'getData' ).returns( '<p>&nbsp;</p>' );

wrapper = mount( <CKEditor editor={ Editor } /> );

setTimeout( () => {
wrapper.setProps( { data: '<p>Foo Bar.</p>' });

expect( editorInstance.setData.calledOnce ).to.be.true;
expect( editorInstance.setData.firstCall.args[ 0 ] ).to.equal( '<p>Foo Bar.</p>' );

done();
} );
} );

it( 'does not update the editor\'s data if value under "data" key is equal to editor\'s data', done => {
const editorInstance = new Editor();

sandbox.stub( Editor, 'create' ).resolves( editorInstance );
sandbox.stub( editorInstance, 'setData' );
sandbox.stub( editorInstance, 'getData' ).returns( '<p>Foo Bar.</p>' );

wrapper = mount( <CKEditor editor={ Editor } /> );

setTimeout( () => {
wrapper.setProps( { data: '<p>Foo Bar.</p>' });

expect( editorInstance.setData.calledOnce ).to.be.false;

done();
} );
} );

it( 'does not set editor\'s data if the editor is not ready', () => {
const editorInstance = new Editor();

sandbox.stub( Editor, 'create' ).resolves( editorInstance );
sandbox.stub( editorInstance, 'setData' );

wrapper = mount( <CKEditor editor={ Editor } /> );

const component = wrapper.instance();

component.componentDidUpdate( { data: 'Foo' } );

expect( component.editor ).to.be.null;
expect( editorInstance.setData.called ).to.be.false;
} );

describe( '#config', () => {
it( 'should replace all react DOM references with the `current` DOM element', done => {
const spy = sinon.spy( Editor, 'create' );
Expand Down

0 comments on commit 520f7b3

Please sign in to comment.