import { expect, test, describe } from "vitest"; import { EditorState } from "./editorState"; import { Strand } from "./strand"; // import { FractionToken, RadicalToken } from "@caret-js/math"; import { Cursor } from "./cursor"; import { CharToken } from "./tokens/char"; import { Token } from "./token"; import { VerticalFlow } from "./flows/vertical"; import { HorizontalFlow } from "./flows/horizontal"; import { t, VNode } from "./vdom"; import { SelectionRange } from "./selectionRange"; import { Transaction } from "./transaction"; // Define custom token types here instead of using @caret-js/math for testing export class FractionToken extends Token { flow = new VerticalFlow(); numerator: Strand; denominator: Strand; constructor(numerator: Strand, denominator: Strand) { super(); this.numerator = numerator; this.denominator = denominator; numerator.parent = this; denominator.parent = this; } get children(): readonly Strand[] { return [this.numerator, this.denominator]; } mapChildren(fn: (child: Strand, index: number) => Strand): FractionToken { const newNumerator = fn(this.numerator, 0); const newDenominator = fn(this.denominator, 1); return new FractionToken(newNumerator, newDenominator); } renderToDebugText = () => ""; renderToDebugHTML = () => t(""); renderToDebugMathML = () => t(""); } export class RadicalToken extends Token { flow = new HorizontalFlow(); index: Strand | null; radicand: Strand; constructor(index: Strand | null, radicand: Strand) { super(); this.index = index; this.radicand = radicand; if (index) index.parent = this; radicand.parent = this; } get children(): readonly Strand[] { return this.index ? [this.index, this.radicand] : [this.radicand]; } mapChildren(fn: (child: Strand, index: number) => Strand): RadicalToken { const newIndex = this.index ? fn(this.index, 0) : null; const newRadicand = fn(this.radicand, this.index ? 1 : 0); return new RadicalToken(newIndex, newRadicand); } renderToDebugText = () => ""; renderToDebugHTML = () => t(""); renderToDebugMathML = () => t(""); } describe("Get Token Path", () => { test("Find top-level token", () => { const x = new CharToken("x"); const editorState = new EditorState(new Strand([x, new CharToken("y")])); const path = editorState.content.findTokenPath(x); expect(path).toEqual({ strandPath: [], tokenIndex: 0 }); }); test("Find deeply nested token", () => { const three = new CharToken("3"); const editorState = new EditorState( new Strand([ new CharToken("z"), new CharToken("="), new FractionToken( new Strand([ new RadicalToken( new Strand([three]), new Strand([new CharToken("x")]) ), ]), new Strand([new CharToken("y")]) ), ]) ); const path = editorState.content.findTokenPath(three); expect(path).toEqual({ strandPath: [ { tokenIndex: 2, childIndex: 0 }, { tokenIndex: 0, childIndex: 0 }, ], tokenIndex: 0, }); }); test("Return null for non-existent node", () => { const editorState = new EditorState(new Strand([new CharToken("x")])); const nonExistentNode = new CharToken("y"); const path = editorState.content.findTokenPath(nonExistentNode); expect(path).toBeNull(); }); }); describe("Get Node At Path", () => { test("Get top-level node", () => { const x = new CharToken("x"); const y = new CharToken("y"); const editorState = new EditorState(new Strand([x, y])); const node = editorState.content.getToken({ strandPath: [], tokenIndex: 1, }); expect(node).toBe(y); }); test("Get deeply nested node", () => { const three = new CharToken("3"); const editorState = new EditorState( new Strand([ new CharToken("z"), new CharToken("="), new FractionToken( new Strand([ new RadicalToken( new Strand([three]), new Strand([new CharToken("x")]) ), ]), new Strand([new CharToken("y")]) ), ]) ); const node = editorState.content.getToken({ strandPath: [ { tokenIndex: 2, childIndex: 0 }, { tokenIndex: 0, childIndex: 0 }, ], tokenIndex: 0, }); expect(node).toBe(three); }); test("Return null for invalid node path", () => { const editorState = new EditorState(new Strand([new CharToken("x")])); const validNode = editorState.content.getToken({ strandPath: [], tokenIndex: 0, }); expect(validNode).not.toBeNull(); const invalidNode = editorState.content.getToken({ strandPath: [{ tokenIndex: 5, childIndex: 0 }], tokenIndex: 0, }); expect(invalidNode).toBeNull(); }); test("Return null for invalid strand path", () => { const editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]) ); const validNode = editorState.content.getToken({ strandPath: [{ tokenIndex: 0, childIndex: 1 }], tokenIndex: 0, }); expect(validNode).not.toBeNull(); const invalidNode = editorState.content.getToken({ strandPath: [{ tokenIndex: 0, childIndex: 2 }], tokenIndex: 0, }); expect(invalidNode).toBeNull(); }); }); test("Node Path Finding and Following Are Inverses", () => { const z = new CharToken("z"); const equals = new CharToken("="); const x = new CharToken("x"); const three = new CharToken("3"); const radical = new RadicalToken(new Strand([three]), new Strand([x])); const y = new CharToken("y"); const fraction = new FractionToken(new Strand([radical]), new Strand([y])); const editorState = new EditorState(new Strand([z, equals, fraction])); const nodes = [z, equals, fraction, radical, x, three, y]; for (const node of nodes) { const path = editorState.content.findTokenPath(node); expect(path).not.toBeNull(); if (path !== null) { const foundNode = editorState.content.getToken(path); expect(foundNode).toBe(node); } } }); describe("Get Strand Path", () => { test("Find top-level strand", () => { const strand = new Strand([new CharToken("x")]); const editorState = new EditorState(strand); const path = editorState.content.findStrandPath(strand); expect(path).toEqual([]); }); test("Find deeply nested strand", () => { const innerStrand = new Strand([new CharToken("x")]); const editorState = new EditorState( new Strand([ new CharToken("z"), new CharToken("="), new FractionToken( new Strand([ new RadicalToken(new Strand([new CharToken("3")]), innerStrand), ]), new Strand([new CharToken("y")]) ), ]) ); const path = editorState.content.findStrandPath(innerStrand); expect(path).toEqual([ { tokenIndex: 2, childIndex: 0 }, { tokenIndex: 0, childIndex: 1 }, ]); }); test("Return null for non-existent strand", () => { const editorState = new EditorState(new Strand([new CharToken("x")])); const nonExistentStrand = new Strand([new CharToken("y")]); const path = editorState.content.findStrandPath(nonExistentStrand); expect(path).toBeNull(); }); }); describe("Get Strand At Path", () => { test("Get top-level strand", () => { const strand = new Strand([new CharToken("x")]); const editorState = new EditorState(strand); const foundStrand = editorState.content.getStrand([]); expect(foundStrand).toBe(strand); }); test("Get deeply nested strand", () => { const innerStrand = new Strand([new CharToken("x")]); const editorState = new EditorState( new Strand([ new CharToken("z"), new CharToken("="), new FractionToken( new Strand([ new RadicalToken(new Strand([new CharToken("3")]), innerStrand), ]), new Strand([new CharToken("y")]) ), ]) ); const foundStrand = editorState.content.getStrand([ { tokenIndex: 2, childIndex: 0 }, { tokenIndex: 0, childIndex: 1 }, ]); expect(foundStrand).toBe(innerStrand); }); test("Return null for invalid strand path (incorrect node index)", () => { const editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]) ); const validStrand = editorState.content.getStrand([ { tokenIndex: 0, childIndex: 0 }, ]); expect(validStrand).not.toBeNull(); const invalidStrand = editorState.content.getStrand([ { tokenIndex: 1, childIndex: 0 }, ]); expect(invalidStrand).toBeNull(); }); test("Return null for invalid strand path (incorrect child index)", () => { const editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]) ); const validStrand = editorState.content.getStrand([ { tokenIndex: 0, childIndex: 1 }, ]); expect(validStrand).not.toBeNull(); const invalidStrand = editorState.content.getStrand([ { tokenIndex: 0, childIndex: 2 }, ]); expect(invalidStrand).toBeNull(); }); }); test("Strand Path Finding and Following Are Inverses", () => { const indexStrand = new Strand([new CharToken("3")]); const radicandStrand = new Strand([new CharToken("x")]); const numeratorStrand = new Strand([ new RadicalToken(indexStrand, radicandStrand), ]); const denominatorStrand = new Strand([new CharToken("y")]); const topStrand = new Strand([ new CharToken("z"), new CharToken("="), new FractionToken(numeratorStrand, denominatorStrand), ]); const editorState = new EditorState(topStrand); const strands = [ topStrand, numeratorStrand, denominatorStrand, indexStrand, radicandStrand, ]; for (const strand of strands) { const path = editorState.content.findStrandPath(strand); expect(path).not.toBeNull(); if (path !== null) { const foundStrand = editorState.content.getStrand(path); expect(foundStrand).toBe(strand); } } }); test("EditorState is set for all nodes and strands upon construction", () => { const x = new CharToken("x"); const three = new CharToken("3"); const indexStrand = new Strand([three]); const radicandStrand = new Strand([x]); const radical = new RadicalToken(indexStrand, radicandStrand); const numeratorStrand = new Strand([radical]); const y = new CharToken("y"); const denominatorStrand = new Strand([y]); const fraction = new FractionToken(numeratorStrand, denominatorStrand); const z = new CharToken("z"); const equals = new CharToken("="); const topStrand = new Strand([z, equals, fraction]); const editorState = new EditorState(topStrand); const tokens = [x, three, radical, y, fraction, z, equals]; const strands = [ indexStrand, radicandStrand, numeratorStrand, denominatorStrand, topStrand, ]; for (const token of tokens) { expect(token.editorState).toBe(editorState); } for (const strand of strands) { expect(strand.editorState).toBe(editorState); } }); describe("Insert At Cursor", () => { test("Inserts character in empty editor", () => { let editorState = new EditorState(new Strand([]), new Cursor([], 0)); editorState = Transaction.insertAtSelection(editorState, [ new CharToken("x"), ]).newState; expect(editorState.content.tokens.length).toBe(1); expect((editorState.content.tokens[0] as CharToken).char).toBe("x"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Inserts complex node at provided cursor", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b")]), new Cursor([], 0) ); const fraction = new FractionToken( new Strand([new CharToken("1")]), new Strand([new CharToken("2")]) ); // editorState.insertAtCursor(fraction, new Cursor([], 1)); editorState = Transaction.insertAtSelection( editorState, [fraction], new SelectionRange(new Cursor([], 1)) ).newState; expect(editorState.content.tokens.length).toBe(3); expect(editorState.content.tokens[1]).toBeInstanceOf(FractionToken); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); // Cursor should not have moved expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); }); test("Inserts character in nested strand", () => { // This test turns x/y into x/zy by inserting 'z' at the start of the denominator strand // The cursor is positioned after the y in the denominator and must be adjusted after insertion const denominatorStrand = new Strand([new CharToken("y")]); const fraction = new FractionToken( new Strand([new CharToken("x")]), denominatorStrand ); let editorState = new EditorState( new Strand([fraction]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); editorState = Transaction.insertAtSelection( editorState, [new CharToken("z")], new SelectionRange(new Cursor([{ tokenIndex: 0, childIndex: 1 }], 0)) ).newState; const newDenominatorStrand = ( editorState.content.tokens[0] as FractionToken ).denominator; expect(newDenominatorStrand.tokens.length).toBe(2); expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("z"); expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("y"); expect(editorState.selection.anchor).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) ); // Cursor should have moved forward to account for the new character expect(editorState.selection.head).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) ); }); }); describe("Delete At Cursor", () => { test("Deletes character before cursor in top-level strand", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), new Cursor([], 2) ); editorState = Transaction.deleteAtSelection( editorState, "backward" ).newState; expect(editorState.content.tokens.length).toBe(2); expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); expect((editorState.content.tokens[1] as CharToken).char).toBe("c"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Deletes character before cursor in nested strand", () => { const denominatorStrand = new Strand([ new CharToken("x"), new CharToken("y"), new CharToken("z"), ]); const fraction = new FractionToken( new Strand([new CharToken("1")]), denominatorStrand ); let editorState = new EditorState( new Strand([fraction]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) ); editorState = Transaction.deleteAtSelection( editorState, "backward" ).newState; const newDenominatorStrand = ( editorState.content.tokens[0] as FractionToken ).denominator; expect(newDenominatorStrand.tokens.length).toBe(2); expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("x"); expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("z"); expect(editorState.selection.anchor).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); expect(editorState.selection.head).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); }); test("No deletion when cursor at start of top-level strand", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b")]), new Cursor([], 0) ); editorState = Transaction.deleteAtSelection( editorState, "backward" ).newState; expect(editorState.content.tokens.length).toBe(2); expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); expect((editorState.content.tokens[1] as CharToken).char).toBe("b"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); }); test("Flattens token when cursor at start of nested strand", () => { const fraction = new FractionToken( new Strand([new CharToken("1")]), new Strand([new CharToken("x"), new CharToken("y")]) ); let editorState = new EditorState( new Strand([fraction]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 0) ); editorState = Transaction.deleteAtSelection( editorState, "backward" ).newState; expect(editorState.content.tokens.length).toBe(3); expect((editorState.content.tokens[0] as CharToken).char).toBe("1"); expect((editorState.content.tokens[1] as CharToken).char).toBe("x"); expect((editorState.content.tokens[2] as CharToken).char).toBe("y"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Deletes character after cursor in top-level strand", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), new Cursor([], 1) ); editorState = Transaction.deleteAtSelection( editorState, "forward" ).newState; expect(editorState.content.tokens.length).toBe(2); expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); expect((editorState.content.tokens[1] as CharToken).char).toBe("c"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Deletes character after cursor in nested strand", () => { const denominatorStrand = new Strand([ new CharToken("x"), new CharToken("y"), new CharToken("z"), ]); const fraction = new FractionToken( new Strand([new CharToken("1")]), denominatorStrand ); let editorState = new EditorState( new Strand([fraction]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); editorState = Transaction.deleteAtSelection( editorState, "forward" ).newState; const newDenominatorStrand = ( editorState.content.tokens[0] as FractionToken ).denominator; expect(newDenominatorStrand.tokens.length).toBe(2); expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("x"); expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("z"); expect(editorState.selection.anchor).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); expect(editorState.selection.head).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); }); test("No deletion when cursor at end of top-level strand", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b")]), new Cursor([], 2) ); editorState = Transaction.deleteAtSelection( editorState, "forward" ).newState; expect(editorState.content.tokens.length).toBe(2); expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); expect((editorState.content.tokens[1] as CharToken).char).toBe("b"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); }); test("Flattens token when cursor at end of nested strand", () => { const denominatorStrand = new Strand([ new CharToken("x"), new CharToken("y"), ]); const fraction = new FractionToken( new Strand([new CharToken("1")]), denominatorStrand ); let editorState = new EditorState( new Strand([fraction]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 2) ); editorState = Transaction.deleteAtSelection( editorState, "forward" ).newState; expect(editorState.content.tokens.length).toBe(3); expect((editorState.content.tokens[0] as CharToken).char).toBe("1"); expect((editorState.content.tokens[1] as CharToken).char).toBe("x"); expect((editorState.content.tokens[2] as CharToken).char).toBe("y"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 3 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 3 }); }); test("Deletes range in top-level strand", () => { let editorState = new EditorState( new Strand([ new CharToken("a"), new CharToken("b"), new CharToken("c"), new CharToken("d"), ]), new SelectionRange(new Cursor([], 1), new Cursor([], 3)) ); editorState = Transaction.deleteAtSelection(editorState).newState; expect(editorState.content.tokens.length).toBe(2); expect((editorState.content.tokens[0] as CharToken).char).toBe("a"); expect((editorState.content.tokens[1] as CharToken).char).toBe("d"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Deletes range in nested strand", () => { const denominatorStrand = new Strand([ new CharToken("w"), new CharToken("x"), new CharToken("y"), new CharToken("z"), ]); const fraction = new FractionToken( new Strand([new CharToken("1")]), denominatorStrand ); let editorState = new EditorState( new Strand([fraction]), new SelectionRange( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 3) ) ); editorState = Transaction.deleteAtSelection(editorState).newState; const newDenominatorStrand = ( editorState.content.tokens[0] as FractionToken ).denominator; expect(newDenominatorStrand.tokens.length).toBe(2); expect((newDenominatorStrand.tokens[0] as CharToken).char).toBe("w"); expect((newDenominatorStrand.tokens[1] as CharToken).char).toBe("z"); expect(editorState.selection.anchor).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); expect(editorState.selection.head).toEqual( new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); }); test("Deletes range across children of one token", () => { const numeratorStrand = new Strand([ new CharToken("a"), new CharToken("b"), ]); const denominatorStrand = new Strand([ new CharToken("x"), new CharToken("y"), ]); let editorState = new EditorState( new Strand([ new CharToken("y"), new CharToken("="), new FractionToken(numeratorStrand, denominatorStrand), ]), new SelectionRange( new Cursor([{ tokenIndex: 2, childIndex: 0 }], 1), new Cursor([{ tokenIndex: 2, childIndex: 1 }], 1) ) ); editorState = Transaction.deleteAtSelection(editorState).newState; expect(editorState.content.tokens.length).toBe(2); expect((editorState.content.tokens[0] as CharToken).char).toBe("y"); expect((editorState.content.tokens[1] as CharToken).char).toBe("="); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); }); test("Deletes range from top strand into nested strand", () => { let editorState = new EditorState( new Strand([ new CharToken("z"), new CharToken("="), new FractionToken( new Strand([new CharToken("1")]), new Strand([new CharToken("x"), new CharToken("y")]) ), new CharToken("+"), new CharToken("2"), ]), new SelectionRange( new Cursor([], 1), new Cursor([{ tokenIndex: 2, childIndex: 1 }], 1) ) ); editorState = Transaction.deleteAtSelection(editorState).newState; expect(editorState.content.tokens.length).toBe(3); expect((editorState.content.tokens[0] as CharToken).char).toBe("z"); expect((editorState.content.tokens[1] as CharToken).char).toBe("+"); expect((editorState.content.tokens[2] as CharToken).char).toBe("2"); expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); }); describe("Cursor Movement Left/Right", () => { test("Move Cursor Right", () => { let editorState = new EditorState( new Strand([new CharToken("x")]), new Cursor([], 0) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Move Cursor Right into Child", () => { let editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]), new Cursor([], 0) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 0 }], pos: 0, }); expect(editorState.selection.head).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 0 }], pos: 0, }); }); test("Move Cursor Right into Sibling", () => { let editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), new CharToken("+"), ]), new Cursor([{ tokenIndex: 0, childIndex: 0 }], 1) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 1 }], pos: 0, }); expect(editorState.selection.head).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 1 }], pos: 0, }); }); test("Move Cursor Right out of Child", () => { let editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 1) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Move Cursor Right at End of Editor", () => { let editorState = new EditorState( new Strand([new CharToken("x")]), new Cursor([], 1) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Move Cursor Left", () => { let editorState = new EditorState( new Strand([new CharToken("x")]), new Cursor([], 1) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); }); test("Move Cursor Left into Child", () => { let editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]), new Cursor([], 1) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 1 }], pos: 1, }); expect(editorState.selection.head).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 1 }], pos: 1, }); }); test("Move Cursor Left into Sibling", () => { let editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]), new Cursor([{ tokenIndex: 0, childIndex: 1 }], 0) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 0 }], pos: 1, }); expect(editorState.selection.head).toEqual({ strandPath: [{ tokenIndex: 0, childIndex: 0 }], pos: 1, }); }); test("Move Cursor Left out of Child", () => { let editorState = new EditorState( new Strand([ new FractionToken( new Strand([new CharToken("x")]), new Strand([new CharToken("y")]) ), ]), new Cursor([{ tokenIndex: 0, childIndex: 0 }], 0) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); }); test("Move Cursor Left at Start of Editor", () => { let editorState = new EditorState( new Strand([new CharToken("x")]), new Cursor([], 0) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 0 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 0 }); }); test("Collapse selection to right", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), new SelectionRange(new Cursor([], 0), new Cursor([], 2)) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); }); test("Collapse selection to right in reverse selection", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), new SelectionRange(new Cursor([], 2), new Cursor([], 0)) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 2 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 2 }); }); test("Collapse selection to left", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), new SelectionRange(new Cursor([], 1), new Cursor([], 3)) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Collapse selection to left in reverse selection", () => { let editorState = new EditorState( new Strand([new CharToken("a"), new CharToken("b"), new CharToken("c")]), new SelectionRange(new Cursor([], 3), new Cursor([], 1)) ); editorState = Transaction.moveCursor(editorState, "left").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 1 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 1 }); }); test("Collapse selection to right in common ancestor strand", () => { let editorState = new EditorState( new Strand([ new CharToken("y"), new CharToken("="), new FractionToken( new Strand([new CharToken("a"), new CharToken("b")]), new Strand([new CharToken("x"), new CharToken("y")]) ), ]), new SelectionRange( new Cursor([], 1), new Cursor([{ tokenIndex: 2, childIndex: 1 }], 1) ) ); editorState = Transaction.moveCursor(editorState, "right").newState; expect(editorState.selection.anchor).toEqual({ strandPath: [], pos: 3 }); expect(editorState.selection.head).toEqual({ strandPath: [], pos: 3 }); }); });