/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Input from '../../src/input'; import Delete from '../../src/delete'; import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range'; import ViewText from '@ckeditor/ckeditor5-engine/src/view/text'; import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; /* global document */ describe( 'injectAndroidBackspaceMutationsHandling', () => { let editor, model, modelRoot, view, viewDocument, viewRoot, mutationsSpy, dateNowStub; testUtils.createSinonSandbox(); before( () => { mutationsSpy = sinon.spy(); } ); beforeEach( () => { const domElement = document.createElement( 'div' ); document.body.appendChild( domElement ); return ClassicTestEditor.create( domElement, { plugins: [ Input, Delete, Paragraph, Heading, Italic, Undo ] } ) .then( newEditor => { editor = newEditor; model = editor.model; modelRoot = model.document.getRoot(); view = editor.editing.view; viewDocument = view.document; viewRoot = viewDocument.getRoot(); editor.setData( '

Heading 1

Paragraph

Heading 2

' ); } ); } ); afterEach( () => { if ( dateNowStub ) { dateNowStub.restore(); dateNowStub = null; } mutationsSpy.resetHistory(); return editor.destroy(); } ); it( 'should handle block merging', () => { // 1. Set selection to '

Heading 1

{}Paragraph

Heading 2

'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 1 ), 0, modelRoot.getChild( 1 ), 0 ) ); } ); const modelContent = 'Heading 1[]ParagraphHeading 2'; const viewContent = '

Heading 1

{}Paragraph

Heading 2

'; expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); // 2. Create mutations which are result of changing HTML to '

Heading 1{}Paragraph

Heading 2

'. const mutations = [ { // `heading1` new text mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ).getChild( 0 ) ], oldChildren: [ viewRoot.getChild( 0 ).getChild( 0 ) ], node: viewRoot.getChild( 0 ) }, { // `paragraph` text removal mutation type: 'children', newChildren: [], oldChildren: [ viewRoot.getChild( 1 ).getChild( 0 ) ], node: viewRoot.getChild( 1 ) }, { // `paragraph` removal mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ) ], oldChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 1 ) ], node: viewRoot } ]; // 3. Simulate 'Backspace' flow on Android. simulateBackspace( mutations ); expect( mutationsSpy.callCount ).to.equal( 0 ); expect( getModelData( model ) ).to.equal( 'Heading 1[]ParagraphHeading 2' ); expect( getViewData( view ) ).to.equal( '

Heading 1{}Paragraph

Heading 2

' ); // Due ot `Undo` issue the selection is after paragraph after undoing changes (ckeditor5-undo/issues/64). expectContentAfterUndo( 'Heading 1[]ParagraphHeading 2', '

Heading 1{}

Paragraph

Heading 2

' ); } ); it.skip( 'should handle two entire blocks removal', () => { // 1. Set selection to '

{Heading 1

Paragraph}

Heading 2

'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 1 ), 9 ) ); } ); const modelContent = '[Heading 1Paragraph]Heading 2'; const viewContent = '

{Heading 1

Paragraph}

Heading 2

'; expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); // 2. Create mutations which are result of changing HTML to '

[]

Heading 2

'. const mutations = [ { // `heading1` text removal mutation type: 'children', newChildren: [ new ViewElement( 'br' ) ], oldChildren: [ viewRoot.getChild( 0 ).getChild( 0 ) ], node: viewRoot.getChild( 0 ) }, { // `paragraph` removal mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 2 ) ], oldChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 1 ), viewRoot.getChild( 2 ) ], node: viewRoot } ]; // 3. Create selection which simulate Android behaviour where upon pressing `Backspace` // selection is changed to `

Heading 1

Paragraph{}

Heading 2

