/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* globals document */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Delete from '@ckeditor/ckeditor5-typing/src/delete'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import TableEditing from '../../src/tableediting'; import { viewTable } from '../_utils/utils'; describe( 'Table cell refresh post-fixer', () => { let editor, model, doc, root, view, element; testUtils.createSinonSandbox(); beforeEach( () => { element = document.createElement( 'div' ); document.body.appendChild( element ); return ClassicTestEditor.create( element, { plugins: [ Paragraph, TableEditing, Delete ] } ) .then( newEditor => { editor = newEditor; model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); view = editor.editing.view; editor.model.schema.register( 'block', { inheritAllFrom: '$block' } ); editor.conversion.elementToElement( { model: 'block', view: 'div' } ); model.schema.extend( '$block', { allowAttributes: [ 'foo', 'bar' ] } ); editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); editor.conversion.attributeToAttribute( { model: 'bar', view: 'bar' } ); } ); } ); afterEach( () => { element.remove(); return editor.destroy(); } ); function getViewForParagraph( table ) { return editor.editing.mapper.toViewElement( table.getNodeByPath( [ 0, 0, 0 ] ) ); } it( 'should rename to

when adding element to the same table cell (append)', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); const paragraph = writer.createElement( 'paragraph' ); writer.insert( paragraph, nodeByPath, 'after' ); writer.setSelection( nodeByPath.nextSibling, 0 ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename to

when adding element to the same table cell (prepend)', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); const paragraph = writer.createElement( 'paragraph' ); writer.insert( paragraph, nodeByPath, 'before' ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename to

when adding more elements to the same table cell', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); const paragraph1 = writer.createElement( 'paragraph' ); const paragraph2 = writer.createElement( 'paragraph' ); writer.insert( paragraph1, nodeByPath, 'after' ); writer.insert( paragraph2, nodeByPath, 'after' ); writer.setSelection( nodeByPath.nextSibling, 0 ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename to

on adding other block element to the same table cell', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); const block = writer.createElement( 'block' ); writer.insert( block, nodeByPath, 'after' ); writer.setSelection( nodeByPath.nextSibling, 0 ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename to

on adding multiple other block elements to the same table cell', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); const block1 = writer.createElement( 'block' ); const block2 = writer.createElement( 'block' ); writer.insert( block1, nodeByPath, 'after' ); writer.insert( block2, nodeByPath, 'after' ); writer.setSelection( nodeByPath.nextSibling, 0 ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should not rename to

when adding and removing ', () => { editor.setData( '

00

' ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); const paragraph = writer.createElement( 'paragraph' ); writer.insert( paragraph, nodeByPath, 'after' ); writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '00' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should properly rename the same element on consecutive changes', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); writer.insertElement( 'paragraph', nodeByPath, 'after' ); writer.setSelection( nodeByPath.nextSibling, 0 ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); model.change( writer => { writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '00' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename to

when setting attribute on ', () => { editor.setData( '

00

' ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename

to when removing one of two paragraphs inside table cell', () => { editor.setData( viewTable( [ [ '

00

foo

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '00' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename

to when removing all but one paragraph inside table cell', () => { editor.setData( viewTable( [ [ '

00

foo

bar

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '00' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should rename

to when removing attribute from ', () => { editor.setData( '

00

' ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '00' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should keep

in the view when attribute value is changed', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should keep

in the view when adding another attribute to a with other attributes', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.setAttribute( 'bar', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should keep

in the view when adding another attribute to a and removing attribute that is already set', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.setAttribute( 'bar', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) ); writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should keep

in the view when attribute value is changed (table cell with multiple blocks)', () => { editor.setData( viewTable( [ [ '

00

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

00

00

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should do nothing on rename to other block', () => { editor.setData( viewTable( [ [ '

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'block' ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '
00
' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should do nothing on adding to existing paragraphs', () => { editor.setData( viewTable( [ [ '

a

b

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.insertElement( 'paragraph', table.getNodeByPath( [ 0, 0, 1 ] ), 'after' ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '

a

b

' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should do nothing when setting attribute on block item other then ', () => { editor.setData( viewTable( [ [ '
foo
' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '
foo
' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.equal( previousView ); } ); it( 'should rename

in to when removing (table cell with 2 paragraphs)', () => { editor.setData( viewTable( [ [ '

00

00

' ] ] ) ); const table = root.getChild( 0 ); const previousView = getViewForParagraph( table ); model.change( writer => { writer.remove( writer.createRangeOn( table.getNodeByPath( [ 0, 0, 1 ] ) ) ); } ); assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ [ '00' ] ], { asWidget: true } ) ); expect( getViewForParagraph( table ) ).to.not.equal( previousView ); } ); it( 'should update view selection after deleting content', () => { editor.setData( viewTable( [ [ '

foo

bar

' ] ] ) ); const tableCell = root.getNodeByPath( [ 0, 0, 0 ] ); // Replace table cell contents with paragraph - as model.deleteContent() does. model.change( writer => { writer.remove( writer.createRangeIn( tableCell ) ); const paragraph = writer.createElement( 'paragraph' ); writer.insert( paragraph, writer.createPositionAt( tableCell, 0 ) ); // Set selection to newly created paragraph. writer.setSelection( paragraph, 0 ); } ); const viewRange = view.document.selection.getFirstRange(); // Trying to map view selection to DOM range shouldn't throw after post-fixer will fix inserted

to . expect( () => view.domConverter.viewRangeToDom( viewRange ) ).to.not.throw(); } ); it( 'should not update view selection after other feature set selection', () => { editor.model.schema.register( 'widget', { isObject: true, isBlock: true, allowWhere: '$block' } ); editor.conversion.elementToElement( { model: 'widget', view: 'widget' } ); editor.setData( viewTable( [ [ '

foo[]

' ] ] ) ); const spy = sinon.spy(); view.document.selection.on( 'change', spy ); // Insert a widget in table cell and select it. model.change( writer => { const widgetElement = writer.createElement( 'widget' ); const tableCell = root.getNodeByPath( [ 0, 0, 0, 0 ] ); writer.insert( widgetElement, writer.createPositionAfter( tableCell ) ); // Update the selection so it will be set on the widget and not in renamed paragraph. writer.setSelection( widgetElement, 'on' ); } ); // View selection should be updated only twice - will be set to null and then to widget. // If called thrice the selection post fixer for table cell was also called. sinon.assert.calledTwice( spy ); } ); // https://github.com/ckeditor/ckeditor5-table/issues/191. it( 'should not fire (and crash) for removed view elements', () => { editor.setData( viewTable( [ [ '

foo

' ] ] ) ); const p = root.getNodeByPath( [ 0, 0, 0, 0 ] ); // Replace table cell contents with paragraph - as model.deleteContent() does. model.change( writer => { writer.setSelection( writer.createRangeIn( root ) ); editor.execute( 'delete' ); // For some reason it didn't crash with `writer.remove()`. writer.setAttribute( 'foo', 'bar', p ); } ); // Trying to map view selection to DOM range shouldn't throw after post-fixer will fix inserted

to . expect( editor.getData() ).to.equal( '' ); } ); } );