/** * @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 { parse, stringify, getData, setData } from '../../src/dev-utils/view'; import ViewDocument from '../../src/view/document'; import DocumentFragment from '../../src/view/documentfragment'; import Position from '../../src/view/position'; import Element from '../../src/view/element'; import AttributeElement from '../../src/view/attributeelement'; import ContainerElement from '../../src/view/containerelement'; import EmptyElement from '../../src/view/emptyelement'; import UIElement from '../../src/view/uielement'; import RawElement from '../../src/view/rawelement'; import Text from '../../src/view/text'; import DocumentSelection from '../../src/view/documentselection'; import Range from '../../src/view/range'; import View from '../../src/view/view'; import XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor'; import createViewRoot from '../view/_utils/createroot'; import { StylesProcessor } from '../../src/view/stylesmap'; describe( 'view test utils', () => { describe( 'getData, setData', () => { afterEach( () => { sinon.restore(); } ); describe( 'getData', () => { it( 'should use stringify method', () => { const element = document.createElement( 'div' ); const stringifySpy = sinon.spy( getData, '_stringify' ); const view = new View( new StylesProcessor() ); const viewDocument = view.document; const options = { showType: false, showPriority: false, withoutSelection: true, renderUIElements: false }; const root = createAttachedRoot( viewDocument, element ); root._appendChild( new Element( viewDocument, 'p' ) ); expect( getData( view, options ) ).to.equal( '
' ); sinon.assert.calledOnce( stringifySpy ); expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root ); expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( null ); const stringifyOptions = stringifySpy.firstCall.args[ 2 ]; expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false ); expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false ); expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true ); expect( stringifyOptions ).to.have.property( 'renderUIElements' ).that.equals( false ); view.destroy(); } ); it( 'should use stringify method with selection', () => { const element = document.createElement( 'div' ); const stringifySpy = sinon.spy( getData, '_stringify' ); const view = new View( new StylesProcessor() ); const viewDocument = view.document; const options = { showType: false, showPriority: false }; const root = createAttachedRoot( viewDocument, element ); root._appendChild( new Element( viewDocument, 'p' ) ); view.change( writer => { writer.setSelection( Range._createFromParentsAndOffsets( root, 0, root, 1 ) ); } ); expect( getData( view, options ) ).to.equal( '[]' ); sinon.assert.calledOnce( stringifySpy ); expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root ); expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( viewDocument.selection ); const stringifyOptions = stringifySpy.firstCall.args[ 2 ]; expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false ); expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false ); expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true ); view.destroy(); } ); it( 'should throw an error when passing invalid document', () => { expect( () => { getData( { invalid: 'view' } ); } ).to.throw( TypeError, 'View needs to be an instance of module:engine/view/view~View.' ); } ); } ); describe( 'setData', () => { it( 'should use parse method', () => { const view = new View( new StylesProcessor() ); const viewDocument = view.document; const data = 'foobarbaz'; const parseSpy = sinon.spy( setData, '_parse' ); createAttachedRoot( viewDocument, document.createElement( 'div' ) ); setData( view, data ); expect( getData( view ) ).to.equal( 'foobarbaz' ); sinon.assert.calledOnce( parseSpy ); const args = parseSpy.firstCall.args; expect( args[ 0 ] ).to.equal( data ); expect( args[ 1 ] ).to.be.an( 'object' ); expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() ); view.destroy(); } ); it( 'should use parse method with selection', () => { const view = new View( new StylesProcessor() ); const viewDocument = view.document; const data = '[baz]'; const parseSpy = sinon.spy( setData, '_parse' ); createAttachedRoot( viewDocument, document.createElement( 'div' ) ); setData( view, data ); expect( getData( view ) ).to.equal( '[baz]' ); const args = parseSpy.firstCall.args; expect( args[ 0 ] ).to.equal( data ); expect( args[ 1 ] ).to.be.an( 'object' ); expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() ); view.destroy(); } ); it( 'should throw an error when passing invalid document', () => { expect( () => { setData( { invalid: 'view' } ); } ).to.throw( TypeError, 'View needs to be an instance of module:engine/view/view~View.' ); } ); } ); } ); describe( 'stringify', () => { let viewDocument; beforeEach( () => { viewDocument = new ViewDocument( new StylesProcessor() ); } ); it( 'should write text', () => { const text = new Text( viewDocument, 'foobar' ); expect( stringify( text ) ).to.equal( 'foobar' ); } ); it( 'should write elements and texts', () => { const text = new Text( viewDocument, 'foobar' ); const b = new Element( viewDocument, 'b', null, text ); const p = new Element( viewDocument, 'p', null, b ); expect( stringify( p ) ).to.equal( 'foobar
' ); } ); it( 'should write elements with attributes (attributes in alphabetical order)', () => { const text = new Text( viewDocument, 'foobar' ); const b = new Element( viewDocument, 'b', { foo: 'bar' }, text ); const p = new Element( viewDocument, 'p', { baz: 'qux', bar: 'taz', class: 'short wide' }, b ); expect( stringify( p ) ).to.equal( 'foobar
' ); } ); it( 'should write selection ranges inside elements', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b1 = new Element( viewDocument, 'b', null, text1 ); const b2 = new Element( viewDocument, 'b', null, text2 ); const p = new Element( viewDocument, 'p', null, [ b1, b2 ] ); const range = Range._createFromParentsAndOffsets( p, 1, p, 2 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( 'foobar[bazqux]
' ); } ); it( 'should support unicode', () => { const text = new Text( viewDocument, 'நிலைக்கு' ); const b = new Element( viewDocument, 'b', null, text ); const p = new Element( viewDocument, 'p', null, b ); const range = Range._createFromParentsAndOffsets( p, 0, text, 4 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '[நிலை}க்கு
' ); } ); it( 'should write collapsed selection ranges inside elements', () => { const text = new Text( viewDocument, 'foobar' ); const p = new Element( viewDocument, 'p', null, text ); const range = Range._createFromParentsAndOffsets( p, 0, p, 0 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '[]foobar
' ); } ); it( 'should write selection ranges inside text', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b1 = new Element( viewDocument, 'b', null, text1 ); const b2 = new Element( viewDocument, 'b', null, text2 ); const p = new Element( viewDocument, 'p', null, [ b1, b2 ] ); const range = Range._createFromParentsAndOffsets( text1, 1, text1, 5 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( 'f{ooba}rbazqux
' ); } ); it( 'should write selection ranges inside text represented by `[` and `]` characters', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b1 = new Element( viewDocument, 'b', null, text1 ); const b2 = new Element( viewDocument, 'b', null, text2 ); const p = new Element( viewDocument, 'p', null, [ b1, b2 ] ); const range = Range._createFromParentsAndOffsets( text1, 1, text1, 5 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection, { sameSelectionCharacters: true } ) ) .to.equal( 'f[ooba]rbazqux
' ); } ); it( 'should write collapsed selection ranges inside texts', () => { const text = new Text( viewDocument, 'foobar' ); const p = new Element( viewDocument, 'p', null, text ); const range = Range._createFromParentsAndOffsets( text, 0, text, 0 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '{}foobar
' ); } ); it( 'should write ranges that start inside text end ends between elements', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b1 = new Element( viewDocument, 'b', null, text1 ); const b2 = new Element( viewDocument, 'b', null, text2 ); const p = new Element( viewDocument, 'p', null, [ b1, b2 ] ); const range = Range._createFromParentsAndOffsets( p, 0, text2, 5 ); const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '[foobarbazqu}x
' ); } ); it( 'should write elements types as namespaces when needed', () => { const text = new Text( viewDocument, 'foobar' ); const b = new AttributeElement( viewDocument, 'b', null, text ); const p = new ContainerElement( viewDocument, 'p', null, b ); expect( stringify( p, null, { showType: true } ) ) .to.equal( 'foobar
' ); } ); it( 'should write elements id when needed', () => { const text = new Text( viewDocument, 'foobar' ); const span = new AttributeElement( viewDocument, 'span', null, text ); span._id = 'foo'; const p = new ContainerElement( viewDocument, 'p', null, span ); expect( stringify( p, null, { showAttributeElementId: true } ) ) .to.equal( 'foobar
' ); } ); it( 'should parse DocumentFragment as root', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b1 = new Element( viewDocument, 'b', null, text1 ); const b2 = new Element( viewDocument, 'b', null, text2 ); const fragment = new DocumentFragment( viewDocument, [ b1, b2 ] ); expect( stringify( fragment, null ) ).to.equal( 'foobarbazqux' ); } ); it( 'should not write ranges outside elements - end position outside element', () => { const text = new Text( viewDocument, 'foobar' ); const b = new Element( viewDocument, 'b', null, text ); const p = new Element( viewDocument, 'p', null, b ); const range = Range._createFromParentsAndOffsets( p, 0, p, 5 ); expect( stringify( p, range ) ).to.equal( '[foobar
' ); } ); it( 'should not write ranges outside elements - start position outside element', () => { const text = new Text( viewDocument, 'foobar' ); const b = new Element( viewDocument, 'b', null, text ); const p = new Element( viewDocument, 'p', null, b ); const range = Range._createFromParentsAndOffsets( p, -1, p, 1 ); expect( stringify( p, range ) ).to.equal( 'foobar]
' ); } ); it( 'should not write ranges outside elements - end position outside text', () => { const text = new Text( viewDocument, 'foobar' ); const b = new Element( viewDocument, 'b', null, text ); const p = new Element( viewDocument, 'p', null, b ); const range = Range._createFromParentsAndOffsets( text, 0, text, 7 ); expect( stringify( p, range ) ).to.equal( '{foobar
' ); } ); it( 'should not write ranges outside elements - start position outside text', () => { const text = new Text( viewDocument, 'foobar' ); const b = new Element( viewDocument, 'b', null, text ); const p = new Element( viewDocument, 'p', null, b ); const range = Range._createFromParentsAndOffsets( text, -1, text, 2 ); expect( stringify( p, range ) ).to.equal( 'fo}obar
' ); } ); it( 'should write multiple ranges from selection #1', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b1 = new Element( viewDocument, 'b', null, text1 ); const b2 = new Element( viewDocument, 'b', null, text2 ); const p = new Element( viewDocument, 'p', null, [ b1, b2 ] ); const range1 = Range._createFromParentsAndOffsets( p, 0, p, 1 ); const range2 = Range._createFromParentsAndOffsets( p, 1, p, 1 ); const selection = new DocumentSelection( [ range2, range1 ] ); expect( stringify( p, selection ) ).to.equal( '[foobar][]bazqux
' ); } ); it( 'should write multiple ranges from selection #2', () => { const text1 = new Text( viewDocument, 'foobar' ); const text2 = new Text( viewDocument, 'bazqux' ); const b = new Element( viewDocument, 'b', null, text1 ); const p = new Element( viewDocument, 'p', null, [ b, text2 ] ); const range1 = Range._createFromParentsAndOffsets( p, 0, p, 1 ); const range2 = Range._createFromParentsAndOffsets( text2, 0, text2, 3 ); const range3 = Range._createFromParentsAndOffsets( text2, 3, text2, 4 ); const range4 = Range._createFromParentsAndOffsets( p, 1, p, 1 ); const selection = new DocumentSelection( [ range1, range2, range3, range4 ] ); expect( stringify( p, selection ) ).to.equal( '[foobar][]{baz}{q}ux
' ); } ); it( 'should use Position instance instead of Selection', () => { const text = new Text( viewDocument, 'foobar' ); const position = new Position( text, 3 ); const string = stringify( text, position ); expect( string ).to.equal( 'foo{}bar' ); } ); it( 'should use Range instance instead of Selection', () => { const text = new Text( viewDocument, 'foobar' ); const range = Range._createFromParentsAndOffsets( text, 3, text, 4 ); const string = stringify( text, range ); expect( string ).to.equal( 'foo{b}ar' ); } ); it( 'should stringify EmptyElement', () => { const img = new EmptyElement( viewDocument, 'img' ); const p = new ContainerElement( viewDocument, 'p', null, img ); expect( stringify( p, null, { showType: true } ) ) .to.equal( '[foobar][]
' ); expect( view ).to.be.instanceof( Element ); expect( view.childCount ).to.equal( 1 ); const b = view.getChild( 0 ); expect( b ).to.be.instanceof( Element ); expect( b.name ).to.equal( 'b' ); expect( b.childCount ).to.equal( 1 ); const text = b.getChild( 0 ); expect( text ).to.be.instanceof( Text ); expect( text.data ).to.equal( 'foobar' ); expect( selection.rangeCount ).to.equal( 2 ); const ranges = [ ...selection.getRanges() ]; expect( ranges[ 0 ].isEqual( Range._createFromParentsAndOffsets( view, 0, b, 1 ) ) ).to.be.true; expect( ranges[ 1 ].isEqual( Range._createFromParentsAndOffsets( view, 1, view, 1 ) ) ).to.be.true; } ); it( 'should support unicode', () => { const { view, selection } = parse( '[நிலை}க்கு
' ); expect( view ).to.be.instanceof( Element ); expect( view.name ).to.equal( 'p' ); expect( view.childCount ).to.equal( 1 ); const b = view.getChild( 0 ); expect( b.name ).to.equal( 'b' ); expect( b.childCount ).to.equal( 1 ); const text = b.getChild( 0 ); expect( text.data ).to.equal( 'நிலைக்கு' ); expect( selection.rangeCount ).to.equal( 1 ); const range = selection.getFirstRange(); expect( range.start.parent ).to.equal( view ); expect( range.start.offset ).to.equal( 0 ); expect( range.end.parent ).to.equal( text ); expect( range.end.offset ).to.equal( 4 ); } ); it( 'should parse ranges #1', () => { const { view, selection } = parse( 'text
' ); } ); it( 'should use provided root element #2', () => { const root = new Element( viewDocument, 'p' ); const data = parse( 'texttest', { rootElement: root } ); expect( stringify( data ) ).to.equal( 'texttest
' ); } ); it( 'should parse an EmptyElement', () => { const parsed = parse( '