import "jest"; import { EditorState } from "prosemirror-state"; import { UxCommand } from "../../constants"; import * as listCommands from "../commands"; import { Command, CommandPredicate } from "../../types"; import { uxCommands } from "../../uxCommands"; import { describeRootsFactory, EditorStateFactory, editorStateFactoryFactory } from "../../__specs__/buildEditorState"; import "../../__specs__/expect.toMapEditorState"; import "../../__specs__/expect.toMatchEditorState"; const describeRoots = describeRootsFactory([["doc", editorStateFactoryFactory(child => ({ doc }) => doc(child))]]); function runSinkListItemSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } for (const type of ["ul", "ol"]) { describe(type, () => { it("no-op on single item", () => { const editorState = build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text{^}"))); }); // The command itself should not apply, but we expect a no-op from the // UxCommand. The UxCommand *does* apply, because it's the // preventDefault command that stops users accidentally switching tab // when using the keyboard shortcut. if (command === listCommands.sinkListItem) { expect(command).not.toMapEditorState(editorState); } else { expect(command).toMapEditorState(editorState, editorState); } }); it("sinks list item for empty selection in 2/3 item", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text")), li(p("te{^}xt")), li(p("text"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("te{^}xt")))), li(p("text"))); }) ); }); it("sinks nested list item for empty selection in 2/3 item", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("1.1")), li(p("1.2{^}")), li(p("1.3")))), li(p("2"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("1.1"), list(li(p("1.2{^}")))), li(p("1.3")))), li(p("2"))); }) ); }); it("sinks into an existing list destination", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("1.1")))), li(p("2{^}")), li(p("3"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("1.1")), li(p("2{^}")))), li(p("3"))); }) ); }); }); } } function runLiftListItemSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } for (const type of ["ul", "ol"]) { describe(type, () => { it(`lifts a ${type}(1/1) into a p`, () => { test( build(({ ul, ol, li, p }) => { const list = type === "ul" ? ul : ol; return list(li(p("text{^}"))); }), build(({ p }) => [p("text{^}")]) ); }); it(`lifts ${type}(1(1/1)/2) into ${type}(2/3)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("te{^}xt")))), li(p("text"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text")), li(p("te{^}xt")), li(p("text"))); }) ); }); it(`lifts ${type}(1(2/3)/3) into the parent`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("1.1")), li(p("1.2{^}")), li(p("1.3")))), li(p("2"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("1.1")))), li(p("1.2{^}"), list(li(p("1.3")))), li(p("2"))); }) ); }); it(`lifts ${type}[[[^]],] with into the parent`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1"), list(li(p("{^}1.1"), list(li(p("1.1.1")))), li(p("1.2"))))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("{^}1.1"), list(li(p("1.1.1")), li(p("1.2"))))); }) ); }); }); } } function runSplitListItemSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } for (const type of ["ul", "ol"]) { describe(type, () => { it("appends empty list item in non-empty root ul(1/1) with cursor at end-of-line", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("test{^}"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("test")), li(p("{^}"))); }) ); }); it("appends empty list item in non-empty nested ul(1/1) with cursor at end-of-line", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("test{^}")))), li(p("text"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("test")), li(p("{^}")))), li(p("text"))); }) ); }); it("appends split content in non-empty nested ul(1/1)", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("te{^}st")))), li(p("text"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("te")), li(p("{^}st")))), li(p("text"))); }) ); }); it("deletes selected text, and split content in non-empty nested ul(1/1)", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("t{^}es{$}t")))), li(p("text"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("text"), list(li(p("t")), li(p("{^}t")))), li(p("text"))); }) ); }); it("splits empty li when not the last item", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("{^}")), li(p())); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p("{^}")), li(p())); }) ); }); }); } } function runEnterSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } for (const type of ["ul" as "ul", "ol" as "ol"]) { describe(type, () => { it("after text append empty item", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("test{^}"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("test")), li(p("{^}"))); }) ); }); it("after text insert empty item to middle", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("test{^}")), li(p())); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("test")), li(p("{^}")), li(p())); }) ); }); it("after empty text insert empty item to middle", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("{^}")), li(p())); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p("{^}")), li(p())); }) ); }); it("last empty nested item lifts", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p(), list(li(p("{^}"))))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p("{^}"))); }) ); }); }); } } function runJoinListItemBackwardSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } for (const type of ["ul", "ol"]) { describe(type, () => { it("append ul(3/3) paragraph onto ul(2/2)", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2")), li(p("{^}3"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}3"))); }) ); }); it("append ul(3/3) paragraph and list onto ul(2/2)", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2")), li(p("{^}3"), list(li(p("3.1")), li(p("3.2"))))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}3"), list(li(p("3.1")), li(p("3.2"))))); }) ); }); it("is no-op when not at the start of the p", () => { expect(command).not.toMapEditorState( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}"))); }) ); }); }); } } function runJoinListItemForwardSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } for (const type of ["ul", "ol"]) { describe(type, () => { it("append ul(3/3) paragraph onto ul(2/2)", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}")), li(p("3"))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}3"))); }) ); }); it("append ul(3/3) paragraph and list onto ul(2/2)", () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}")), li(p("3"), list(li(p("3.1")), li(p("3.2"))))); }), build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("1")), li(p("2{^}3"), list(li(p("3.1")), li(p("3.2"))))); }) ); }); it("is no-op when not at the end of the p", () => { expect(command).not.toMapEditorState( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("{^}1")), li(p("2"))); }) ); }); }); } } function runCursorAtEmptyLastLiPredicate(predicate: CommandPredicate, build: EditorStateFactory) { function test(scenario: EditorState, expected: boolean) { expect(predicate(scenario)).toBe(expected); } for (const type of ["ul", "ol"]) { it(`in empty ${type}(1/1)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("{^}"))); }), true ); }); it(`in non-empty ${type}(1/1)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("foo{^}"))); }), false ); }); it(`in empty ${type}(1/2)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("{^}")), li(p())); }), false ); }); it(`in non-empty ${type}(1/2)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("foo{^}")), li(p())); }), false ); }); it(`in empty ${type}(2/2)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p("{^}"))); }), true ); }); it(`in non-empty ${type}(2/2)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p("foo{^}"))); }), false ); }); it(`in empty ${type}(1/3)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p("{^}")), li(p()), li(p())); }), false ); }); it(`in empty ${type}(2/3)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p("{^}")), li(p())); }), false ); }); it(`in empty ${type}(3/3)`, () => { test( build(({ ul, ol, li, p }) => { const list = type == "ul" ? ul : ol; return list(li(p()), li(p()), li(p("{^}"))); }), true ); }); } } function runToggleOrderedListSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } it("lifts ol(1/2)", () => { test(build(({ ol, li, p }) => ol(li(p("{^}1")), li(p("2")))), build(({ ol, li, p }) => [p("{^}1"), ol(li(p("2")))])); }); it("lifts ol(2/3)", () => { test( build(({ ol, li, p }) => ol(li(p("1")), li(p("{^}2")), li(p("3")))), build(({ ol, li, p }) => [ol(li(p("1"))), p("{^}2"), ol(li(p("3")))]) ); }); it("lifts ol(3/3)", () => { test( build(({ ol, li, p }) => ol(li(p("1")), li(p("2")), li(p("{^}3")))), build(({ ol, li, p }) => [ol(li(p("1")), li(p("2"))), p("{^}3")]) ); }); it("converts ul(1/1)", () => { test(build(({ ul, li, p }) => ul(li(p("1")))), build(({ ol, li, p }) => ol(li(p("1"))))); }); it("converts ul(1/2)", () => { test(build(({ ul, li, p }) => ul(li(p("1")), li(p("2")))), build(({ ol, li, p }) => ol(li(p("1")), li(p("2"))))); }); it("wraps p", () => { test(build(({ p }) => p("{^}1")), build(({ ol, li, p }) => ol(li(p("1"))))); }); it("wraps p and joins with adjacent previous ol", () => { test(build(({ p, ol, li }) => [ol(li(p("1"))), p("{^}2")]), build(({ ol, li, p }) => ol(li(p("1")), li(p("{^}2"))))); }); it("wraps p and joins with adjacent next ol", () => { test(build(({ p, ol, li }) => [p("{^}1"), ol(li(p("2")))]), build(({ ol, li, p }) => ol(li(p("{^}1")), li(p("2"))))); }); it("wraps p and joins with adjacent previous and next ol", () => { test( build(({ p, ol, li }) => [ol(li(p("1"))), p("{^}2"), ol(li(p("3")))]), build(({ ol, li, p }) => [ol(li(p("1")), li(p("{^}2")), li(p("3")))]) ); }); } function runToggleUnorderedListSuite(command: Command, build: EditorStateFactory) { function test(initial: EditorState, expected: EditorState) { expect(command).toMapEditorState(initial, expected); } it("lifts ul(1/2)", () => { test(build(({ ul, li, p }) => ul(li(p("{^}1")), li(p("2")))), build(({ ul, li, p }) => [p("{^}1"), ul(li(p("2")))])); }); it("lifts ul(2/3)", () => { test( build(({ ul, li, p }) => ul(li(p("1")), li(p("{^}2")), li(p("3")))), build(({ ul, li, p }) => [ul(li(p("1"))), p("{^}2"), ul(li(p("3")))]) ); }); it("lifts ul(3/3)", () => { test( build(({ ul, li, p }) => ul(li(p("1")), li(p("2")), li(p("{^}3")))), build(({ ul, li, p }) => [ul(li(p("1")), li(p("2"))), p("{^}3")]) ); }); it("converts ol(1/1)", () => { test(build(({ ol, li, p }) => ol(li(p("1")))), build(({ ul, li, p }) => ul(li(p("1"))))); }); it("converts ol(1/2)", () => { test(build(({ ol, li, p }) => ol(li(p("1")), li(p("2")))), build(({ ul, li, p }) => ul(li(p("1")), li(p("2"))))); }); it("wraps p", () => { test(build(({ p }) => p("{^}1")), build(({ ul, li, p }) => ul(li(p("1"))))); }); it("wraps p and joins with adjacent previous ul", () => { test(build(({ p, ul, li }) => [ul(li(p("1"))), p("{^}2")]), build(({ ul, li, p }) => ul(li(p("1")), li(p("{^}2"))))); }); it("wraps p and joins with adjacent next ul", () => { test(build(({ p, ul, li }) => [p("{^}1"), ul(li(p("2")))]), build(({ ul, li, p }) => ul(li(p("{^}1")), li(p("2"))))); }); it("wraps p and joins with adjacent previous and next ul", () => { test( build(({ p, ul, li }) => [ul(li(p("1"))), p("{^}2"), ul(li(p("3")))]), build(({ ul, li, p }) => [ul(li(p("1")), li(p("{^}2")), li(p("3")))]) ); }); } describe(listCommands.sinkListItem.name, () => { describeRoots(build => { runSinkListItemSuite(listCommands.sinkListItem, build); describe("via ui", () => { runSinkListItemSuite(uxCommands[UxCommand.Sink], build); }); }); }); describe(listCommands.liftListItem.name, () => { describeRoots(build => { runLiftListItemSuite(listCommands.liftListItem, build); describe("via ui", () => { runLiftListItemSuite(uxCommands[UxCommand.Lift], build); }); }); }); describe(listCommands.splitListItem.name, () => { describeRoots(build => { runSplitListItemSuite(listCommands.splitListItem, build); describe("via ui", () => { runSplitListItemSuite(uxCommands[UxCommand.Enter], build); }); }); }); describe(listCommands.joinListItemBackward.name, () => { describeRoots(build => { runJoinListItemBackwardSuite(listCommands.joinListItemBackward, build); describe("via ui", () => { runJoinListItemBackwardSuite(uxCommands[UxCommand.DeleteBackward], build); }); }); }); describe(listCommands.joinListItemForward.name, () => { describeRoots(build => { runJoinListItemForwardSuite(listCommands.joinListItemForward, build); describe("via ui", () => { runJoinListItemForwardSuite(uxCommands[UxCommand.DeleteForward], build); }); }); }); describe("Enter", () => { describeRoots(editorStateFactory => { runEnterSuite(listCommands.ux[UxCommand.Enter], editorStateFactory); describe("via ui", () => { runEnterSuite(uxCommands[UxCommand.Enter], editorStateFactory); }); }); }); describe(listCommands.cursorAtEmptyLastLiPredicate.name, () => { describeRoots(build => { runCursorAtEmptyLastLiPredicate(listCommands.cursorAtEmptyLastLiPredicate, build); }); }); describe(listCommands.toggleOrderedList.name, () => { describeRoots(build => { runToggleOrderedListSuite(listCommands.toggleOrderedList, build); describe("via ui", () => { runToggleOrderedListSuite(uxCommands[UxCommand.ToggleOrderedList], build); }); }); }); describe(listCommands.toggleUnorderedList.name, () => { describeRoots(build => { runToggleUnorderedListSuite(listCommands.toggleUnorderedList, build); describe("via ui", () => { runToggleUnorderedListSuite(uxCommands[UxCommand.ToggleUnorderedList], build); }); }); });