r101696 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r101695‎ | r101696 | r101697 >
Date:21:20, 2 November 2011
Author:tparscal
Status:ok
Tags:
Comment:
Moving hype tests to VisualEditor extension
Modified paths:
  • /trunk/extensions/VisualEditor/tests/es.DocumentModel.test.js (added) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/tests/es.DocumentModel.test.js
@@ -0,0 +1,990 @@
 2+module( 'es/models' );
 3+
 4+/*
 5+ * Sample plain object (WikiDom).
 6+ *
 7+ * There are two kinds of nodes in WikiDom:
 8+ *
 9+ * {Object} ElementNode
 10+ * type: {String} Symbolic node type name
 11+ * [attributes]: {Object} List of symbolic attribute name and literal value pairs
 12+ * [content]: {Object} Content node (not defined if node has children)
 13+ * [children]: {Object[]} Child nodes (not defined if node has content)
 14+ *
 15+ * {Object} ContentNode
 16+ * text: {String} Plain text data of content
 17+ * [annotations]: {Object[]} List of annotation objects that can be used to render text
 18+ * type: {String} Symbolic name of annotation type
 19+ * start: {Integer} Offset within text to begin annotation
 20+ * end: {Integer} Offset within text to end annotation
 21+ * [data]: {Object} Additional information, only used by more complex annotations
 22+ */
 23+var obj = {
 24+ 'type': 'document',
 25+ 'children': [
 26+ {
 27+ 'type': 'paragraph',
 28+ 'content': {
 29+ 'text': 'abc',
 30+ 'annotations': [
 31+ {
 32+ 'type': 'bold',
 33+ 'start': 1,
 34+ 'end': 2
 35+ },
 36+ {
 37+ 'type': 'italic',
 38+ 'start': 2,
 39+ 'end': 3
 40+ }
 41+ ]
 42+ }
 43+ },
 44+ {
 45+ 'type': 'table',
 46+ 'children': [
 47+ {
 48+ 'type': 'tableRow',
 49+ 'children': [
 50+ {
 51+ 'type': 'tableCell',
 52+ 'children': [
 53+ {
 54+ 'type': 'paragraph',
 55+ 'content': {
 56+ 'text': 'd'
 57+ }
 58+ },
 59+ {
 60+ 'type': 'list',
 61+ 'children': [
 62+ {
 63+ 'type': 'listItem',
 64+ 'attributes': {
 65+ 'styles': ['bullet']
 66+ },
 67+ 'content': {
 68+ 'text': 'e'
 69+ }
 70+ },
 71+ {
 72+ 'type': 'listItem',
 73+ 'attributes': {
 74+ 'styles': ['bullet', 'bullet']
 75+ },
 76+ 'content': {
 77+ 'text': 'f'
 78+ }
 79+ },
 80+ {
 81+ 'type': 'listItem',
 82+ 'attributes': {
 83+ 'styles': ['number']
 84+ },
 85+ 'content': {
 86+ 'text': 'g'
 87+ }
 88+ }
 89+ ]
 90+ }
 91+ ]
 92+ }
 93+ ]
 94+ }
 95+ ]
 96+ },
 97+ {
 98+ 'type': 'paragraph',
 99+ 'content': {
 100+ 'text': 'h'
 101+ }
 102+ }
 103+ ]
 104+};
 105+
 106+/*
 107+ * Sample content data.
 108+ *
 109+ * There are three types of components in content data:
 110+ *
 111+ * {String} Plain text character
 112+ *
 113+ * {Array} Annotated character
 114+ * {String} Character
 115+ * {String} Hash
 116+ * {Object}... List of annotation object references
 117+ *
 118+ * {Object} Opening or closing structural element
 119+ * type: {String} Symbolic node type name, if closing element first character will be "/"
 120+ * node: {Object} Reference to model tree node
 121+ * [attributes]: {Object} List of symbolic attribute name and literal value pairs
 122+ */
 123+var data = [
 124+ // 0 - Beginning of paragraph
 125+ { 'type': 'paragraph' },
 126+ // 1 - Plain content
 127+ 'a',
 128+ // 2 - Annotated content
 129+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 130+ // 3 - Annotated content
 131+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 132+ // 4 - End of paragraph
 133+ { 'type': '/paragraph' },
 134+ // 5 - Beginning of table
 135+ { 'type': 'table' },
 136+ // 6 - Beginning of row
 137+ { 'type': 'tableRow' },
 138+ // 7 - Beginning of cell
 139+ { 'type': 'tableCell' },
 140+ // 8 - Beginning of paragraph
 141+ { 'type': 'paragraph' },
 142+ // 9 - Plain content
 143+ 'd',
 144+ // 10 - End of paragraph
 145+ { 'type': '/paragraph' },
 146+ // 11 - Beginning of list
 147+ { 'type': 'list' },
 148+ // 12 - Beginning of bullet list item
 149+ { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
 150+ // 13 - Plain content
 151+ 'e',
 152+ // 14 - End of item
 153+ { 'type': '/listItem' },
 154+ // 15 - Beginning of nested bullet list item
 155+ { 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
 156+ // 16 - Plain content
 157+ 'f',
 158+ // 17 - End of item
 159+ { 'type': '/listItem' },
 160+ // 18 - Beginning of numbered list item
 161+ { 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
 162+ // 19 - Plain content
 163+ 'g',
 164+ // 20 - End of item
 165+ { 'type': '/listItem' },
 166+ // 21 - End of list
 167+ { 'type': '/list' },
 168+ // 22 - End of cell
 169+ { 'type': '/tableCell' },
 170+ // 23 - End of row
 171+ { 'type': '/tableRow' },
 172+ // 24 - End of table
 173+ { 'type': '/table' },
 174+ // 25 - Beginning of paragraph
 175+ { 'type': 'paragraph' },
 176+ // 26 - Plain content
 177+ 'h',
 178+ // 27 - End of paragraph
 179+ { 'type': '/paragraph' }
 180+];
 181+
 182+/**
 183+ * Sample content data index.
 184+ *
 185+ * This is a node tree that describes each partition within the document's content data. This is
 186+ * what is automatically built by the es.DocumentModel constructor.
 187+ */
 188+var tree = [
 189+ new es.ParagraphModel( data[0], 3 ),
 190+ new es.TableModel( data[5], [
 191+ new es.TableRowModel( data[6], [
 192+ new es.TableCellModel( data[7], [
 193+ new es.ParagraphModel( data[8], 1 ),
 194+ new es.ListModel( data[11], [
 195+ new es.ListItemModel( data[12], 1 ),
 196+ new es.ListItemModel( data[15], 1 ),
 197+ new es.ListItemModel( data[18], 1 )
 198+ ] )
 199+ ] )
 200+ ] )
 201+ ] ),
 202+ new es.ParagraphModel( data[25], 1 )
 203+];
 204+
 205+test( 'es.DocumentModel.getData', 1, function() {
 206+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 207+
 208+ // Test 1
 209+ deepEqual( documentModel.getData(), data, 'Flattening plain objects results in correct data' );
 210+} );
 211+
 212+test( 'es.DocumentModel.getChildren', 1, function() {
 213+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 214+
 215+ function equalLengths( a, b ) {
 216+ if ( a.length !== b.length ) {
 217+ return false;
 218+ }
 219+ for ( var i = 0; i < a.length; i++ ) {
 220+ if ( a[i].getContentLength() !== b[i].getContentLength() ) {
 221+ console.log( 'mismatched content lengths', a[i], b[i] );
 222+ return false;
 223+ }
 224+ if ( !equalLengths( a[i].getChildren(), b[i].getChildren() ) ) {
 225+ return false;
 226+ }
 227+ }
 228+ return true;
 229+ }
 230+
 231+ // Test 1
 232+ ok(
 233+ equalLengths( documentModel.getChildren(), tree ),
 234+ 'Nodes in the model tree contain correct lengths'
 235+ );
 236+} );
 237+
 238+test( 'es.DocumentModel.getRelativeContentOffset', 7, function() {
 239+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 240+
 241+ // Test 1
 242+ equal(
 243+ documentModel.getRelativeContentOffset( 1, 1 ),
 244+ 2,
 245+ 'getRelativeContentOffset advances forwards through the inside of elements'
 246+ );
 247+ // Test 2
 248+ equal(
 249+ documentModel.getRelativeContentOffset( 2, -1 ),
 250+ 1,
 251+ 'getRelativeContentOffset advances backwards through the inside of elements'
 252+ );
 253+ // Test 3
 254+ equal(
 255+ documentModel.getRelativeContentOffset( 3, 1 ),
 256+ 4,
 257+ 'getRelativeContentOffset uses the offset after the last character in an element'
 258+ );
 259+ // Test 3
 260+ equal(
 261+ documentModel.getRelativeContentOffset( 1, -1 ),
 262+ 1,
 263+ 'getRelativeContentOffset treats the begining a document as a non-content offset'
 264+ );
 265+ // Test 4
 266+ equal(
 267+ documentModel.getRelativeContentOffset( 27, 1 ),
 268+ 27,
 269+ 'getRelativeContentOffset treats the end a document as a non-content offset'
 270+ );
 271+ // Test 5
 272+ equal(
 273+ documentModel.getRelativeContentOffset( 4, 1 ),
 274+ 9,
 275+ 'getRelativeContentOffset advances forwards between elements'
 276+ );
 277+ // Test 6
 278+ equal(
 279+ documentModel.getRelativeContentOffset( 26, -1 ),
 280+ 20,
 281+ 'getRelativeContentOffset advances backwards between elements'
 282+ );
 283+} );
 284+
 285+test( 'es.DocumentModel.getContent', 6, function() {
 286+ var documentModel = es.DocumentModel.newFromPlainObject( obj ),
 287+ childNodes = documentModel.getChildren();
 288+
 289+ // Test 1
 290+ deepEqual(
 291+ childNodes[0].getContent( new es.Range( 1, 3 ) ),
 292+ [
 293+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 294+ ['c', { 'type': 'italic', 'hash': '#italic' }]
 295+ ],
 296+ 'getContent can return an ending portion of the content'
 297+ );
 298+
 299+ // Test 2
 300+ deepEqual(
 301+ childNodes[0].getContent( new es.Range( 0, 2 ) ),
 302+ ['a', ['b', { 'type': 'bold', 'hash': '#bold' }]],
 303+ 'getContent can return a beginning portion of the content'
 304+ );
 305+
 306+ // Test 3
 307+ deepEqual(
 308+ childNodes[0].getContent( new es.Range( 1, 2 ) ),
 309+ [['b', { 'type': 'bold', 'hash': '#bold' }]],
 310+ 'getContent can return a middle portion of the content'
 311+ );
 312+
 313+ // Test 4
 314+ try {
 315+ childNodes[0].getContent( new es.Range( -1, 3 ) );
 316+ } catch ( negativeIndexError ) {
 317+ ok( true, 'getContent throws exceptions when given a range with start < 0' );
 318+ }
 319+
 320+ // Test 5
 321+ try {
 322+ childNodes[0].getContent( new es.Range( 0, 4 ) );
 323+ } catch ( outOfRangeError ) {
 324+ ok( true, 'getContent throws exceptions when given a range with end > length' );
 325+ }
 326+
 327+ // Test 6
 328+ deepEqual( childNodes[2].getContent(), ['h'], 'Content can be extracted from nodes' );
 329+} );
 330+
 331+test( 'es.DocumentModel.getIndexOfAnnotation', 3, function() {
 332+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 333+
 334+ var bold = { 'type': 'bold', 'hash': '#bold' },
 335+ italic = { 'type': 'italic', 'hash': '#italic' },
 336+ nothing = { 'type': 'nothing', 'hash': '#nothing' },
 337+ character = ['a', bold, italic];
 338+
 339+ // Test 1
 340+ equal(
 341+ es.DocumentModel.getIndexOfAnnotation( character, bold ),
 342+ 1,
 343+ 'getIndexOfAnnotation get the correct index'
 344+ );
 345+
 346+ // Test 2
 347+ equal(
 348+ es.DocumentModel.getIndexOfAnnotation( character, italic ),
 349+ 2,
 350+ 'getIndexOfAnnotation get the correct index'
 351+ );
 352+
 353+ // Test 3
 354+ equal(
 355+ es.DocumentModel.getIndexOfAnnotation( character, nothing ),
 356+ -1,
 357+ 'getIndexOfAnnotation returns -1 if the annotation was not found'
 358+ );
 359+} );
 360+
 361+test( 'es.DocumentModel.getWordBoundaries', 2, function() {
 362+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 363+ deepEqual(
 364+ documentModel.getWordBoundaries( 2 ),
 365+ new es.Range( 1, 4 ),
 366+ 'getWordBoundaries returns range around nearest whole word'
 367+ );
 368+ strictEqual(
 369+ documentModel.getWordBoundaries( 5 ),
 370+ null,
 371+ 'getWordBoundaries returns null when given non-content offset'
 372+ );
 373+} );
 374+
 375+test( 'es.DocumentModel.getAnnotationBoundaries', 2, function() {
 376+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 377+ deepEqual(
 378+ documentModel.getAnnotationBoundaries( 2, { 'type': 'bold' } ),
 379+ new es.Range( 2, 3 ),
 380+ 'getWordBoundaries returns range around content covered by annotation'
 381+ );
 382+ strictEqual(
 383+ documentModel.getAnnotationBoundaries( 1, { 'type': 'bold' } ),
 384+ null,
 385+ 'getWordBoundaries returns null if offset is not covered by annotation'
 386+ );
 387+} );
 388+
 389+test( 'es.DocumentModel.getAnnotationsFromOffset', 4, function() {
 390+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 391+ deepEqual(
 392+ documentModel.getAnnotationsFromOffset( 1 ),
 393+ [],
 394+ 'getAnnotationsFromOffset returns empty array for non-annotated content'
 395+ );
 396+ deepEqual(
 397+ documentModel.getAnnotationsFromOffset( 2 ),
 398+ [{ 'type': 'bold', 'hash': '#bold' }],
 399+ 'getAnnotationsFromOffset returns annotations of annotated content correctly'
 400+ );
 401+ deepEqual(
 402+ documentModel.getAnnotationsFromOffset( 3 ),
 403+ [{ 'type': 'italic', 'hash': '#italic' }],
 404+ 'getAnnotationsFromOffset returns annotations of annotated content correctly'
 405+ );
 406+ deepEqual(
 407+ documentModel.getAnnotationsFromOffset( 0 ),
 408+ [],
 409+ 'getAnnotationsFromOffset returns empty array when given a non-content offset'
 410+ );
 411+} );
 412+
 413+test( 'es.DocumentModel.prepareElementAttributeChange', 4, function() {
 414+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 415+
 416+ // Test 1
 417+ deepEqual(
 418+ documentModel.prepareElementAttributeChange( 0, 'set', 'test', 1234 ).getOperations(),
 419+ [
 420+ { 'type': 'attribute', 'method': 'set', 'key': 'test', 'value': 1234 },
 421+ { 'type': 'retain', 'length': 28 }
 422+ ],
 423+ 'prepareElementAttributeChange retains data after attribute change for first element'
 424+ );
 425+
 426+ // Test 2
 427+ deepEqual(
 428+ documentModel.prepareElementAttributeChange( 5, 'set', 'test', 1234 ).getOperations(),
 429+ [
 430+ { 'type': 'retain', 'length': 5 },
 431+ { 'type': 'attribute', 'method': 'set', 'key': 'test', 'value': 1234 },
 432+ { 'type': 'retain', 'length': 23 }
 433+ ],
 434+ 'prepareElementAttributeChange retains data before and after attribute change'
 435+ );
 436+
 437+ // Test 3
 438+ try {
 439+ documentModel.prepareElementAttributeChange( 1, 'set', 'test', 1234 );
 440+ } catch ( invalidOffsetError ) {
 441+ ok(
 442+ true,
 443+ 'prepareElementAttributeChange throws an exception when offset is not an element'
 444+ );
 445+ }
 446+
 447+ // Test 4
 448+ try {
 449+ documentModel.prepareElementAttributeChange( 4, 'set', 'test', 1234 );
 450+ } catch ( closingElementError ) {
 451+ ok(
 452+ true,
 453+ 'prepareElementAttributeChange throws an exception when offset is a closing element'
 454+ );
 455+ }
 456+} );
 457+
 458+test( 'es.DocumentModel.prepareContentAnnotation', 1, function() {
 459+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 460+
 461+ // Test 1
 462+ deepEqual(
 463+ documentModel.prepareContentAnnotation(
 464+ new es.Range( 1, 4 ), 'set', { 'type': 'bold' }
 465+ ).getOperations(),
 466+ [
 467+ { 'type': 'retain', 'length': 1 },
 468+ {
 469+ 'type': 'annotate',
 470+ 'method': 'set',
 471+ 'bias': 'start',
 472+ 'annotation': { 'type': 'bold', 'hash': '#bold' }
 473+ },
 474+ { 'type': 'retain', 'length': 1 },
 475+ {
 476+ 'type': 'annotate',
 477+ 'method': 'set',
 478+ 'bias': 'stop',
 479+ 'annotation': { 'type': 'bold', 'hash': '#bold' }
 480+ },
 481+ { 'type': 'retain', 'length': 1 },
 482+ {
 483+ 'type': 'annotate',
 484+ 'method': 'set',
 485+ 'bias': 'start',
 486+ 'annotation': { 'type': 'bold', 'hash': '#bold' }
 487+ },
 488+ { 'type': 'retain', 'length': 1 },
 489+ {
 490+ 'type': 'annotate',
 491+ 'method': 'set',
 492+ 'bias': 'stop',
 493+ 'annotation': { 'type': 'bold', 'hash': '#bold' }
 494+ },
 495+ { 'type': 'retain', 'length': 24 }
 496+ ],
 497+ 'prepareContentAnnotation skips over content that is already set or cleared'
 498+ );
 499+} );
 500+
 501+test( 'es.DocumentModel.prepareRemoval', 1, function() {
 502+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 503+
 504+ // Test 1
 505+ deepEqual(
 506+ documentModel.prepareRemoval( new es.Range( 1, 4 ) ).getOperations(),
 507+ [
 508+ { 'type': 'retain', 'length': 1 },
 509+ {
 510+ 'type': 'remove',
 511+ 'data': [
 512+ 'a',
 513+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 514+ ['c', { 'type': 'italic', 'hash': '#italic' }]
 515+ ]
 516+ },
 517+ { 'type': 'retain', 'length': 24 }
 518+ ],
 519+ 'prepareRemoval includes the content being removed'
 520+ );
 521+
 522+ /*
 523+ // Test 2
 524+ deepEqual(
 525+ documentModel.prepareRemoval( new es.Range( 15, 18 ) ).getOperations(),
 526+ [
 527+ { 'type': 'retain', 'length': 15 },
 528+ {
 529+ 'type': 'remove',
 530+ 'data': [
 531+ { 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
 532+ 'b',
 533+ { 'type': '/listItem' }
 534+ ]
 535+ },
 536+ { 'type': 'retain', 'length': 10 }
 537+ ],
 538+ 'prepareRemoval removes entire elements'
 539+ );
 540+
 541+ // Test 3
 542+ deepEqual(
 543+ documentModel.prepareRemoval( new es.Range( 17, 19 ) ).getOperations(),
 544+ [
 545+ { 'type': 'retain', 'length': 17 },
 546+ {
 547+ 'type': 'remove',
 548+ 'data': [
 549+ { 'type': '/listItem' },
 550+ { 'type': 'listItem', 'attributes': { 'styles': ['number'] } }
 551+ ]
 552+ },
 553+ { 'type': 'retain', 'length': 9 }
 554+ ],
 555+ 'prepareRemoval merges two list items'
 556+ );
 557+ */
 558+} );
 559+
 560+test( 'es.DocumentModel.prepareInsertion', 11, function() {
 561+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 562+
 563+ // Test 1
 564+ deepEqual(
 565+ documentModel.prepareInsertion( 1, ['d', 'e', 'f'] ).getOperations(),
 566+ [
 567+ { 'type': 'retain', 'length': 1 },
 568+ { 'type': 'insert', 'data': ['d', 'e', 'f'] },
 569+ { 'type': 'retain', 'length': 27 }
 570+ ],
 571+ 'prepareInsertion retains data up to the offset and includes the content being inserted'
 572+ );
 573+
 574+ // Test 2
 575+ deepEqual(
 576+ documentModel.prepareInsertion(
 577+ 5, [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
 578+ ).getOperations(),
 579+ [
 580+ { 'type': 'retain', 'length': 5 },
 581+ {
 582+ 'type': 'insert',
 583+ 'data': [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
 584+ },
 585+ { 'type': 'retain', 'length': 23 }
 586+ ],
 587+ 'prepareInsertion inserts a paragraph between two structural elements'
 588+ );
 589+
 590+ // Test 3
 591+ deepEqual(
 592+ documentModel.prepareInsertion( 5, ['d', 'e', 'f'] ).getOperations(),
 593+ [
 594+ { 'type': 'retain', 'length': 5 },
 595+ {
 596+ 'type': 'insert',
 597+ 'data': [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
 598+ },
 599+ { 'type': 'retain', 'length': 23 }
 600+ ],
 601+ 'prepareInsertion wraps unstructured content inserted between elements in a paragraph'
 602+ );
 603+
 604+ // Test 4
 605+ deepEqual(
 606+ documentModel.prepareInsertion(
 607+ 5, [{ 'type': 'paragraph' }, 'd', 'e', 'f']
 608+ ).getOperations(),
 609+ [
 610+ { 'type': 'retain', 'length': 5 },
 611+ {
 612+ 'type': 'insert',
 613+ 'data': [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
 614+ },
 615+ { 'type': 'retain', 'length': 23 }
 616+ ],
 617+ 'prepareInsertion completes opening elements in inserted content'
 618+ );
 619+
 620+ // Test 5
 621+ deepEqual(
 622+ documentModel.prepareInsertion(
 623+ 2, [ { 'type': 'table' }, { 'type': '/table' } ]
 624+ ).getOperations(),
 625+ [
 626+ { 'type': 'retain', 'length': 2 },
 627+ {
 628+ 'type': 'insert',
 629+ 'data': [ { 'type': '/paragraph' }, { 'type': 'table' }, { 'type': '/table' }, { 'type': 'paragraph' } ]
 630+ },
 631+ { 'type': 'retain', 'length': 26 }
 632+ ],
 633+ 'prepareInsertion splits up paragraph when inserting a table in the middle'
 634+ );
 635+
 636+ // Test 6
 637+ deepEqual(
 638+ documentModel.prepareInsertion(
 639+ 2, [ 'f', 'o', 'o', { 'type': '/paragraph' }, { 'type': 'paragraph' }, 'b', 'a', 'r' ]
 640+ ).getOperations(),
 641+ [
 642+ { 'type': 'retain', 'length': 2 },
 643+ {
 644+ 'type': 'insert',
 645+ 'data': [ 'f', 'o', 'o', { 'type': '/paragraph' }, { 'type': 'paragraph' }, 'b', 'a', 'r' ]
 646+ },
 647+ { 'type': 'retain', 'length': 26 }
 648+ ],
 649+ 'prepareInsertion splits up paragraph when inserting a paragraph closing and opening into a paragraph'
 650+ );
 651+
 652+ // Test 7
 653+ deepEqual(
 654+ documentModel.prepareInsertion(
 655+ 0, [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
 656+ ).getOperations(),
 657+ [
 658+ {
 659+ 'type': 'insert',
 660+ 'data': [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
 661+ },
 662+ { 'type': 'retain', 'length': 28 }
 663+ ],
 664+ 'prepareInsertion inserts at the beginning, then retains up to the end'
 665+ );
 666+
 667+ // Test 8
 668+ deepEqual(
 669+ documentModel.prepareInsertion(
 670+ 28, [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
 671+ ).getOperations(),
 672+ [
 673+ { 'type': 'retain', 'length': 28 },
 674+ {
 675+ 'type': 'insert',
 676+ 'data': [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
 677+ }
 678+ ],
 679+ 'prepareInsertion inserts at the end'
 680+ );
 681+
 682+ // Test 9
 683+ raises(
 684+ function() {
 685+ documentModel.prepareInsertion(
 686+ -1,
 687+ [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
 688+ );
 689+ },
 690+ /^Offset -1 out of bounds/,
 691+ 'prepareInsertion throws exception for negative offset'
 692+ );
 693+
 694+ // Test 10
 695+ raises(
 696+ function() {
 697+ documentModel.prepareInsertion(
 698+ 29,
 699+ [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
 700+ );
 701+ },
 702+ /^Offset 29 out of bounds/,
 703+ 'prepareInsertion throws exception for offset past the end'
 704+ );
 705+
 706+ // Test 11
 707+ raises(
 708+ function() {
 709+ documentModel.prepareInsertion(
 710+ 5,
 711+ [{ 'type': 'paragraph' }, 'a', { 'type': 'listItem' }, { 'type': '/paragraph' }]
 712+ );
 713+ },
 714+ /^Input is malformed: expected \/listItem but got \/paragraph at index 3$/,
 715+ 'prepareInsertion throws exception for malformed input'
 716+ );
 717+} );
 718+
 719+test( 'es.DocumentModel.commit, es.DocumentModel.rollback', 10, function() {
 720+ var documentModel = es.DocumentModel.newFromPlainObject( obj );
 721+
 722+ var elementAttributeChange = documentModel.prepareElementAttributeChange(
 723+ 0, 'set', 'test', 1
 724+ );
 725+
 726+ // Test 1
 727+ documentModel.commit( elementAttributeChange );
 728+ deepEqual(
 729+ documentModel.getData( new es.Range( 0, 5 ) ),
 730+ [
 731+ { 'type': 'paragraph', 'attributes': { 'test': 1 } },
 732+ 'a',
 733+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 734+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 735+ { 'type': '/paragraph' }
 736+ ],
 737+ 'commit applies an element attribute change transaction to the content'
 738+ );
 739+
 740+ // Test 2
 741+ documentModel.rollback( elementAttributeChange );
 742+ deepEqual(
 743+ documentModel.getData( new es.Range( 0, 5 ) ),
 744+ [
 745+ { 'type': 'paragraph' },
 746+ 'a',
 747+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 748+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 749+ { 'type': '/paragraph' }
 750+ ],
 751+ 'rollback reverses the effect of an element attribute change transaction on the content'
 752+ );
 753+
 754+ var contentAnnotation = documentModel.prepareContentAnnotation(
 755+ new es.Range( 1, 4 ), 'set', { 'type': 'bold' }
 756+ );
 757+
 758+ // Test 3
 759+ documentModel.commit( contentAnnotation );
 760+ deepEqual(
 761+ documentModel.getData( new es.Range( 0, 5 ) ),
 762+ [
 763+ { 'type': 'paragraph' },
 764+ ['a', { 'type': 'bold', 'hash': '#bold' }],
 765+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 766+ ['c', { 'type': 'italic', 'hash': '#italic' }, { 'type': 'bold', 'hash': '#bold' }],
 767+ { 'type': '/paragraph' }
 768+ ],
 769+ 'commit applies a content annotation transaction to the content'
 770+ );
 771+
 772+ // Test 4
 773+ documentModel.rollback( contentAnnotation );
 774+ deepEqual(
 775+ documentModel.getData( new es.Range( 0, 5 ) ),
 776+ [
 777+ { 'type': 'paragraph' },
 778+ 'a',
 779+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 780+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 781+ { 'type': '/paragraph' }
 782+ ],
 783+ 'rollback reverses the effect of a content annotation transaction on the content'
 784+ );
 785+
 786+ var insertion = documentModel.prepareInsertion( 3, ['d'] );
 787+
 788+ // Test 5
 789+ documentModel.commit( insertion );
 790+ deepEqual(
 791+ documentModel.getData( new es.Range( 0, 6 ) ),
 792+ [
 793+ { 'type': 'paragraph' },
 794+ 'a',
 795+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 796+ 'd',
 797+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 798+ { 'type': '/paragraph' }
 799+ ],
 800+ 'commit applies an insertion transaction to the content'
 801+ );
 802+
 803+ // Test 6
 804+ deepEqual(
 805+ documentModel.getChildren()[0].getContent(),
 806+ [
 807+ 'a',
 808+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 809+ 'd',
 810+ ['c', { 'type': 'italic', 'hash': '#italic' }]
 811+ ],
 812+ 'commit keeps model tree up to date'
 813+ );
 814+
 815+ // Test 7
 816+ documentModel.rollback( insertion );
 817+ deepEqual(
 818+ documentModel.getData( new es.Range( 0, 5 ) ),
 819+ [
 820+ { 'type': 'paragraph' },
 821+ 'a',
 822+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 823+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 824+ { 'type': '/paragraph' }
 825+ ],
 826+ 'rollback reverses the effect of an insertion transaction on the content'
 827+ );
 828+
 829+ // Test 8
 830+ deepEqual(
 831+ documentModel.getChildren()[0].getContent(),
 832+ [
 833+ 'a',
 834+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 835+ ['c', { 'type': 'italic', 'hash': '#italic' }]
 836+ ],
 837+ 'rollback keeps model tree up to date'
 838+ );
 839+
 840+ var removal = documentModel.prepareRemoval( new es.Range( 2, 4 ) );
 841+
 842+ // Test 9
 843+ documentModel.commit( removal );
 844+ deepEqual(
 845+ documentModel.getData( new es.Range( 0, 3 ) ),
 846+ [
 847+ { 'type': 'paragraph' },
 848+ 'a',
 849+ { 'type': '/paragraph' }
 850+ ],
 851+ 'commit applies a removal transaction to the content'
 852+ );
 853+
 854+ // Test 10
 855+ documentModel.rollback( removal );
 856+ deepEqual(
 857+ documentModel.getData( new es.Range( 0, 5 ) ),
 858+ [
 859+ { 'type': 'paragraph' },
 860+ 'a',
 861+ ['b', { 'type': 'bold', 'hash': '#bold' }],
 862+ ['c', { 'type': 'italic', 'hash': '#italic' }],
 863+ { 'type': '/paragraph' }
 864+ ],
 865+ 'rollback reverses the effect of a removal transaction on the content'
 866+ );
 867+
 868+} );
 869+
 870+test( 'es.DocumentDocumentModelNode child operations', 20, function() {
 871+ // Example data (integers) is used for simplicity of testing
 872+ var node1 = new es.DocumentModelNode( null ),
 873+ node2 = new es.DocumentModelNode( null ),
 874+ node3 = new es.DocumentModelNode( null, [new es.DocumentModelNode()] ),
 875+ node4 = new es.DocumentModelNode(
 876+ null,
 877+ [new es.DocumentModelNode(), new es.DocumentModelNode()]
 878+ );
 879+
 880+ // Event triggering is detected using a callback that increments a counter
 881+ var updates = 0;
 882+ node1.on( 'update', function() {
 883+ updates++;
 884+ } );
 885+ var attaches = 0;
 886+ node2.on( 'afterAttach', function() {
 887+ attaches++;
 888+ } );
 889+ node3.on( 'afterAttach', function() {
 890+ attaches++;
 891+ } );
 892+ node4.on( 'afterAttach', function() {
 893+ attaches++;
 894+ } );
 895+ var detaches = 0;
 896+ node2.on( 'afterDetach', function() {
 897+ detaches++;
 898+ } );
 899+ node3.on( 'afterDetach', function() {
 900+ detaches++;
 901+ } );
 902+ node4.on( 'afterDetach', function() {
 903+ detaches++;
 904+ } );
 905+ function strictArrayValueEqual( a, b, msg ) {
 906+ if ( a.length !== b.length ) {
 907+ ok( false, msg );
 908+ return;
 909+ }
 910+ for ( var i = 0; i < a.length; i++ ) {
 911+ if ( a[i] !== b[i] ) {
 912+ ok( false, msg );
 913+ return;
 914+ }
 915+ }
 916+ ok( true, msg );
 917+ }
 918+
 919+ // Test 1
 920+ node1.push( node2 );
 921+ equal( updates, 1, 'push emits update events' );
 922+ strictArrayValueEqual( node1.getChildren(), [node2], 'push appends a node' );
 923+
 924+ // Test 2
 925+ equal( attaches, 1, 'push attaches added node' );
 926+
 927+ // Test 3, 4
 928+ node1.unshift( node3 );
 929+ equal( updates, 2, 'unshift emits update events' );
 930+ strictArrayValueEqual( node1.getChildren(), [node3, node2], 'unshift prepends a node' );
 931+
 932+ // Test 5
 933+ equal( attaches, 2, 'unshift attaches added node' );
 934+
 935+ // Test 6, 7
 936+ node1.splice( 1, 0, node4 );
 937+ equal( updates, 3, 'splice emits update events' );
 938+ strictArrayValueEqual( node1.getChildren(), [node3, node4, node2], 'splice inserts nodes' );
 939+
 940+ // Test 8
 941+ equal( attaches, 3, 'splice attaches added nodes' );
 942+
 943+ // Test 9
 944+ node1.reverse();
 945+ equal( updates, 4, 'reverse emits update events' );
 946+
 947+ // Test 10, 11
 948+ node1.sort( function( a, b ) {
 949+ return a.getChildren().length < b.getChildren().length ? -1 : 1;
 950+ } );
 951+ equal( updates, 5, 'sort emits update events' );
 952+ strictArrayValueEqual(
 953+ node1.getChildren(),
 954+ [node2, node3, node4],
 955+ 'sort reorderes nodes correctly'
 956+ );
 957+
 958+ // Test 12, 13
 959+ node1.pop();
 960+ equal( updates, 6, 'pop emits update events' );
 961+ strictArrayValueEqual(
 962+ node1.getChildren(),
 963+ [node2, node3],
 964+ 'pop removes the last child node'
 965+ );
 966+
 967+ // Test 14
 968+ equal( detaches, 1, 'pop detaches a node' );
 969+
 970+ // Test 15, 16
 971+ node1.shift();
 972+ equal( updates, 7, 'es.ModelNode emits update events on shift' );
 973+ strictArrayValueEqual(
 974+ node1.getChildren(),
 975+ [node3],
 976+ 'es.ModelNode removes first Node on shift'
 977+ );
 978+
 979+ // Test 17
 980+ equal( detaches, 2, 'shift detaches a node' );
 981+
 982+ // Test 18
 983+ strictEqual( node3.getParent(), node1, 'getParent returns the correct reference' );
 984+
 985+ // Test 19
 986+ try {
 987+ var view = node3.createView();
 988+ } catch ( err ){
 989+ ok( true, 'createView throws an exception when not overridden' );
 990+ }
 991+} );

Status & tagging log