`. const newSelection = ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 1 ), 9, modelRoot.getChild( 1 ), 9 ); // 4. Simulate 'Backspace' flow on Android. simulateBackspace( mutations, newSelection ); expect( mutationsSpy.callCount ).to.equal( 0 ); expect( getModelData( model ) ).to.equal( '[]Heading 2' ); expect( getViewData( view ) ).to.equal( '

[]

Heading 2

' ); expectContentAfterUndo( '[Heading 1Paragraph]Heading 2', '

{Heading 1

Paragraph}

Heading 2

' ); } ); it.skip( 'should handle two partially selected blocks removal', () => { // 1. Set selection to '

Hea{ding 1

Paragraph}

Heading 2

'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 3, modelRoot.getChild( 1 ), 9 ) ); } ); const modelContent = 'Hea[ding 1Paragraph]Heading 2'; const viewContent = '

Hea{ding 1

Paragraph}

Heading 2

'; expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); // 2. Create mutations which are result of changing HTML to '

Hea{}

Heading 2

'. const mutations = [ { // `heading1` text partial removal mutation type: 'text', newText: 'Hea', oldText: 'Heading 1', node: viewRoot.getChild( 0 ).getChild( 0 ) }, { // `paragraph` removal mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 2 ) ], oldChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 1 ), viewRoot.getChild( 2 ) ], node: viewRoot } ]; // 3. Create selection which simulate Android behaviour where upon pressing `Backspace` // selection is changed to `

Heading 1

Paragraph{}

Heading 2

`. const newSelection = ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 1 ), 9, modelRoot.getChild( 1 ), 9 ); // 4. Simulate 'Backspace' flow on Android. simulateBackspace( mutations, newSelection ); expect( mutationsSpy.callCount ).to.equal( 0 ); expect( getModelData( model ) ).to.equal( 'Hea[]Heading 2' ); expect( getViewData( view ) ).to.equal( '

Hea{}

Heading 2

' ); expectContentAfterUndo( modelContent, viewContent ); } ); it.skip( 'should handle blocks removal if selection ends on the boundary of inline element', () => { editor.setData( '

Heading 1

Paragraph

Heading 2

' ); // 1. Set selection to '

{Heading 1

Paragraph

]Heading 2

'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 2 ), 0 ) ); } ); const modelContent = '[Heading 1Paragraph' + ']<$text italic="true">Heading 2'; const viewContent = '

{Heading 1

Paragraph

]Heading 2

'; expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); // 2. Create mutations which are result of changing HTML to '

{}Heading 2

'. const mutations = [ { // `heading1` text to children mutation type: 'children', newChildren: Array.from( viewRoot.getChild( 2 ).getChildren() ), oldChildren: [ viewRoot.getChild( 0 ).getChild( 0 ) ], node: viewRoot.getChild( 0 ) }, { // `heading2` children removal mutation type: 'children', newChildren: [], oldChildren: Array.from( viewRoot.getChild( 2 ).getChildren() ), node: viewRoot.getChild( 2 ) }, { // `paragrpah` and `heading2` removal mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ) ], oldChildren: Array.from( viewRoot.getChildren() ), node: viewRoot } ]; // 3. Create selection which simulate Android behaviour where upon pressing `Backspace` // selection is changed to `

Heading 1

Paragraph

{}Heading 2

