model.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. import { stringify, parse, getData, setData } from '../../src/dev-utils/model';
  6. import Model from '../../src/model/model';
  7. import DocumentFragment from '../../src/model/documentfragment';
  8. import Element from '../../src/model/element';
  9. import Text from '../../src/model/text';
  10. import Range from '../../src/model/range';
  11. import Position from '../../src/model/position';
  12. import count from '@ckeditor/ckeditor5-utils/src/count';
  13. describe( 'model test utils', () => {
  14. let model, document, root, selection;
  15. beforeEach( () => {
  16. model = new Model();
  17. document = model.document;
  18. root = document.createRoot();
  19. selection = document.selection;
  20. model.change( writer => {
  21. writer.setSelection( null );
  22. } );
  23. model.schema.register( 'a', {
  24. allowWhere: '$text',
  25. allowIn: '$root',
  26. allowAttributes: [ 'bar', 'car', 'foo' ]
  27. } );
  28. model.schema.register( 'b', {
  29. allowWhere: '$text',
  30. allowIn: '$root',
  31. allowAttributes: [ 'barFoo', 'fooBar', 'x' ]
  32. } );
  33. model.schema.register( 'c', {
  34. allowWhere: '$text',
  35. allowIn: [ '$root', 'b' ]
  36. } );
  37. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  38. model.schema.extend( '$text', {
  39. allowIn: [ '$root', 'a', 'b' ]
  40. } );
  41. } );
  42. afterEach( () => {
  43. sinon.restore();
  44. } );
  45. describe( 'getData', () => {
  46. it( 'should use stringify method', () => {
  47. const stringifySpy = sinon.spy( getData, '_stringify' );
  48. root._appendChild( new Element( 'b', null, new Text( 'btext' ) ) );
  49. expect( getData( model, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
  50. sinon.assert.calledOnce( stringifySpy );
  51. sinon.assert.calledWithExactly( stringifySpy, root, null, null );
  52. } );
  53. it( 'should use stringify method with selection', () => {
  54. const stringifySpy = sinon.spy( getData, '_stringify' );
  55. root._appendChild( new Element( 'b', null, new Text( 'btext' ) ) );
  56. model.change( writer => {
  57. writer.setSelection( new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) ) );
  58. } );
  59. expect( getData( model ) ).to.equal( '[<b>btext</b>]' );
  60. sinon.assert.calledOnce( stringifySpy );
  61. sinon.assert.calledWithExactly( stringifySpy, root, document.selection, null );
  62. } );
  63. it( 'should throw an error when passing invalid document', () => {
  64. expect( () => {
  65. getData( { invalid: 'document' } );
  66. } ).to.throw( TypeError, 'Model needs to be an instance of module:engine/model/model~Model.' );
  67. } );
  68. describe( 'markers', () => {
  69. it( 'should stringify collapsed marker', () => {
  70. setData( model, '<paragraph>bar</paragraph>' );
  71. model.markers._set( 'foo', new Range( Position._createAt( document.getRoot(), 0 ) ) );
  72. expect( getData( model, { convertMarkers: true, withoutSelection: true } ) )
  73. .to.equal( '<foo:start></foo:start><paragraph>bar</paragraph>' );
  74. } );
  75. it( 'should stringify non-collapsed marker', () => {
  76. setData( model, '<paragraph>bar</paragraph>' );
  77. const markerRange = new Range( Position._createAt( document.getRoot(), 0 ), Position._createAt( document.getRoot(), 1 ) );
  78. model.markers._set( 'foo', markerRange );
  79. expect( getData( model, { convertMarkers: true, withoutSelection: true } ) )
  80. .to.equal( '<foo:start></foo:start><paragraph>bar</paragraph><foo:end></foo:end>' );
  81. } );
  82. } );
  83. } );
  84. describe( 'setData', () => {
  85. it( 'should use parse method', () => {
  86. const parseSpy = sinon.spy( setData, '_parse' );
  87. const options = {};
  88. const data = '<b>btext</b>text';
  89. setData( model, data, options );
  90. expect( getData( model, { withoutSelection: true } ) ).to.equal( data );
  91. sinon.assert.calledOnce( parseSpy );
  92. const args = parseSpy.firstCall.args;
  93. expect( args[ 0 ] ).to.equal( data );
  94. } );
  95. it( 'should use parse method with selection', () => {
  96. const parseSpy = sinon.spy( setData, '_parse' );
  97. const options = {};
  98. const data = '[<b>btext</b>]';
  99. setData( model, data, options );
  100. expect( getData( model ) ).to.equal( data );
  101. sinon.assert.calledOnce( parseSpy );
  102. const args = parseSpy.firstCall.args;
  103. expect( args[ 0 ] ).to.equal( data );
  104. } );
  105. it( 'should insert text', () => {
  106. testUtils( 'this is test text', '[]this is test text' );
  107. } );
  108. it( 'should insert text with selection around', () => {
  109. testUtils( '[this is test text]' );
  110. } );
  111. it( 'should insert text with selection inside #1', () => {
  112. testUtils( 'this [is test] text' );
  113. } );
  114. it( 'should insert text with selection inside #2', () => {
  115. testUtils( '[this is test] text' );
  116. } );
  117. it( 'should insert text with selection inside #3', () => {
  118. testUtils( 'this is [test text]' );
  119. } );
  120. it( 'should insert unicode text with selection', () => {
  121. testUtils( 'நி[லைக்]கு' );
  122. } );
  123. it( 'should insert element', () => {
  124. testUtils( '<b>foo bar</b>', '[]<b>foo bar</b>' );
  125. } );
  126. it( 'should insert element with selection inside #1', () => {
  127. testUtils( '<b>[foo ]bar</b>' );
  128. } );
  129. it( 'should insert element with selection inside #2', () => {
  130. testUtils( '[<b>foo ]bar</b>' );
  131. } );
  132. it( 'should insert element with selection inside #3', () => {
  133. testUtils( '<b>[foo bar</b>]' );
  134. } );
  135. it( 'should insert backward selection', () => {
  136. setData( model, '<b>[foo bar</b>]', { lastRangeBackward: true } );
  137. expect( getData( model ) ).to.equal( '<b>[foo bar</b>]' );
  138. expect( document.selection.isBackward ).to.true;
  139. } );
  140. it( 'should throw an error when passing invalid document', () => {
  141. expect( () => {
  142. setData( { invalid: 'document' } );
  143. } ).to.throw( TypeError, 'Model needs to be an instance of module:engine/model/model~Model.' );
  144. } );
  145. it( 'should set attributes to the selection', () => {
  146. setData( model, '<b>[foo bar]</b>', { selectionAttributes: { foo: 'bar' } } );
  147. expect( document.selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
  148. } );
  149. // #815.
  150. it( 'should work in a special root', () => {
  151. const model = new Model();
  152. model.schema.register( 'textOnly' );
  153. model.schema.extend( '$text', { allowIn: 'textOnly' } );
  154. model.document.createRoot( 'textOnly', 'textOnly' );
  155. setData( model, 'a[b]c', { rootName: 'textOnly' } );
  156. expect( getData( model, { rootName: 'textOnly' } ) ).to.equal( 'a[b]c' );
  157. } );
  158. function testUtils( data, expected ) {
  159. expected = expected || data;
  160. setData( model, data );
  161. expect( getData( model ) ).to.equal( expected );
  162. }
  163. } );
  164. describe( 'stringify', () => {
  165. it( 'should stringify text', () => {
  166. const text = new Text( 'text', { underline: true, bold: true } );
  167. expect( stringify( text ) ).to.equal( '<$text bold="true" underline="true">text</$text>' );
  168. } );
  169. it( 'should stringify element', () => {
  170. const element = new Element( 'a', null, [
  171. new Element( 'b', null, new Text( 'btext' ) ),
  172. new Text( 'atext' )
  173. ] );
  174. expect( stringify( element ) ).to.equal( '<a><b>btext</b>atext</a>' );
  175. } );
  176. it( 'should stringify document fragment', () => {
  177. const fragment = new DocumentFragment( [
  178. new Element( 'b', null, new Text( 'btext' ) ),
  179. new Text( 'atext' )
  180. ] );
  181. expect( stringify( fragment ) ).to.equal( '<b>btext</b>atext' );
  182. } );
  183. it( 'writes elements and texts', () => {
  184. root._appendChild( [
  185. new Element( 'a', null, new Text( 'atext' ) ),
  186. new Element( 'b', null, [
  187. new Element( 'c1' ),
  188. new Text( 'ctext' ),
  189. new Element( 'c2' )
  190. ] ),
  191. new Element( 'd' )
  192. ] );
  193. expect( stringify( root ) ).to.equal(
  194. '<a>atext</a><b><c1></c1>ctext<c2></c2></b><d></d>'
  195. );
  196. } );
  197. it( 'writes element attributes', () => {
  198. root._appendChild(
  199. new Element( 'a', { foo: true, bar: 1, car: false }, [
  200. new Element( 'b', { fooBar: 'x y', barFoo: { x: 1, y: 2 } } )
  201. ] )
  202. );
  203. // Note: attributes are written in a very simplistic way, because they are not to be parsed. They are just
  204. // to be compared in the tests with some patterns.
  205. expect( stringify( root ) ).to.equal(
  206. '<a bar="1" car="false" foo="true"><b barFoo="{"x":1,"y":2}" fooBar="x y"></b></a>'
  207. );
  208. } );
  209. it( 'writes text attributes', () => {
  210. root._appendChild( [
  211. new Text( 'foo', { bold: true } ),
  212. new Text( 'bar' ),
  213. new Text( 'bom', { bold: true, italic: true } ),
  214. new Element( 'a', null, [
  215. new Text( 'pom', { underline: true, bold: true } )
  216. ] )
  217. ] );
  218. expect( stringify( root ) ).to.equal(
  219. '<$text bold="true">foo</$text>bar<$text bold="true" italic="true">bom</$text>' +
  220. '<a><$text bold="true" underline="true">pom</$text></a>'
  221. );
  222. } );
  223. it( 'writes unicode text', () => {
  224. root._appendChild( new Text( 'நிலைக்கு' ) );
  225. expect( stringify( root ) ).to.equal( 'நிலைக்கு' );
  226. } );
  227. describe( 'selection', () => {
  228. let elA, elB;
  229. beforeEach( () => {
  230. elA = new Element( 'a' );
  231. elB = new Element( 'b' );
  232. root._appendChild( [
  233. elA,
  234. new Text( 'foo' ),
  235. new Text( 'bar', { bold: true } ),
  236. elB
  237. ] );
  238. } );
  239. it( 'writes selection in an empty root', () => {
  240. const root = document.createRoot( '$root', 'empty' );
  241. model.change( writer => {
  242. writer.setSelection( root, 0 );
  243. } );
  244. expect( stringify( root, selection ) ).to.equal(
  245. '[]'
  246. );
  247. } );
  248. it( 'writes selection collapsed in an element', () => {
  249. model.change( writer => {
  250. writer.setSelection( root, 0 );
  251. } );
  252. expect( stringify( root, selection ) ).to.equal(
  253. '[]<a></a>foo<$text bold="true">bar</$text><b></b>'
  254. );
  255. } );
  256. it( 'writes selection collapsed in a text', () => {
  257. model.change( writer => {
  258. writer.setSelection( root, 3 );
  259. } );
  260. expect( stringify( root, selection ) ).to.equal(
  261. '<a></a>fo[]o<$text bold="true">bar</$text><b></b>'
  262. );
  263. } );
  264. it( 'writes selection collapsed at the text left boundary', () => {
  265. model.change( writer => {
  266. writer.setSelection( elA, 'after' );
  267. } );
  268. expect( stringify( root, selection ) ).to.equal(
  269. '<a></a>[]foo<$text bold="true">bar</$text><b></b>'
  270. );
  271. } );
  272. it( 'writes selection collapsed at the text right boundary', () => {
  273. model.change( writer => {
  274. writer.setSelection( elB, 'before' );
  275. } );
  276. expect( stringify( root, selection ) ).to.equal(
  277. '<a></a>foo<$text bold="true">bar[]</$text><b></b>'
  278. );
  279. } );
  280. it( 'writes selection collapsed at the end of the root', () => {
  281. model.change( writer => {
  282. writer.setSelection( root, 'end' );
  283. // Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
  284. writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
  285. } );
  286. expect( stringify( root, selection ) ).to.equal(
  287. '<a></a>foo<$text bold="true">bar</$text><b></b>[]'
  288. );
  289. } );
  290. it( 'writes selection collapsed selection in a text with attributes', () => {
  291. model.change( writer => {
  292. writer.setSelection( root, 5 );
  293. } );
  294. expect( stringify( root, selection ) ).to.equal(
  295. '<a></a>foo<$text bold="true">b[]ar</$text><b></b>'
  296. );
  297. } );
  298. it( 'writes flat selection containing couple of nodes', () => {
  299. model.change( writer => {
  300. writer.setSelection( new Range( Position._createAt( root, 0 ), Position._createAt( root, 4 ) ) );
  301. } );
  302. expect( stringify( root, selection ) ).to.equal(
  303. '[<a></a>foo]<$text bold="true">bar</$text><b></b>'
  304. );
  305. } );
  306. it( 'writes flat selection within text', () => {
  307. model.change( writer => {
  308. writer.setSelection( new Range( Position._createAt( root, 2 ), Position._createAt( root, 3 ) ) );
  309. } );
  310. expect( stringify( root, selection ) ).to.equal(
  311. '<a></a>f[o]o<$text bold="true">bar</$text><b></b>'
  312. );
  313. } );
  314. it( 'writes multi-level selection', () => {
  315. model.change( writer => {
  316. writer.setSelection( new Range( Position._createAt( elA, 0 ), Position._createAt( elB, 0 ) ) );
  317. } );
  318. expect( stringify( root, selection ) ).to.equal(
  319. '<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
  320. );
  321. } );
  322. it( 'writes selection when is backward', () => {
  323. model.change( writer => {
  324. writer.setSelection( new Range( Position._createAt( elA, 0 ), Position._createAt( elB, 0 ) ), { backward: true } );
  325. } );
  326. expect( stringify( root, selection ) ).to.equal(
  327. '<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
  328. );
  329. } );
  330. it( 'writes selection in unicode text', () => {
  331. const root = document.createRoot( '$root', 'empty' );
  332. root._appendChild( new Text( 'நிலைக்கு' ) );
  333. model.change( writer => {
  334. writer.setSelection( new Range( Position._createAt( root, 2 ), Position._createAt( root, 6 ) ) );
  335. } );
  336. expect( stringify( root, selection ) ).to.equal( 'நி[லைக்]கு' );
  337. } );
  338. it( 'uses range and coverts it to selection', () => {
  339. const range = new Range( Position._createAt( elA, 0 ), Position._createAt( elB, 0 ) );
  340. expect( stringify( root, range ) ).to.equal(
  341. '<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
  342. );
  343. } );
  344. it( 'uses position and converts it to collapsed selection', () => {
  345. const position = new Position( root, [ 0 ] );
  346. expect( stringify( root, position ) ).to.equal(
  347. '[]<a></a>foo<$text bold="true">bar</$text><b></b>'
  348. );
  349. } );
  350. } );
  351. } );
  352. describe( 'parse', () => {
  353. test( 'creates empty DocumentFragment from empty string', {
  354. data: '',
  355. check( fragment ) {
  356. expect( fragment ).to.be.instanceOf( DocumentFragment );
  357. }
  358. } );
  359. test( 'creates empty DocumentFragment with selection', {
  360. data: '[]'
  361. } );
  362. test( 'returns Element if range is around single element', {
  363. data: '[<a></a>]',
  364. check( el, selection ) {
  365. const fragment = el.parent;
  366. expect( el ).to.be.instanceOf( Element );
  367. expect( fragment ).to.be.instanceOf( DocumentFragment );
  368. expect( selection.rangeCount ).to.equal( 1 );
  369. const range = new Range( Position._createAt( fragment, 0 ), Position._createAt( fragment, 1 ) );
  370. expect( selection.getFirstRange().isEqual( range ) ).to.be.true;
  371. }
  372. } );
  373. test( 'returns DocumentFragment when multiple elements on root', {
  374. data: '<a></a><b></b>',
  375. check( fragment ) {
  376. expect( fragment ).to.be.instanceOf( DocumentFragment );
  377. expect( fragment.childCount ).to.equal( 2 );
  378. }
  379. } );
  380. test( 'creates elements', {
  381. data: '<a></a><b><c></c></b>'
  382. } );
  383. test( 'creates text nodes', {
  384. data: 'foo<a>bar</a>bom'
  385. } );
  386. test( 'creates text nodes with unicode text', {
  387. data: 'நிலைக்கு'
  388. } );
  389. test( 'sets elements attributes', {
  390. data: '<a bar="true" car="x y" foo="1"></a><b x="y"></b>',
  391. output: '<a bar="true" car="x y" foo="1"></a><b x="y"></b>',
  392. check( root ) {
  393. expect( root.getChild( 0 ).getAttribute( 'bar' ) ).to.equal( true );
  394. expect( root.getChild( 0 ).getAttribute( 'car' ) ).to.equal( 'x y' );
  395. expect( root.getChild( 0 ).getAttribute( 'foo' ) ).to.equal( 1 );
  396. expect( root.getChild( 1 ).getAttribute( 'x' ) ).to.equal( 'y' );
  397. }
  398. } );
  399. test( 'sets text attributes', {
  400. data: '<$text bar="true" car="x y" foo="1">foo</$text><$text x="y">bar</$text>bom',
  401. check( root ) {
  402. expect( root.childCount ).to.equal( 3 );
  403. expect( root.maxOffset ).to.equal( 9 );
  404. expect( root.getChild( 0 ).getAttribute( 'bar' ) ).to.equal( true );
  405. expect( root.getChild( 0 ).getAttribute( 'car' ) ).to.equal( 'x y' );
  406. expect( root.getChild( 0 ).getAttribute( 'foo' ) ).to.equal( 1 );
  407. expect( root.getChild( 1 ).getAttribute( 'x' ) ).to.equal( 'y' );
  408. expect( count( root.getChild( 2 ).getAttributes() ) ).to.equal( 0 );
  409. }
  410. } );
  411. test( 'creates element with complex attributes', {
  412. data: '<a foo=\'{"x":1,"y":2}\'></a>',
  413. output: '<a foo="{"x":1,"y":2}"></a>',
  414. check( a ) {
  415. expect( count( a.getAttributes() ) ).to.equal( 1 );
  416. expect( a.getAttribute( 'foo' ) ).to.have.property( 'x', 1 );
  417. expect( a.getAttribute( 'foo' ) ).to.have.property( 'y', 2 );
  418. }
  419. } );
  420. test( 'returns single parsed element', {
  421. data: '<paragraph></paragraph>',
  422. check( p ) {
  423. expect( p instanceof Element ).to.be.true;
  424. }
  425. } );
  426. test( 'returns DocumentFragment for multiple parsed elements', {
  427. data: '<paragraph></paragraph><paragraph></paragraph>',
  428. check( fragment ) {
  429. expect( fragment instanceof DocumentFragment ).to.be.true;
  430. }
  431. } );
  432. it( 'throws when invalid XML', () => {
  433. expect( () => {
  434. parse( '<a><b></a></b>', model.schema );
  435. } ).to.throw( Error, /Parse error/ );
  436. } );
  437. it( 'throws when try to set element not registered in schema', () => {
  438. expect( () => {
  439. parse( '<xyz></xyz>', model.schema );
  440. } ).to.throw( Error, 'Element \'xyz\' was not allowed in given position.' );
  441. } );
  442. it( 'throws when try to set text directly to $root without registering it', () => {
  443. const model = new Model();
  444. expect( () => {
  445. parse( 'text', model.schema );
  446. } ).to.throw( Error, 'Text was not allowed in given position.' );
  447. } );
  448. it( 'converts data in the specified context', () => {
  449. const model = new Model();
  450. model.schema.register( 'foo' );
  451. model.schema.extend( '$text', { allowIn: 'foo' } );
  452. expect( () => {
  453. parse( 'text', model.schema, { context: [ 'foo' ] } );
  454. } ).to.not.throw();
  455. } );
  456. describe( 'selection', () => {
  457. test( 'sets collapsed selection in an element', {
  458. data: '<a>[]</a>',
  459. check( root, selection ) {
  460. expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
  461. }
  462. } );
  463. test( 'sets collapsed selection between elements', {
  464. data: '<a></a>[]<b></b>'
  465. } );
  466. test( 'sets collapsed selection before a text', {
  467. data: '<a></a>[]foo'
  468. } );
  469. test( 'sets collapsed selection after a text', {
  470. data: 'foo[]'
  471. } );
  472. test( 'sets collapsed selection within a text', {
  473. data: 'foo[]bar',
  474. check( text, selection ) {
  475. expect( text.offsetSize ).to.equal( 6 );
  476. expect( text.getPath() ).to.deep.equal( [ 0 ] );
  477. expect( selection.getFirstRange().start.path ).to.deep.equal( [ 3 ] );
  478. expect( selection.getFirstRange().end.path ).to.deep.equal( [ 3 ] );
  479. }
  480. } );
  481. it( 'sets selection attributes', () => {
  482. const result = parse( 'foo[]bar', model.schema, {
  483. selectionAttributes: {
  484. bold: true,
  485. italic: true
  486. }
  487. } );
  488. expect( stringify( result.model, result.selection ) ).to.equal( 'foo<$text bold="true" italic="true">[]</$text>bar' );
  489. } );
  490. test( 'sets collapsed selection between text and text with attributes', {
  491. data: 'foo[]<$text bold="true">bar</$text>',
  492. check( root, selection ) {
  493. expect( root.maxOffset ).to.equal( 6 );
  494. expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
  495. }
  496. } );
  497. test( 'sets selection containing an element', {
  498. data: 'x[<a></a>]'
  499. } );
  500. it( 'sets selection with attribute containing an element', () => {
  501. const result = parse( 'x[<a></a>]', model.schema, {
  502. selectionAttributes: {
  503. bold: true
  504. }
  505. } );
  506. expect( stringify( result.model, result.selection ) ).to.equal( 'x[<a></a>]' );
  507. expect( result.selection.getAttribute( 'bold' ) ).to.be.true;
  508. } );
  509. it( 'sets a backward selection containing an element', () => {
  510. const result = parse( 'x[<a></a>]', model.schema, {
  511. lastRangeBackward: true
  512. } );
  513. expect( stringify( result.model, result.selection ) ).to.equal( 'x[<a></a>]' );
  514. expect( result.selection.isBackward ).to.true;
  515. } );
  516. test( 'sets selection within a text', {
  517. data: 'x[y]z'
  518. } );
  519. it( 'sets selection within a text with different attributes', () => {
  520. const result = parse( '<$text bold="true">fo[o</$text>ba]r', model.schema, {
  521. selectionAttributes: { bold: true }
  522. } );
  523. expect( stringify( result.model, result.selection ) ).to.equal( '<$text bold="true">fo[o</$text>ba]r' );
  524. expect( result.selection.getAttribute( 'bold' ) ).to.true;
  525. } );
  526. it( 'throws when missing selection start', () => {
  527. expect( () => {
  528. parse( 'foo]', model.schema );
  529. } ).to.throw( Error, /^Parse error/ );
  530. } );
  531. it( 'throws when missing selection end', () => {
  532. expect( () => {
  533. parse( '[foo', model.schema );
  534. } ).to.throw( Error, /^Parse error/ );
  535. } );
  536. } );
  537. function test( title, options ) {
  538. it( title, () => {
  539. const output = options.output || options.data;
  540. const data = parse( options.data, model.schema );
  541. let converted, selection;
  542. if ( data.selection && data.model ) {
  543. converted = data.model;
  544. selection = data.selection;
  545. } else {
  546. converted = data;
  547. }
  548. expect( stringify( converted, selection ) ).to.equal( output );
  549. if ( options.check ) {
  550. options.check( converted, selection );
  551. }
  552. } );
  553. }
  554. } );
  555. } );