/** * @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[]
a[
]
a[
]
a[
ab
ab[
[
]
', { 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$text>[b]
' ); modifySelection( dataController, document.selection, { direction: 'backward' } ); expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<$text bold="true">foo[]$text>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[]
foo[
[]foo
', '[]foo
', { direction: 'backward' } ); test( 'extends over object elements - forward', '[foo[]
foo[
[
foo[]
foo[
[
x
x
x
x
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
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 ); } ); } } );