`. const newSelection = ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 0, modelRoot.getChild( 2 ), 0 ); // 4. Simulate 'Backspace' flow on Android. simulateBackspace( mutations, newSelection ); expect( mutationsSpy.callCount ).to.equal( 0 ); expect( getModelData( model ) ).to.equal( '<$text italic="true">[]Heading 2' ); expect( getViewData( view ) ).to.equal( '

{}Heading 2

' ); expectContentAfterUndo( modelContent, viewContent ); } ); it( 'should handle selection changed by the user before `backspace` on block merging', () => { // 1. Stub `Date.now` so we can simulate user selection change timing. let dateNowValue = 0; dateNowStub = sinon.stub( Date, 'now' ).callsFake( () => { dateNowValue += 500; return dateNowValue; } ); editor.setData( '

Heading 1

Paragraph

Heading 2

' ); // 2. Set selection to '

{Heading 1

Paragraph

]Heading 2

'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 2 ), 0 ) ); } ); const modelContent = '[Heading 1Paragraph' + ']<$text italic="true">Heading 2'; const viewContent = '

{Heading 1

Paragraph

]Heading 2

'; expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); // 3. Create mutations which are result of changing HTML to '

Heading 1

Paragraph{}Heading 2

'. // This is still a block container removal so 'injectAndroidBackspaceMutationsHandling' will get triggered. const mutations = [ { // `paragraph` children added mutation type: 'children', newChildren: [ viewRoot.getChild( 1 ).getChild( 0 ) ].concat( Array.from( viewRoot.getChild( 2 ).getChildren() ) ), oldChildren: Array.from( viewRoot.getChild( 1 ).getChildren() ), node: viewRoot.getChild( 1 ) }, { // `heading2` children removal mutation type: 'children', newChildren: [], oldChildren: Array.from( viewRoot.getChild( 2 ).getChildren() ), node: viewRoot.getChild( 2 ) }, { // `heading2` removal mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 1 ) ], oldChildren: Array.from( viewRoot.getChildren() ), node: viewRoot } ]; // 4. Simulate user selection change which is identical as Android native change on 'Backspace'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 0, modelRoot.getChild( 2 ), 0 ) ); } ); // 5. Simulate 'Backspace' flow on Android. simulateBackspace( mutations ); expect( mutationsSpy.callCount ).to.equal( 0 ); expect( getModelData( model ) ).to.equal( 'Heading 1Paragraph[]' + '<$text italic="true">Heading 2' ); expect( getViewData( view ) ).to.equal( '

Heading 1

Paragraph{}Heading 2

' ); // Due ot `Undo` issue the selection is after paragraph after undoing changes (ckeditor5-undo/issues/64). expectContentAfterUndo( 'Heading 1Paragraph[]' + '<$text italic="true">Heading 2', '

Heading 1

Paragraph{}

Heading 2

' ); } ); it( 'should not be triggered for container insertion mutations', () => { // 1. Set selection to '

Heading 1

Paragraph{}

Heading 2

'. model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 1 ), 9, modelRoot.getChild( 1 ), 9 ) ); } ); const modelContent = 'Heading 1Paragraph[]Heading 2'; const viewContent = '

Heading 1

Paragraph{}

Heading 2

'; expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); const viewFoo = new ViewText( 'foo' ); const viewP = new ViewElement( 'p', null, viewFoo ); // 2. Create mutations which are result of changing HTML to '

Heading 1

Paragraph{}

Foo

Heading 2

'. const mutations = [ { // new paragraph insertion mutation type: 'children', newChildren: [ viewRoot.getChild( 0 ), viewRoot.getChild( 1 ), viewP, viewRoot.getChild( 2 ) ], oldChildren: Array.from( viewRoot.getChildren() ), node: viewRoot.getChild( 0 ) } ]; // 3. Spy mutations listener calls. It should be called ones // as it was not stopped by 'injectAndroidBackspaceMutationsHandling' handler. viewDocument.on( 'mutations', mutationsSpy, { priority: 'lowest' } ); // 4. Fire mutations event. viewDocument.fire( 'mutations', mutations ); expect( mutationsSpy.callCount ).to.equal( 1 ); expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); } ); function simulateBackspace( mutations, newSelection ) { // Spy mutations listener calls. If Android handler was triggered it should prevent further calls. viewDocument.on( 'mutations', mutationsSpy, { priority: 'lowest' } ); // Simulate selection change on Android devices before `keydown` event. if ( newSelection ) { model.change( writer => { writer.setSelection( newSelection ); } ); } // Fire `keydown` event with `229` key code so it is consistent with what happens on Android devices. viewDocument.fire( 'keydown', { keyCode: 229 } ); // Fire mutations event. viewDocument.fire( 'mutations', mutations ); } function expectContentAfterUndo( modelContent, viewContent ) { editor.execute( 'undo' ); expect( getModelData( model ) ).to.equal( modelContent ); expect( getViewData( view ) ).to.equal( viewContent ); } } );