A world-class math input for the web
at main 1027 lines 33 kB view raw
1import { expect, test, describe } from "vitest"; 2import { EditorState } from "./editorState"; 3import { Strand } from "./strand"; 4// import { FractionToken, RadicalToken } from "@caret-js/math"; 5import { Cursor } from "./cursor"; 6import { CharToken } from "./tokens/char"; 7import { Token } from "./token"; 8import { VerticalFlow } from "./flows/vertical"; 9import { HorizontalFlow } from "./flows/horizontal"; 10import { t, VNode } from "./vdom"; 11import { SelectionRange } from "./selectionRange"; 12import { Transaction } from "./transaction"; 13 14// Define custom token types here instead of using @caret-js/math for testing 15export class FractionToken extends Token { 16 flow = new VerticalFlow(); 17 numerator: Strand; 18 denominator: Strand; 19 20 constructor(numerator: Strand, denominator: Strand) { 21 super(); 22 this.numerator = numerator; 23 this.denominator = denominator; 24 numerator.parent = this; 25 denominator.parent = this; 26 } 27 28 get children(): readonly Strand[] { 29 return [this.numerator, this.denominator]; 30 } 31 32 mapChildren(fn: (child: Strand, index: number) => Strand): FractionToken { 33 const newNumerator = fn(this.numerator, 0); 34 const newDenominator = fn(this.denominator, 1); 35 return new FractionToken(newNumerator, newDenominator); 36 } 37 38 renderToDebugText = () => ""; 39 renderToDebugHTML = () => t(""); 40 renderToDebugMathML = () => t(""); 41} 42 43export class RadicalToken extends Token { 44 flow = new HorizontalFlow(); 45 index: Strand | null; 46 radicand: Strand; 47 48 constructor(index: Strand | null, radicand: Strand) { 49 super(); 50 this.index = index; 51 this.radicand = radicand; 52 53 if (index) index.parent = this; 54 radicand.parent = this; 55 } 56 57 get children(): readonly Strand[] { 58 return this.index ? [this.index, this.radicand] : [this.radicand]; 59 } 60 61 mapChildren(fn: (child: Strand, index: number) => Strand): RadicalToken { 62 const newIndex = this.index ? fn(this.index, 0) : null; 63 const newRadicand = fn(this.radicand, this.index ? 1 : 0); 64 return new RadicalToken(newIndex, newRadicand); 65 } 66 67 renderToDebugText = () => ""; 68 renderToDebugHTML = () => t(""); 69 renderToDebugMathML = () => t(""); 70} 71 72describe("Get Token Path", () => { 73 test("Find top-level token", () => { 74 const x = new CharToken("x"); 75 const editorState = new EditorState(new Strand([x, new CharToken("y")])); 76 77 const path = editorState.content.findTokenPath(x); 78 expect(path).toEqual({ strandPath: [], tokenIndex: 0 }); 79 }); 80 81 test("Find deeply nested token", () => { 82 const three = new CharToken("3"); 83 const editorState = new EditorState( 84 new Strand([ 85 new CharToken("z"), 86 new CharToken("="), 87 new FractionToken( 88 new Strand([ 89 new RadicalToken( 90 new Strand([three]), 91 new Strand([new CharToken("x")]) 92 ), 93 ]), 94 new Strand([new CharToken("y")]) 95 ), 96 ]) 97 ); 98 99 const path = editorState.content.findTokenPath(three); 100 expect(path).toEqual({ 101 strandPath: [ 102 { tokenIndex: 2, childIndex: 0 }, 103 { tokenIndex: 0, childIndex: 0 }, 104 ], 105 tokenIndex: 0, 106 }); 107 }); 108 109 test("Return null for non-existent node", () => { 110 const editorState = new EditorState(new Strand([new CharToken("x")])); 111 const nonExistentNode = new CharToken("y"); 112 113 const path = editorState.content.findTokenPath(nonExistentNode); 114 expect(path).toBeNull(); 115 }); 116}); 117 118describe("Get Node At Path", () => { 119 test("Get top-level node", () => { 120 const x = new CharToken("x"); 121 const y = new CharToken("y"); 122 const editorState = new EditorState(new Strand([x, y])); 123 124 const node = editorState.content.getToken({ 125 strandPath: [], 126 tokenIndex: 1, 127 }); 128 expect(node).toBe(y); 129 }); 130 131 test("Get deeply nested node", () => { 132 const three = new CharToken("3"); 133 const editorState = new EditorState( 134 new Strand([ 135 new CharToken("z"), 136 new CharToken("="), 137 new FractionToken( 138 new Strand([ 139 new RadicalToken( 140 new Strand([three]), 141 new Strand([new CharToken("x")]) 142 ), 143 ]), 144 new Strand([new CharToken("y")]) 145 ), 146 ]) 147 ); 148 149 const node = editorState.content.getToken({ 150 strandPath: [ 151 { tokenIndex: 2, childIndex: 0 }, 152 { tokenIndex: 0, childIndex: 0 }, 153 ], 154 tokenIndex: 0, 155 }); 156 expect(node).toBe(three); 157 }); 158 159 test("Return null for invalid node path", () => { 160 const editorState = new EditorState(new Strand([new CharToken("x")])); 161 162 const validNode = editorState.content.getToken({ 163 strandPath: [], 164 tokenIndex: 0, 165 }); 166 expect(validNode).not.toBeNull(); 167 168 const invalidNode = editorState.content.getToken({ 169 strandPath: [{ tokenIndex: 5, childIndex: 0 }], 170 tokenIndex: 0, 171 }); 172 expect(invalidNode).toBeNull(); 173 }); 174 175 test("Return null for invalid strand path", () => { 176 const editorState = new EditorState( 177 new Strand([ 178 new FractionToken( 179 new Strand([new CharToken("x")]), 180 new Strand([new CharToken("y")]) 181 ), 182 ]) 183 ); 184 185 const validNode = editorState.content.getToken({ 186 strandPath: [{ tokenIndex: 0, childIndex: 1 }], 187 tokenIndex: 0, 188 }); 189 expect(validNode).not.toBeNull(); 190 191 const invalidNode = editorState.content.getToken({ 192 strandPath: [{ tokenIndex: 0, childIndex: 2 }], 193 tokenIndex: 0, 194 }); 195 expect(invalidNode).toBeNull(); 196 }); 197}); 198 199test("Node Path Finding and Following Are Inverses", () => { 200 const z = new CharToken("z"); 201 const equals = new CharToken("="); 202 const x = new CharToken("x"); 203 const three = new CharToken("3"); 204 const radical = new RadicalToken(new Strand([three]), new Strand([x])); 205 const y = new CharToken("y"); 206 const fraction = new FractionToken(new Strand([radical]), new Strand([y])); 207 const editorState = new EditorState(new Strand([z, equals, fraction])); 208 209 const nodes = [z, equals, fraction, radical, x, three, y]; 210 211 for (const node of nodes) { 212 const path = editorState.content.findTokenPath(node); 213 expect(path).not.toBeNull(); 214 if (path !== null) { 215 const foundNode = editorState.content.getToken(path); 216 expect(foundNode).toBe(node); 217 } 218 } 219}); 220 221describe("Get Strand Path", () => { 222 test("Find top-level strand", () => { 223 const strand = new Strand([new CharToken("x")]); 224 const editorState = new EditorState(strand); 225 226 const path = editorState.content.findStrandPath(strand); 227 expect(path).toEqual([]); 228 }); 229 230 test("Find deeply nested strand", () => { 231 const innerStrand = new Strand([new CharToken("x")]); 232 const editorState = new EditorState( 233 new Strand([ 234 new CharToken("z"), 235 new CharToken("="), 236 new FractionToken( 237 new Strand([ 238 new RadicalToken(new Strand([new CharToken("3")]), innerStrand), 239 ]), 240 new Strand([new CharToken("y")]) 241 ), 242 ]) 243 ); 244 245 const path = editorState.content.findStrandPath(innerStrand); 246 expect(path).toEqual([ 247 { tokenIndex: 2, childIndex: 0 }, 248 { tokenIndex: 0, childIndex: 1 }, 249 ]); 250 }); 251 252 test("Return null for non-existent strand", () => { 253 const editorState = new EditorState(new Strand([new CharToken("x")])); 254 const nonExistentStrand = new Strand([new CharToken("y")]); 255 256 const path = editorState.content.findStrandPath(nonExistentStrand); 257 expect(path).toBeNull(); 258 }); 259}); 260 261describe("Get Strand At Path", () => { 262 test("Get top-level strand", () => { 263 const strand = new Strand([new CharToken("x")]); 264 const editorState = new EditorState(strand); 265 266 const foundStrand = editorState.content.getStrand([]); 267 expect(foundStrand).toBe(strand); 268 }); 269 270 test("Get deeply nested strand", () => { 271 const innerStrand = new Strand([new CharToken("x")]); 272 const editorState = new EditorState( 273 new Strand([ 274 new CharToken("z"), 275 new CharToken("="), 276 new FractionToken( 277 new Strand([ 278 new RadicalToken(new Strand([new CharToken("3")]), innerStrand), 279 ]), 280 new Strand([new CharToken("y")]) 281 ), 282 ]) 283 ); 284 285 const foundStrand = editorState.content.getStrand([ 286 { tokenIndex: 2, childIndex: 0 }, 287 { tokenIndex: 0, childIndex: 1 }, 288 ]); 289 expect(foundStrand).toBe(innerStrand); 290 }); 291 292 test("Return null for invalid strand path (incorrect node index)", () => { 293 const editorState = new EditorState( 294 new Strand([ 295 new FractionToken( 296 new Strand([new CharToken("x")]), 297 new Strand([new CharToken("y")]) 298 ), 299 ]) 300 ); 301 302 const validStrand = editorState.content.getStrand([ 303 { tokenIndex: 0, childIndex: 0 }, 304 ]); 305 expect(validStrand).not.toBeNull(); 306 307 const invalidStrand = editorState.content.getStrand([ 308 { tokenIndex: 1, childIndex: 0 }, 309 ]); 310 expect(invalidStrand).toBeNull(); 311 }); 312 313 test("Return null for invalid strand path (incorrect child index)", () => { 314 const editorState = new EditorState( 315 new Strand([ 316 new FractionToken( 317 new Strand([new CharToken("x")]), 318 new Strand([new CharToken("y")]) 319 ), 320 ]) 321 ); 322 323 const validStrand = editorState.content.getStrand([ 324 { tokenIndex: 0, childIndex: 1 }, 325 ]); 326 expect(validStrand).not.toBeNull(); 327 328 const invalidStrand = editorState.content.getStrand([ 329 { tokenIndex: 0, childIndex: 2 }, 330 ]); 331 expect(invalidStrand).toBeNull(); 332 }); 333}); 334 335test("Strand Path Finding and Following Are Inverses", () => { 336 const indexStrand = new Strand([new CharToken("3")]); 337 const radicandStrand = new Strand([new CharToken("x")]); 338 const numeratorStrand = new Strand([ 339 new RadicalToken(indexStrand, radicandStrand), 340 ]); 341 const denominatorStrand = new Strand([new CharToken("y")]); 342 const topStrand = new Strand([ 343 new CharToken("z"), 344 new CharToken("="), 345 new FractionToken(numeratorStrand, denominatorStrand), 346 ]); 347 348 const editorState = new EditorState(topStrand); 349 350 const strands = [ 351 topStrand, 352 numeratorStrand, 353 denominatorStrand, 354 indexStrand, 355 radicandStrand, 356 ]; 357 358 for (const strand of strands) { 359 const path = editorState.content.findStrandPath(strand); 360 expect(path).not.toBeNull(); 361 if (path !== null) { 362 const foundStrand = editorState.content.getStrand(path); 363 expect(foundStrand).toBe(strand); 364 } 365 } 366}); 367 368test("EditorState is set for all nodes and strands upon construction", () => { 369 const x = new CharToken("x"); 370 const three = new CharToken("3"); 371 const indexStrand = new Strand([three]); 372 const radicandStrand = new Strand([x]); 373 const radical = new RadicalToken(indexStrand, radicandStrand); 374 const numeratorStrand = new Strand([radical]); 375 const y = new CharToken("y"); 376 const denominatorStrand = new Strand([y]); 377 const fraction = new FractionToken(numeratorStrand, denominatorStrand); 378 const z = new CharToken("z"); 379 const equals = new CharToken("="); 380 const topStrand = new Strand([z, equals, fraction]); 381 382 const editorState = new EditorState(topStrand); 383 384 const tokens = [x, three, radical, y, fraction, z, equals]; 385 const strands = [ 386 indexStrand, 387 radicandStrand, 388 numeratorStrand, 389 denominatorStrand, 390 topStrand, 391 ]; 392 393 for (const token of tokens) { 394 expect(token.editorState).toBe(editorState); 395 } 396 397 for (const strand of strands) { 398 expect(strand.editorState).toBe(editorState); 399 } 400}); 401 402describe("Insert At Cursor", () => { 403 test("Inserts character in empty editor", () => { 404 let editorState = new EditorState(new Strand([]), new Cursor([], 0)); 405 editorState = Transaction.insertAtSelection(editorState, [ 406 new CharToken("x"), 407 ]).newState; 408 409 expect(editorState.content.tokens.length).toBe(1); 410 expect((editorState.content.tokens[0] as CharToken).char).toBe("x"); 411 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 412 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 413 }); 414 415 test("Inserts complex node at provided cursor", () => { 416 let editorState = new EditorState( 417 new Strand([new CharToken("a"), new CharToken("b")]), 418 new Cursor([], 0) 419 ); 420 421 const fraction = new FractionToken( 422 new Strand([new CharToken("1")]), 423 new Strand([new CharToken("2")]) 424 ); 425 426 // editorState.insertAtCursor(fraction, new Cursor([], 1)); 427 editorState = Transaction.insertAtSelection( 428 editorState, 429 [fraction], 430 new SelectionRange(new Cursor([], 1)) 431 ).newState; 432 433 expect(editorState.content.tokens.length).toBe(3); 434 expect(editorState.content.tokens[1]).toBeInstanceOf(FractionToken); 435 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); // Cursor should not have moved 436 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); 437 }); 438 439 test("Inserts character in nested strand", () => { 440 // This test turns x/y into x/zy by inserting 'z' at the start of the denominator strand 441 // The cursor is positioned after the y in the denominator and must be adjusted after insertion 442 443 const denominatorStrand = new Strand([new CharToken("y")]); 444 const fraction = new FractionToken( 445 new Strand([new CharToken("x")]), 446 denominatorStrand 447 ); 448 let editorState = new EditorState( 449 new Strand([fraction]), 450 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 451 ); 452 453 editorState = Transaction.insertAtSelection( 454 editorState, 455 [new CharToken("z")], 456 new SelectionRange(new Cursor([{ tokenIndex: 0, childIndex: 1 }], 0)) 457 ).newState; 458 459 const newDenominatorStrand = ( 460 editorState.content.tokens[0] as FractionToken 461 ).denominator; 462 463 expect(newDenominatorStrand.tokens.length).toBe(2); 464 expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("z"); 465 expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("y"); 466 expect(editorState.selection.anchor).toEqual( 467 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) 468 ); // Cursor should have moved forward to account for the new character 469 expect(editorState.selection.head).toEqual( 470 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) 471 ); 472 }); 473}); 474 475describe("Delete At Cursor", () => { 476 test("Deletes character before cursor in top-level strand", () => { 477 let editorState = new EditorState( 478 new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), 479 new Cursor([], 2) 480 ); 481 482 editorState = Transaction.deleteAtSelection( 483 editorState, 484 "backward" 485 ).newState; 486 487 expect(editorState.content.tokens.length).toBe(2); 488 expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); 489 expect((editorState.content.tokens[1] as CharToken).char).toBe("c"); 490 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 491 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 492 }); 493 494 test("Deletes character before cursor in nested strand", () => { 495 const denominatorStrand = new Strand([ 496 new CharToken("x"), 497 new CharToken("y"), 498 new CharToken("z"), 499 ]); 500 const fraction = new FractionToken( 501 new Strand([new CharToken("1")]), 502 denominatorStrand 503 ); 504 let editorState = new EditorState( 505 new Strand([fraction]), 506 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) 507 ); 508 509 editorState = Transaction.deleteAtSelection( 510 editorState, 511 "backward" 512 ).newState; 513 514 const newDenominatorStrand = ( 515 editorState.content.tokens[0] as FractionToken 516 ).denominator; 517 518 expect(newDenominatorStrand.tokens.length).toBe(2); 519 expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("x"); 520 expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("z"); 521 expect(editorState.selection.anchor).toEqual( 522 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 523 ); 524 expect(editorState.selection.head).toEqual( 525 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 526 ); 527 }); 528 529 test("No deletion when cursor at start of top-level strand", () => { 530 let editorState = new EditorState( 531 new Strand([new CharToken("a"), new CharToken("b")]), 532 new Cursor([], 0) 533 ); 534 535 editorState = Transaction.deleteAtSelection( 536 editorState, 537 "backward" 538 ).newState; 539 540 expect(editorState.content.tokens.length).toBe(2); 541 expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); 542 expect((editorState.content.tokens[1] as CharToken).char).toBe("b"); 543 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); 544 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); 545 }); 546 547 test("Flattens token when cursor at start of nested strand", () => { 548 const fraction = new FractionToken( 549 new Strand([new CharToken("1")]), 550 new Strand([new CharToken("x"), new CharToken("y")]) 551 ); 552 let editorState = new EditorState( 553 new Strand([fraction]), 554 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 0) 555 ); 556 557 editorState = Transaction.deleteAtSelection( 558 editorState, 559 "backward" 560 ).newState; 561 562 expect(editorState.content.tokens.length).toBe(3); 563 expect((editorState.content.tokens[0] as CharToken).char).toBe("1"); 564 expect((editorState.content.tokens[1] as CharToken).char).toBe("x"); 565 expect((editorState.content.tokens[2] as CharToken).char).toBe("y"); 566 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 567 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 568 }); 569 570 test("Deletes character after cursor in top-level strand", () => { 571 let editorState = new EditorState( 572 new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), 573 new Cursor([], 1) 574 ); 575 576 editorState = Transaction.deleteAtSelection( 577 editorState, 578 "forward" 579 ).newState; 580 581 expect(editorState.content.tokens.length).toBe(2); 582 expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); 583 expect((editorState.content.tokens[1] as CharToken).char).toBe("c"); 584 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 585 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 586 }); 587 588 test("Deletes character after cursor in nested strand", () => { 589 const denominatorStrand = new Strand([ 590 new CharToken("x"), 591 new CharToken("y"), 592 new CharToken("z"), 593 ]); 594 const fraction = new FractionToken( 595 new Strand([new CharToken("1")]), 596 denominatorStrand 597 ); 598 let editorState = new EditorState( 599 new Strand([fraction]), 600 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 601 ); 602 603 editorState = Transaction.deleteAtSelection( 604 editorState, 605 "forward" 606 ).newState; 607 608 const newDenominatorStrand = ( 609 editorState.content.tokens[0] as FractionToken 610 ).denominator; 611 612 expect(newDenominatorStrand.tokens.length).toBe(2); 613 expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("x"); 614 expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("z"); 615 expect(editorState.selection.anchor).toEqual( 616 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 617 ); 618 expect(editorState.selection.head).toEqual( 619 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 620 ); 621 }); 622 623 test("No deletion when cursor at end of top-level strand", () => { 624 let editorState = new EditorState( 625 new Strand([new CharToken("a"), new CharToken("b")]), 626 new Cursor([], 2) 627 ); 628 629 editorState = Transaction.deleteAtSelection( 630 editorState, 631 "forward" 632 ).newState; 633 634 expect(editorState.content.tokens.length).toBe(2); 635 expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); 636 expect((editorState.content.tokens[1] as CharToken).char).toBe("b"); 637 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); 638 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); 639 }); 640 641 test("Flattens token when cursor at end of nested strand", () => { 642 const denominatorStrand = new Strand([ 643 new CharToken("x"), 644 new CharToken("y"), 645 ]); 646 const fraction = new FractionToken( 647 new Strand([new CharToken("1")]), 648 denominatorStrand 649 ); 650 let editorState = new EditorState( 651 new Strand([fraction]), 652 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) 653 ); 654 655 editorState = Transaction.deleteAtSelection( 656 editorState, 657 "forward" 658 ).newState; 659 660 expect(editorState.content.tokens.length).toBe(3); 661 expect((editorState.content.tokens[0] as CharToken).char).toBe("1"); 662 expect((editorState.content.tokens[1] as CharToken).char).toBe("x"); 663 expect((editorState.content.tokens[2] as CharToken).char).toBe("y"); 664 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 3 }); 665 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 3 }); 666 }); 667 668 test("Deletes range in top-level strand", () => { 669 let editorState = new EditorState( 670 new Strand([ 671 new CharToken("a"), 672 new CharToken("b"), 673 new CharToken("c"), 674 new CharToken("d"), 675 ]), 676 new SelectionRange(new Cursor([], 1), new Cursor([], 3)) 677 ); 678 679 editorState = Transaction.deleteAtSelection(editorState).newState; 680 681 expect(editorState.content.tokens.length).toBe(2); 682 expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); 683 expect((editorState.content.tokens[1] as CharToken).char).toBe("d"); 684 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 685 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 686 }); 687 688 test("Deletes range in nested strand", () => { 689 const denominatorStrand = new Strand([ 690 new CharToken("w"), 691 new CharToken("x"), 692 new CharToken("y"), 693 new CharToken("z"), 694 ]); 695 const fraction = new FractionToken( 696 new Strand([new CharToken("1")]), 697 denominatorStrand 698 ); 699 let editorState = new EditorState( 700 new Strand([fraction]), 701 new SelectionRange( 702 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1), 703 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 3) 704 ) 705 ); 706 707 editorState = Transaction.deleteAtSelection(editorState).newState; 708 709 const newDenominatorStrand = ( 710 editorState.content.tokens[0] as FractionToken 711 ).denominator; 712 713 expect(newDenominatorStrand.tokens.length).toBe(2); 714 expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("w"); 715 expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("z"); 716 expect(editorState.selection.anchor).toEqual( 717 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 718 ); 719 expect(editorState.selection.head).toEqual( 720 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 721 ); 722 }); 723 724 test("Deletes range across children of one token", () => { 725 const numeratorStrand = new Strand([ 726 new CharToken("a"), 727 new CharToken("b"), 728 ]); 729 const denominatorStrand = new Strand([ 730 new CharToken("x"), 731 new CharToken("y"), 732 ]); 733 let editorState = new EditorState( 734 new Strand([ 735 new CharToken("y"), 736 new CharToken("="), 737 new FractionToken(numeratorStrand, denominatorStrand), 738 ]), 739 new SelectionRange( 740 new Cursor([{ tokenIndex: 2, childIndex: 0 }], 1), 741 new Cursor([{ tokenIndex: 2, childIndex: 1 }], 1) 742 ) 743 ); 744 745 editorState = Transaction.deleteAtSelection(editorState).newState; 746 747 expect(editorState.content.tokens.length).toBe(2); 748 expect((editorState.content.tokens[0] as CharToken).char).toBe("y"); 749 expect((editorState.content.tokens[1] as CharToken).char).toBe("="); 750 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); 751 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); 752 }); 753 754 test("Deletes range from top strand into nested strand", () => { 755 let editorState = new EditorState( 756 new Strand([ 757 new CharToken("z"), 758 new CharToken("="), 759 new FractionToken( 760 new Strand([new CharToken("1")]), 761 new Strand([new CharToken("x"), new CharToken("y")]) 762 ), 763 new CharToken("+"), 764 new CharToken("2"), 765 ]), 766 new SelectionRange( 767 new Cursor([], 1), 768 new Cursor([{ tokenIndex: 2, childIndex: 1 }], 1) 769 ) 770 ); 771 772 editorState = Transaction.deleteAtSelection(editorState).newState; 773 774 expect(editorState.content.tokens.length).toBe(3); 775 expect((editorState.content.tokens[0] as CharToken).char).toBe("z"); 776 expect((editorState.content.tokens[1] as CharToken).char).toBe("+"); 777 expect((editorState.content.tokens[2] as CharToken).char).toBe("2"); 778 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 779 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 780 }); 781}); 782 783describe("Cursor Movement Left/Right", () => { 784 test("Move Cursor Right", () => { 785 let editorState = new EditorState( 786 new Strand([new CharToken("x")]), 787 new Cursor([], 0) 788 ); 789 790 editorState = Transaction.moveCursor(editorState, "right").newState; 791 792 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 793 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 794 }); 795 796 test("Move Cursor Right into Child", () => { 797 let editorState = new EditorState( 798 new Strand([ 799 new FractionToken( 800 new Strand([new CharToken("x")]), 801 new Strand([new CharToken("y")]) 802 ), 803 ]), 804 new Cursor([], 0) 805 ); 806 807 editorState = Transaction.moveCursor(editorState, "right").newState; 808 809 expect(editorState.selection.anchor).toEqual({ 810 strandPath: [{ tokenIndex: 0, childIndex: 0 }], 811 pos: 0, 812 }); 813 expect(editorState.selection.head).toEqual({ 814 strandPath: [{ tokenIndex: 0, childIndex: 0 }], 815 pos: 0, 816 }); 817 }); 818 819 test("Move Cursor Right into Sibling", () => { 820 let editorState = new EditorState( 821 new Strand([ 822 new FractionToken( 823 new Strand([new CharToken("x")]), 824 new Strand([new CharToken("y")]) 825 ), 826 new CharToken("+"), 827 ]), 828 new Cursor([{ tokenIndex: 0, childIndex: 0 }], 1) 829 ); 830 831 editorState = Transaction.moveCursor(editorState, "right").newState; 832 833 expect(editorState.selection.anchor).toEqual({ 834 strandPath: [{ tokenIndex: 0, childIndex: 1 }], 835 pos: 0, 836 }); 837 expect(editorState.selection.head).toEqual({ 838 strandPath: [{ tokenIndex: 0, childIndex: 1 }], 839 pos: 0, 840 }); 841 }); 842 843 test("Move Cursor Right out of Child", () => { 844 let editorState = new EditorState( 845 new Strand([ 846 new FractionToken( 847 new Strand([new CharToken("x")]), 848 new Strand([new CharToken("y")]) 849 ), 850 ]), 851 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) 852 ); 853 854 editorState = Transaction.moveCursor(editorState, "right").newState; 855 856 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 857 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 858 }); 859 860 test("Move Cursor Right at End of Editor", () => { 861 let editorState = new EditorState( 862 new Strand([new CharToken("x")]), 863 new Cursor([], 1) 864 ); 865 866 editorState = Transaction.moveCursor(editorState, "right").newState; 867 868 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 869 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 870 }); 871 872 test("Move Cursor Left", () => { 873 let editorState = new EditorState( 874 new Strand([new CharToken("x")]), 875 new Cursor([], 1) 876 ); 877 878 editorState = Transaction.moveCursor(editorState, "left").newState; 879 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); 880 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); 881 }); 882 883 test("Move Cursor Left into Child", () => { 884 let editorState = new EditorState( 885 new Strand([ 886 new FractionToken( 887 new Strand([new CharToken("x")]), 888 new Strand([new CharToken("y")]) 889 ), 890 ]), 891 new Cursor([], 1) 892 ); 893 894 editorState = Transaction.moveCursor(editorState, "left").newState; 895 896 expect(editorState.selection.anchor).toEqual({ 897 strandPath: [{ tokenIndex: 0, childIndex: 1 }], 898 pos: 1, 899 }); 900 expect(editorState.selection.head).toEqual({ 901 strandPath: [{ tokenIndex: 0, childIndex: 1 }], 902 pos: 1, 903 }); 904 }); 905 906 test("Move Cursor Left into Sibling", () => { 907 let editorState = new EditorState( 908 new Strand([ 909 new FractionToken( 910 new Strand([new CharToken("x")]), 911 new Strand([new CharToken("y")]) 912 ), 913 ]), 914 new Cursor([{ tokenIndex: 0, childIndex: 1 }], 0) 915 ); 916 917 editorState = Transaction.moveCursor(editorState, "left").newState; 918 919 expect(editorState.selection.anchor).toEqual({ 920 strandPath: [{ tokenIndex: 0, childIndex: 0 }], 921 pos: 1, 922 }); 923 expect(editorState.selection.head).toEqual({ 924 strandPath: [{ tokenIndex: 0, childIndex: 0 }], 925 pos: 1, 926 }); 927 }); 928 929 test("Move Cursor Left out of Child", () => { 930 let editorState = new EditorState( 931 new Strand([ 932 new FractionToken( 933 new Strand([new CharToken("x")]), 934 new Strand([new CharToken("y")]) 935 ), 936 ]), 937 new Cursor([{ tokenIndex: 0, childIndex: 0 }], 0) 938 ); 939 940 editorState = Transaction.moveCursor(editorState, "left").newState; 941 942 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); 943 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); 944 }); 945 946 test("Move Cursor Left at Start of Editor", () => { 947 let editorState = new EditorState( 948 new Strand([new CharToken("x")]), 949 new Cursor([], 0) 950 ); 951 952 editorState = Transaction.moveCursor(editorState, "left").newState; 953 954 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); 955 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); 956 }); 957 958 test("Collapse selection to right", () => { 959 let editorState = new EditorState( 960 new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), 961 new SelectionRange(new Cursor([], 0), new Cursor([], 2)) 962 ); 963 964 editorState = Transaction.moveCursor(editorState, "right").newState; 965 966 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); 967 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); 968 }); 969 970 test("Collapse selection to right in reverse selection", () => { 971 let editorState = new EditorState( 972 new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), 973 new SelectionRange(new Cursor([], 2), new Cursor([], 0)) 974 ); 975 976 editorState = Transaction.moveCursor(editorState, "right").newState; 977 978 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); 979 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); 980 }); 981 982 test("Collapse selection to left", () => { 983 let editorState = new EditorState( 984 new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), 985 new SelectionRange(new Cursor([], 1), new Cursor([], 3)) 986 ); 987 988 editorState = Transaction.moveCursor(editorState, "left").newState; 989 990 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 991 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 992 }); 993 994 test("Collapse selection to left in reverse selection", () => { 995 let editorState = new EditorState( 996 new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), 997 new SelectionRange(new Cursor([], 3), new Cursor([], 1)) 998 ); 999 1000 editorState = Transaction.moveCursor(editorState, "left").newState; 1001 1002 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); 1003 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); 1004 }); 1005 1006 test("Collapse selection to right in common ancestor strand", () => { 1007 let editorState = new EditorState( 1008 new Strand([ 1009 new CharToken("y"), 1010 new CharToken("="), 1011 new FractionToken( 1012 new Strand([new CharToken("a"), new CharToken("b")]), 1013 new Strand([new CharToken("x"), new CharToken("y")]) 1014 ), 1015 ]), 1016 new SelectionRange( 1017 new Cursor([], 1), 1018 new Cursor([{ tokenIndex: 2, childIndex: 1 }], 1) 1019 ) 1020 ); 1021 1022 editorState = Transaction.moveCursor(editorState, "right").newState; 1023 1024 expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 3 }); 1025 expect(editorState.selection.head).toEqual({ strandPath: [], pos: 3 }); 1026 }); 1027});