Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1800 from ckeditor/t/ckeditor5/1304
Browse files Browse the repository at this point in the history
Other: Added error handling to the common code execution paths. Part of ckeditor/ckeditor5#1304.
  • Loading branch information
Piotr Jasiun committed Oct 9, 2019
2 parents 5560824 + ae2d706 commit 220b52f
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 37 deletions.
43 changes: 26 additions & 17 deletions src/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import deleteContent from './utils/deletecontent';
import modifySelection from './utils/modifyselection';
import getSelectedContent from './utils/getselectedcontent';
import { injectSelectionPostFixer } from './utils/selection-post-fixer';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

/**
* Editor's data model. Read about the model in the
Expand Down Expand Up @@ -153,14 +154,18 @@ export default class Model {
* @returns {*} Value returned by the callback.
*/
change( callback ) {
if ( this._pendingChanges.length === 0 ) {
// If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
this._pendingChanges.push( { batch: new Batch(), callback } );

return this._runPendingChanges()[ 0 ];
} else {
// If this is not the outermost block, just execute the callback.
return callback( this._currentWriter );
try {
if ( this._pendingChanges.length === 0 ) {
// If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
this._pendingChanges.push( { batch: new Batch(), callback } );

return this._runPendingChanges()[ 0 ];
} else {
// If this is not the outermost block, just execute the callback.
return callback( this._currentWriter );
}
} catch ( err ) {
CKEditorError.rethrowUnexpectedError( err, this );
}
}

Expand Down Expand Up @@ -198,17 +203,21 @@ export default class Model {
* @param {Function} callback Callback function which may modify the model.
*/
enqueueChange( batchOrType, callback ) {
if ( typeof batchOrType === 'string' ) {
batchOrType = new Batch( batchOrType );
} else if ( typeof batchOrType == 'function' ) {
callback = batchOrType;
batchOrType = new Batch();
}
try {
if ( typeof batchOrType === 'string' ) {
batchOrType = new Batch( batchOrType );
} else if ( typeof batchOrType == 'function' ) {
callback = batchOrType;
batchOrType = new Batch();
}

this._pendingChanges.push( { batch: batchOrType, callback } );
this._pendingChanges.push( { batch: batchOrType, callback } );

if ( this._pendingChanges.length == 1 ) {
this._runPendingChanges();
if ( this._pendingChanges.length == 1 ) {
this._runPendingChanges();
}
} catch ( err ) {
CKEditorError.rethrowUnexpectedError( err, this );
}
}

Expand Down
44 changes: 24 additions & 20 deletions src/view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,29 +457,33 @@ export default class View {
);
}

// Recursive call to view.change() method - execute listener immediately.
if ( this._ongoingChange ) {
return callback( this._writer );
}

// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
// event for all nested calls.
this._ongoingChange = true;
const callbackResult = callback( this._writer );
this._ongoingChange = false;
try {
// Recursive call to view.change() method - execute listener immediately.
if ( this._ongoingChange ) {
return callback( this._writer );
}

// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all changes.
// Also, we don't need to render anything if there're no changes since last rendering.
if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) {
this._postFixersInProgress = true;
this.document._callPostFixers( this._writer );
this._postFixersInProgress = false;
// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
// event for all nested calls.
this._ongoingChange = true;
const callbackResult = callback( this._writer );
this._ongoingChange = false;

// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all
// changes. Also, we don't need to render anything if there're no changes since last rendering.
if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) {
this._postFixersInProgress = true;
this.document._callPostFixers( this._writer );
this._postFixersInProgress = false;

this.fire( 'render' );
}

this.fire( 'render' );
return callbackResult;
} catch ( err ) {
CKEditorError.rethrowUnexpectedError( err, this );
}

return callbackResult;
}

/**
Expand Down
54 changes: 54 additions & 0 deletions tests/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import ModelSelection from '../../src/model/selection';
import ModelDocumentFragment from '../../src/model/documentfragment';
import Batch from '../../src/model/batch';
import { getData, setData, stringify } from '../../src/dev-utils/model';
import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

describe( 'Model', () => {
let model, schema, changes;
Expand Down Expand Up @@ -321,6 +323,58 @@ describe( 'Model', () => {
expect( writer.batch.type ).to.equal( 'transparent' );
} );
} );

it( 'should catch a non-ckeditor error inside the `change()` block and throw the CKEditorError error outside of it', () => {
const error = new TypeError( 'foo' );
error.stack = 'bar';

expectToThrowCKEditorError( () => {
model.change( () => {
throw error;
} );
}, /unexpected-error/, model, {
originalError: {
message: 'foo',
stack: 'bar',
name: 'TypeError'
}
} );
} );

it( 'should throw the original CKEditorError error if it was thrown inside the `change()` block', () => {
expectToThrowCKEditorError( () => {
model.change( () => {
throw new CKEditorError( 'foo', null, { foo: 1 } );
} );
}, /foo/, null, { foo: 1 } );
} );

it( 'should catch a non-ckeditor error inside the `enqueueChange()` block and throw the CKEditorError error outside of it', () => {
const error = new TypeError( 'foo' );
error.stack = 'bar';

expectToThrowCKEditorError( () => {
model.enqueueChange( () => {
throw error;
} );
}, /unexpected-error/, model, {
originalError: {
message: 'foo',
stack: 'bar',
name: 'TypeError'
}
} );
} );

it( 'should throw the original CKEditorError error if it was thrown inside the `enqueueChange()` block', () => {
const err = new CKEditorError( 'foo', null, { foo: 1 } );

expectToThrowCKEditorError( () => {
model.enqueueChange( () => {
throw err;
} );
}, /foo/, null, { foo: 1 } );
} );
} );

describe( 'applyOperation()', () => {
Expand Down
26 changes: 26 additions & 0 deletions tests/view/view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import createViewRoot from '../_utils/createroot';
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
import env from '@ckeditor/ckeditor5-utils/src/env';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

describe( 'view', () => {
const DEFAULT_OBSERVERS_COUNT = 6;
Expand Down Expand Up @@ -802,6 +803,31 @@ describe( 'view', () => {
expect( result2 ).to.equal( true );
expect( result3 ).to.undefined;
} );

it( 'should catch native errors and wrap them into the CKEditorError errors', () => {
const error = new TypeError( 'foo' );
error.stack = 'bar';

expectToThrowCKEditorError( () => {
view.change( () => {
throw error;
} );
}, /unexpected-error/, view, {
originalError: {
message: 'foo',
stack: 'bar',
name: 'TypeError'
}
} );
} );

it( 'should rethrow custom CKEditorError errors', () => {
expectToThrowCKEditorError( () => {
view.change( () => {
throw new CKEditorError( 'foo', view );
} );
}, /foo/, view );
} );
} );

describe( 'createPositionAt()', () => {
Expand Down

0 comments on commit 220b52f

Please sign in to comment.