/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import Document from '../../src/model/document'; import DataController from '../../src/controller/datacontroller'; import Selection from '../../src/model/selection'; import modifySelection from '../../src/controller/modifyselection'; import { setData, stringify } from '../../src/dev-utils/model'; describe( 'DataController', () => { let document, dataController; beforeEach( () => { document = new Document(); dataController = new DataController( document ); document.schema.registerItem( 'p', '$block' ); document.schema.registerItem( 'x', '$block' ); document.schema.allow( { name: 'x', inside: 'p' } ); document.createRoot(); } ); describe( 'modifySelection', () => { describe( 'unit=character', () => { describe( 'within element', () => { test( 'does nothing on empty content', '[]', '[]' ); test( 'does nothing on empty content (with empty element)', '

[]

', '

[]

' ); test( 'does nothing on empty content (backward)', '[]', '[]', { direction: 'backward' } ); test( 'does nothing on root boundary', '

foo[]

', '

foo[]

' ); test( 'does nothing on root boundary (backward)', '

[]foo

', '

[]foo

', { direction: 'backward' } ); test( 'extends one character forward', '

f[]oo

', '

f[o]o

' ); it( 'extends one character backward', () => { setData( document, '

fo[]o

', { lastRangeBackward: true } ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

f[o]o

' ); expect( document.selection.isBackward ).to.true; } ); test( 'extends one character forward (non-collapsed)', '

f[o]obar

', '

f[oo]bar

' ); it( 'extends one character backward (non-collapsed)', () => { setData( document, '

foob[a]r

', { lastRangeBackward: true } ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

foo[ba]r

' ); expect( document.selection.isBackward ).to.true; } ); test( 'extends to element boundary', '

fo[]o

', '

fo[o]

' ); it( 'extends to element boundary (backward)', () => { setData( document, '

f[]oo

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

[f]oo

' ); expect( document.selection.isBackward ).to.true; } ); test( 'shrinks forward selection (to collapsed)', '

foo[b]ar

', '

foo[]bar

', { direction: 'backward' } ); it( 'shrinks backward selection (to collapsed)', () => { setData( document, '

foo[b]ar

', { lastRangeBackward: true } ); modifySelection( dataController, document.selection ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

foob[]ar

' ); expect( document.selection.isBackward ).to.false; } ); test( 'unicode support - combining mark forward', '

foo[]b̂ar

', '

foo[b̂]ar

' ); it( 'unicode support - combining mark backward', () => { setData( document, '

foob̂[]ar

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

foo[b̂]ar

' ); expect( document.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark multiple', '

fo[]o̻̐ͩbar

', '

fo[o̻̐ͩ]bar

' ); it( 'unicode support - combining mark multiple backward', () => { setData( document, '

foo̻̐ͩ[]bar

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

fo[o̻̐ͩ]bar

' ); expect( document.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark to the end', '

fo[]o̻̐ͩ

', '

fo[o̻̐ͩ]

' ); test( 'unicode support - surrogate pairs forward', '

[]\uD83D\uDCA9

', '

[\uD83D\uDCA9]

' ); it( 'unicode support - surrogate pairs backward', () => { setData( document, '

\uD83D\uDCA9[]

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

[\uD83D\uDCA9]

' ); expect( document.selection.isBackward ).to.true; } ); } ); describe( 'beyond element', () => { test( 'extends over boundary of empty elements', '

[]

', '

[

]

' ); it( 'extends over boundary of empty elements (backward)', () => { setData( document, '

[]

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

[

]

' ); expect( document.selection.isBackward ).to.true; } ); test( 'extends over boundary of non-empty elements', '

a[]

bcd

', '

a[

]bcd

' ); it( 'extends over boundary of non-empty elements (backward)', () => { setData( document, '

a

[]bcd

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

a[

]bcd

' ); expect( document.selection.isBackward ).to.true; } ); test( 'extends over character after boundary', '

a[

]bcd

', '

a[

b]cd

' ); it( 'extends over character after boundary (backward)', () => { setData( document, '

abc[

]d

', { lastRangeBackward: true } ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

ab[c

]d

' ); expect( document.selection.isBackward ).to.true; } ); test( 'stops on the first position where text is allowed - inside block', '

a[]

bcd

', '

a[

]bcd

' ); test( 'stops on the first position where text is allowed - inside inline element', '

a[

]bcdef

', '

a[

]bcdef

' ); test( 'extends over element when next node is a text', '

a[]bc

', '

a[]bc

' ); test( 'extends over element when next node is a text - backward', '

ab[]c

', '

ab[]c

', { direction: 'backward' } ); it( 'shrinks over boundary of empty elements', () => { setData( document, '

[

]

', { lastRangeBackward: true } ); modifySelection( dataController, document.selection ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

[]

' ); expect( document.selection.isBackward ).to.false; } ); it( 'shrinks over boundary of empty elements (backward)', () => { setData( document, '

[

]

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

[]

' ); expect( document.selection.isBackward ).to.false; } ); it( 'shrinks over boundary of non-empty elements', () => { setData( document, '

a[

]b

', { lastRangeBackward: true } ); modifySelection( dataController, document.selection ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

a

[]b

' ); expect( document.selection.isBackward ).to.false; } ); test( 'shrinks over boundary of non-empty elements (backward)', '

a[

]b

', '

a[]

b

', { direction: 'backward' } ); it( 'updates selection attributes', () => { setData( document, '

<$text bold="true">foo[b]

' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

<$text bold="true">foo[]b

' ); expect( document.selection.getAttribute( 'bold' ) ).to.equal( true ); } ); } ); describe( 'beyond element – skipping incorrect positions', () => { beforeEach( () => { document.schema.registerItem( 'quote' ); document.schema.allow( { name: 'quote', inside: '$root' } ); document.schema.allow( { name: '$block', inside: 'quote' } ); } ); test( 'skips position at the beginning of an element which does not allow text', '

x[]

y

z

', '

x[

]y

z

' ); test( 'skips position at the end of an element which does not allow text - backward', '

x

y

[]z

', '

x

y[

]z

', { direction: 'backward' } ); test( 'skips position at the end of an element which does not allow text', '

x[

y]

z

', '

x[

y

]z

' ); test( 'skips position at the beginning of an element which does not allow text - backward', '

x

[]y

z

', '

x[

]y

z

', { direction: 'backward' } ); test( 'extends to an empty block after skipping incorrect position', '

x[]

z

', '

x[

]

z

' ); test( 'extends to an empty block after skipping incorrect position - backward', '

x

[]z

', '

x

[

]z

', { direction: 'backward' } ); } ); } ); describe( 'unit=codePoint', () => { it( 'does nothing on empty content', () => { document.schema.allow( { name: '$text', inside: '$root' } ); setData( document, '' ); modifySelection( dataController, document.selection, { unit: 'codePoint' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '[]' ); } ); test( 'does nothing on empty content (with empty element)', '

[]

', '

[]

', { unit: 'codePoint' } ); test( 'extends one user-perceived character forward - latin letters', '

f[]oo

', '

f[o]o

', { unit: 'codePoint' } ); it( 'extends one user-perceived character backward - latin letters', () => { setData( document, '

fo[]o

' ); modifySelection( dataController, document.selection, { unit: 'codePoint', direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

f[o]o

' ); expect( document.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark forward', '

foo[]b̂ar

', '

foo[b]̂ar

', { unit: 'codePoint' } ); it( 'unicode support - combining mark backward', () => { setData( document, '

foob̂[]ar

' ); // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection. // Document's selection will throw errors in some test cases (which are correct cases, but only for // non-document selections). const testSelection = Selection.createFromSelection( document.selection ); modifySelection( dataController, testSelection, { unit: 'codePoint', direction: 'backward' } ); expect( stringify( document.getRoot(), testSelection ) ).to.equal( '

foob[̂]ar

' ); expect( testSelection.isBackward ).to.true; } ); test( 'unicode support - combining mark multiple', '

fo[]o̻̐ͩbar

', '

fo[o]̻̐ͩbar

', { unit: 'codePoint' } ); test( 'unicode support - surrogate pairs forward', '

[]\uD83D\uDCA9

', '

[\uD83D\uDCA9]

', { unit: 'codePoint' } ); it( 'unicode support surrogate pairs backward', () => { setData( document, '

\uD83D\uDCA9[]

' ); modifySelection( dataController, document.selection, { unit: 'codePoint', direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '

[\uD83D\uDCA9]

' ); expect( document.selection.isBackward ).to.true; } ); } ); describe( 'objects handling', () => { beforeEach( () => { document.schema.registerItem( 'obj' ); document.schema.allow( { name: 'obj', inside: '$root' } ); document.schema.allow( { name: '$text', inside: 'obj' } ); document.schema.objects.add( 'obj' ); document.schema.registerItem( 'inlineObj', '$inline' ); document.schema.allow( { name: '$text', inside: 'inlineObj' } ); document.schema.objects.add( 'inlineObj' ); } ); test( 'extends over next object element when at the end of an element', '

foo[]

bar', '

foo[

bar]' ); test( 'extends over previous object element when at the beginning of an element ', 'bar

[]foo

', '[bar

]foo

', { direction: 'backward' } ); test( 'extends over object elements - forward', '[]', '[]' ); it( 'extends over object elements - backward', () => { setData( document, '[]', { lastRangeBackward: true } ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '[]' ); expect( document.selection.isBackward ).to.true; } ); test( 'extends over inline objects - forward', '

foo[]bar

', '

foo[bar]

' ); test( 'extends over inline objects - backward', '

bar[]foo

', '

[bar]foo

', { direction: 'backward' } ); test( 'extends over empty inline objects - forward', '

foo[]

', '

foo[]

' ); test( 'extends over empty inline objects - backward', '

[]foo

', '

[]foo

', { direction: 'backward' } ); } ); describe( 'limits handling', () => { beforeEach( () => { document.schema.registerItem( 'inlineLimit' ); document.schema.allow( { name: 'inlineLimit', inside: '$block' } ); document.schema.allow( { name: '$text', inside: 'inlineLimit' } ); document.schema.registerItem( 'blockLimit' ); document.schema.allow( { name: 'blockLimit', inside: '$root' } ); document.schema.allow( { name: 'p', inside: 'blockLimit' } ); document.schema.limits.add( 'inlineLimit' ); document.schema.limits.add( 'blockLimit' ); } ); test( 'should not extend to outside of inline limit element', '

xfoo[]x

', '

xfoo[]x

' ); test( 'should not extend to outside of inline limit element - backward', '

x[]foox

', '

x[]foox

', { direction: 'backward' } ); test( 'should not extend to outside of block limit element', '

x

foo[]

x

', '

x

foo[]

x

' ); test( 'should not extend to outside of block limit element - backward', '

x

[]foo

x

', '

x

[]foo

x

', { direction: 'backward' } ); // This may seem counterintuitive but it makes sense. The limit element means // that it can't be left or modified from inside. If you want the same behavior from outside // register it as an object. test( 'should enter a limit element', '

foo[]

x

', '

foo[

]x

' ); test( 'should enter a limit element - backward', '

x

[]foo

', '

x[

]foo

', { direction: 'backward' } ); } ); } ); function test( title, input, output, options ) { it( title, () => { input = input.normalize(); output = output.normalize(); setData( document, input ); // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection. // Document's selection will throw errors in some test cases (which are correct cases, but only for // non-document selections). const testSelection = Selection.createFromSelection( document.selection ); modifySelection( dataController, testSelection, options ); expect( stringify( document.getRoot(), testSelection ) ).to.equal( output ); } ); } } );