bindtwostepcarettoattribute.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document */
  6. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  7. import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
  8. import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
  9. import { upcastElementToAttribute } from '../../src/conversion/upcast-converters';
  10. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  11. import { setData } from '../../src/dev-utils/model';
  12. describe( 'bindTwoStepCaretToAttribute()', () => {
  13. let editor, model, emitter, selection, viewDoc, preventDefaultSpy;
  14. beforeEach( () => {
  15. emitter = Object.create( DomEmitterMixin );
  16. return VirtualTestEditor.create().then( newEditor => {
  17. editor = newEditor;
  18. model = editor.model;
  19. selection = model.document.selection;
  20. viewDoc = editor.editing.view.document;
  21. preventDefaultSpy = sinon.spy();
  22. editor.model.schema.extend( '$text', {
  23. allowAttributes: [ 'a', 'b', 'c' ],
  24. allowIn: '$root'
  25. } );
  26. editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'a', model: 'a' } ) );
  27. editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'b', model: 'b' } ) );
  28. editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'c', model: 'c' } ) );
  29. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' );
  30. } );
  31. } );
  32. afterEach( () => {
  33. return editor.destroy();
  34. } );
  35. describe( 'moving right', () => {
  36. it( 'should do nothing for unrelated attribute (at the beginning)', () => {
  37. setData( model, '[]<$text c="true">foo</$text>' );
  38. testTwoStepCaretMovement( [
  39. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  40. '→',
  41. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  42. ] );
  43. } );
  44. it( 'should do nothing for unrelated attribute (at the end)', () => {
  45. setData( model, '<$text c="true">foo[]</$text>' );
  46. testTwoStepCaretMovement( [
  47. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  48. '→',
  49. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  50. ] );
  51. } );
  52. it( 'should "enter" the text with attribute in two steps', () => {
  53. setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
  54. testTwoStepCaretMovement( [
  55. // Gravity is not overridden, caret is at the beginning of the text but is "outside" of the text.
  56. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  57. '→',
  58. // Gravity is overridden, caret movement is blocked, selection at the beginning but "inside" the text.
  59. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: true, preventDefault: 1 },
  60. '→',
  61. // Caret movement was not blocked this time (still once) so everything works normally.
  62. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1 },
  63. ] );
  64. } );
  65. it( 'should "leave" the text with attribute in two steps', () => {
  66. setData( model, '<$text a="true" b="true">bar[]</$text><$text c="true">foo</$text>' );
  67. testTwoStepCaretMovement( [
  68. // Gravity is not overridden, caret is at the end of the text but is "inside" of the text.
  69. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  70. '→',
  71. // Gravity is overridden, caret movement is blocked, selection at the end but "outside" the text.
  72. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 1 },
  73. '→',
  74. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1 },
  75. ] );
  76. } );
  77. it( 'should use two-steps movement when between nodes with the same attribute but different value', () => {
  78. setData( model, '<$text a="1">bar[]</$text><$text a="2">foo</$text>' );
  79. testTwoStepCaretMovement( [
  80. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  81. '→',
  82. // <$text a="1">bar</$text>[]<$text a="2">foo</$text>
  83. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 },
  84. '→',
  85. // <$text a="1">bar</$text><$text a="2">[]foo</$text>
  86. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 2 },
  87. '→',
  88. // <$text a="1">bar</$text><$text a="2">f[]oo</$text>
  89. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2 }
  90. ] );
  91. } );
  92. // https://github.com/ckeditor/ckeditor5/issues/937
  93. it( 'should not require two-steps between unrelated attributes inside the initial attribute', () => {
  94. setData( model, '<$text a="1">fo[]o</$text><$text a="1" b="2">bar</$text><$text a="1">baz</$text>' );
  95. testTwoStepCaretMovement( [
  96. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  97. '→',
  98. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  99. '→',
  100. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  101. '→',
  102. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  103. '→',
  104. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  105. '→',
  106. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 }
  107. ] );
  108. } );
  109. it( 'should handle passing through the only character in the block', () => {
  110. setData( model, '[]<$text a="1">x</$text>' );
  111. testTwoStepCaretMovement( [
  112. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  113. '→',
  114. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  115. '→',
  116. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 },
  117. '→',
  118. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 }
  119. ] );
  120. } );
  121. it( 'should handle passing through the only character in the block (no attribute in the initial selection)', () => {
  122. setData( model, '[]<$text a="1">x</$text>' );
  123. model.change( writer => writer.removeSelectionAttribute( 'a' ) );
  124. testTwoStepCaretMovement( [
  125. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  126. '→',
  127. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1 },
  128. '→',
  129. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1 },
  130. '→',
  131. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 2 },
  132. '→',
  133. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 2 }
  134. ] );
  135. } );
  136. it( 'should handle passing through the only-child with an attribute (multiple characters)', () => {
  137. setData( model, '[]<$text a="1">xyz</$text>' );
  138. testTwoStepCaretMovement( [
  139. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  140. '→',
  141. // <$text a="1">x{}yz</$text>
  142. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  143. '→',
  144. // <$text a="1">xy{}z</$text>
  145. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  146. '→',
  147. // <$text a="1">xyz{}</$text>
  148. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  149. '→',
  150. // <$text a="1">xyz</$text>{}
  151. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 },
  152. '→',
  153. // <$text a="1">xyz</$text>{}
  154. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 }
  155. ] );
  156. } );
  157. } );
  158. describe( 'moving left', () => {
  159. it( 'should "enter" the text with attribute in two steps', () => {
  160. setData( model, '<$text>foo</$text><$text a="true" b="true">bar</$text><$text c="true">b[]iz</$text>' );
  161. testTwoStepCaretMovement( [
  162. // Gravity is not overridden, caret is a one character after the and of the text.
  163. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  164. '←',
  165. // Caret movement was not blocked but the gravity is overridden.
  166. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 0 },
  167. '←',
  168. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1 },
  169. '←',
  170. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1 },
  171. ] );
  172. } );
  173. it( 'should "leave" the text with attribute in two steps', () => {
  174. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  175. testTwoStepCaretMovement( [
  176. // Gravity is not overridden, caret is a one character after the beginning of the text.
  177. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  178. '←',
  179. // Caret movement was not blocked.
  180. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: true, preventDefault: 0 },
  181. '←',
  182. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1 },
  183. '←',
  184. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1 }
  185. ] );
  186. } );
  187. it( 'should do nothing for unrelated attribute (at the beginning)', () => {
  188. setData( model, '<$text c="true">[]foo</$text>' );
  189. testTwoStepCaretMovement( [
  190. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  191. '←',
  192. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  193. '←',
  194. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 }
  195. ] );
  196. } );
  197. it( 'should do nothing for unrelated attribute (at the end)', () => {
  198. setData( model, '<$text c="true">foo</$text>[]' );
  199. testTwoStepCaretMovement( [
  200. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  201. '←',
  202. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 },
  203. '←',
  204. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0 }
  205. ] );
  206. } );
  207. it( 'should do nothing when caret is at the beginning of block element', () => {
  208. setData( model, '[]foo', { lastRangeBackward: true } );
  209. testTwoStepCaretMovement( [
  210. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  211. '←',
  212. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  213. ] );
  214. } );
  215. it( 'should require two-steps movement when caret goes between text node with the same attribute but different value', () => {
  216. setData( model, '<$text a="2">foo</$text><$text a="1">b[]ar</$text>' );
  217. testTwoStepCaretMovement( [
  218. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  219. '←',
  220. // <$text a="2">foo</$text><$text a="1">[]bar</$text>
  221. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0 },
  222. '←',
  223. // <$text a="2">foo</$text>[]<$text a="1">bar</$text>
  224. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 },
  225. '←',
  226. // <$text a="2">foo[]</$text><$text a="1">bar</$text>
  227. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2 },
  228. '←',
  229. // <$text a="2">fo[]o</$text><$text a="1">bar</$text>
  230. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2 }
  231. ] );
  232. } );
  233. // https://github.com/ckeditor/ckeditor5/issues/937
  234. it( 'should not require two-steps between unrelated attributes inside the initial attribute', () => {
  235. setData( model, '<$text a="1">foo</$text><$text a="1" b="2">bar</$text><$text a="1">b[]az</$text>' );
  236. testTwoStepCaretMovement( [
  237. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  238. '←',
  239. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  240. '←',
  241. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  242. '←',
  243. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0 },
  244. '←',
  245. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 }
  246. ] );
  247. } );
  248. it( 'should handle passing through the only-child with an attribute (single character)', () => {
  249. setData( model, '<$text a="1">x</$text>[]' );
  250. testTwoStepCaretMovement( [
  251. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  252. '←',
  253. // <$text a="1">{}x</$text>
  254. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  255. '←',
  256. // {}<$text a="1">x</$text> (because it's a first-child)
  257. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  258. '←',
  259. // {}<$text a="1">x</$text>
  260. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 }
  261. ] );
  262. } );
  263. it( 'should handle passing through the only character in the block (no attribute in the initial selection)', () => {
  264. setData( model, '<$text a="1">x</$text>[]' );
  265. model.change( writer => writer.removeSelectionAttribute( 'a' ) );
  266. testTwoStepCaretMovement( [
  267. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  268. '←',
  269. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1 },
  270. '←',
  271. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1 },
  272. '←',
  273. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 },
  274. '←',
  275. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 }
  276. ] );
  277. } );
  278. it( 'should handle passing through the only-child with an attribute (multiple characters)', () => {
  279. setData( model, '<$text a="1">xyz</$text>[]' );
  280. testTwoStepCaretMovement( [
  281. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  282. '←',
  283. // <$text a="1">xy{}z</$text>
  284. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  285. '←',
  286. // <$text a="1">x{}yz</$text>
  287. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  288. '←',
  289. // <$text a="1">{}xyz</$text>
  290. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0 },
  291. '←',
  292. // {}<$text a="1">xyz</$text>
  293. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 },
  294. '←',
  295. // {}<$text a="1">xyz</$text>
  296. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 }
  297. ] );
  298. } );
  299. } );
  300. describe( 'moving and typing around the attribute', () => {
  301. it( 'should handle typing after the attribute', () => {
  302. setData( model, '<$text a="1">x[]</$text>' );
  303. testTwoStepCaretMovement( [
  304. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  305. 'y',
  306. // <$text a="1">xy[]</$text>
  307. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  308. '→',
  309. // <$text a="1">xy</$text>[]
  310. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 },
  311. 'z',
  312. // <$text a="1">xy</$text>z[]
  313. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1 },
  314. '←',
  315. // <$text a="1">xy</$text>[]z
  316. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 },
  317. '←',
  318. // <$text a="1">xy[]</$text>z
  319. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2 },
  320. 'w',
  321. // <$text a="1">xyw[]</$text>
  322. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2 },
  323. ] );
  324. } );
  325. it( 'should handle typing before the attribute', () => {
  326. setData( model, '<$text a="1">[]x</$text>' );
  327. testTwoStepCaretMovement( [
  328. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  329. '←',
  330. // []<$text a="1">x</$text>
  331. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  332. 'z',
  333. // z[]<$text a="1">x</$text>
  334. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  335. 'x',
  336. // zx[]<$text a="1">x</$text>
  337. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0 },
  338. '→',
  339. // zx<$text a="1">[]x</$text>
  340. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1 },
  341. 'a',
  342. // zx<$text a="1">a[]x</$text>
  343. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1 },
  344. ] );
  345. } );
  346. } );
  347. describe( 'mouse', () => {
  348. it( 'should not override gravity when selection is placed at the beginning of text', () => {
  349. setData( model, '<$text a="true">[]foo</$text>' );
  350. expect( selection.isGravityOverridden ).to.false;
  351. } );
  352. it( 'should not override gravity when selection is placed at the end of text', () => {
  353. setData( model, '<$text a="true">foo[]</$text>' );
  354. expect( selection.isGravityOverridden ).to.false;
  355. } );
  356. } );
  357. it( 'should do nothing when key other then arrow left and right is pressed', () => {
  358. setData( model, '<$text a="true">foo[]</$text>' );
  359. expect( () => {
  360. fireKeyDownEvent( { keyCode: keyCodes.arrowup } );
  361. } ).to.not.throw();
  362. } );
  363. it( 'should do nothing for non-collapsed selection', () => {
  364. setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
  365. fireKeyDownEvent( { keyCode: keyCodes.arrowright } );
  366. expect( selection.isGravityOverridden ).to.false;
  367. } );
  368. it( 'should do nothing when shift key is pressed', () => {
  369. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  370. fireKeyDownEvent( {
  371. keyCode: keyCodes.arrowleft,
  372. shiftKey: true
  373. } );
  374. expect( selection.isGravityOverridden ).to.false;
  375. } );
  376. it( 'should do nothing when alt key is pressed', () => {
  377. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  378. fireKeyDownEvent( {
  379. keyCode: keyCodes.arrowleft,
  380. altKey: true
  381. } );
  382. expect( selection.isGravityOverridden ).to.false;
  383. } );
  384. it( 'should do nothing when ctrl key is pressed', () => {
  385. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  386. fireKeyDownEvent( {
  387. keyCode: keyCodes.arrowleft,
  388. ctrlKey: true
  389. } );
  390. expect( selection.isGravityOverridden ).to.false;
  391. } );
  392. it( 'should do nothing when the not a direct selection change but at the attribute boundary', () => {
  393. setData( model, '<$text a="true">foo[]</$text>bar' );
  394. testTwoStepCaretMovement( [
  395. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0 },
  396. '→',
  397. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1 },
  398. ] );
  399. // Simulate an external text insertion BEFORE the user selection to trigger #change:range.
  400. model.enqueueChange( 'transparent', writer => {
  401. writer.insertText( 'x', selection.getFirstPosition().getShiftedBy( -2 ) );
  402. } );
  403. expect( selection.isGravityOverridden ).to.true;
  404. expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
  405. } );
  406. const keyMap = {
  407. '→': 'arrowright',
  408. '←': 'arrowleft'
  409. };
  410. function fireKeyDownEvent( options ) {
  411. const eventData = Object.assign( { domTarget: document.body }, options );
  412. viewDoc.fire( 'keydown', eventData );
  413. }
  414. function getSelectionAttributesArray( selection ) {
  415. return Array.from( selection.getAttributeKeys() );
  416. }
  417. function testTwoStepCaretMovement( scenario ) {
  418. for ( const step of scenario ) {
  419. if ( typeof step == 'string' ) {
  420. // An arrow key pressed. Fire the view event and update the model selection.
  421. if ( keyMap[ step ] ) {
  422. let preventDefaultCalled;
  423. fireKeyDownEvent( {
  424. keyCode: keyCodes[ keyMap[ step ] ],
  425. preventDefault: () => {
  426. preventDefaultSpy();
  427. preventDefaultCalled = true;
  428. }
  429. } );
  430. const position = selection.getFirstPosition();
  431. if ( !preventDefaultCalled ) {
  432. if ( step == '→' ) {
  433. if ( !position.isAtEnd ) {
  434. model.change( writer => {
  435. writer.setSelection( selection.getFirstPosition().getShiftedBy( 1 ) );
  436. } );
  437. }
  438. } else if ( step == '←' ) {
  439. if ( !position.isAtStart ) {
  440. model.change( writer => {
  441. writer.setSelection( selection.getFirstPosition().getShiftedBy( -1 ) );
  442. } );
  443. }
  444. }
  445. }
  446. }
  447. // A regular key pressed. Type some text in the model.
  448. else {
  449. model.change( writer => {
  450. writer.insertText( step, selection.getAttributes(), selection.getFirstPosition() );
  451. } );
  452. }
  453. }
  454. // If not a key, then it's an assertion.
  455. else {
  456. const stepIndex = scenario.indexOf( step );
  457. const stepString = `in step #${ stepIndex } ${ JSON.stringify( step ) }`;
  458. expect( getSelectionAttributesArray( selection ) ).to.have.members( step.selectionAttributes, '#attributes ' + stepString );
  459. expect( selection.isGravityOverridden ).to.equal( step.isGravityOverridden, '#isGravityOverridden ' + stepString );
  460. expect( preventDefaultSpy.callCount ).to.equal( step.preventDefault, '#preventDefault ' + stepString );
  461. }
  462. }
  463. }
  464. } );