Skip to content

Commit

Permalink
editor: set up integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
thecodrr committed Feb 27, 2023
1 parent 912e97d commit 16ffec1
Show file tree
Hide file tree
Showing 11 changed files with 17,429 additions and 4,014 deletions.
5 changes: 4 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ packages/editor/styles/
packages/editor/languages/

# editor mobile
packages/editor-mobile/build.bundle
packages/editor-mobile/build.bundle

# snapshots
tap-snapshots
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ dist
nx-cloud.env
.idea
.eslintcache
.env.local
.env.local
.nyc_output
20,771 changes: 16,769 additions & 4,002 deletions packages/editor/package-lock.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"@tiptap/extension-text-align": "^2.0.0-beta.218",
"@tiptap/extension-text-style": "^2.0.0-beta.218",
"@tiptap/extension-underline": "^2.0.0-beta.218",
"@tiptap/starter-kit": "^2.0.0-beta.218",
"@tiptap/pm": "^2.0.0-beta.218",
"@tiptap/starter-kit": "^2.0.0-beta.218",
"detect-indent": "^7.0.0",
"katex": "^0.16.2",
"prism-themes": "^1.9.0",
Expand All @@ -45,17 +45,24 @@
"zustand": "^3.7.2"
},
"devDependencies": {
"@happy-dom/global-registrator": "^8.9.0",
"@mdi/js": "^6.9.96",
"@mdi/react": "^1.6.0",
"@swc/core": "^1.3.36",
"@types/katex": "^0.14.0",
"@types/prismjs": "^1.26.0",
"@types/react": "17.0.2",
"@types/react-color": "^3.0.6",
"@types/react-dom": "17.0.2",
"@types/react-modal": "^3.13.1",
"@types/tap": "^15.0.8",
"@types/tinycolor2": "^1.4.3",
"expect": "^29.4.3",
"framer-motion": "^6.5.1",
"isomorphic-fetch": "^3.0.0",
"prosemirror-test-builder": "^1.1.0",
"tap": "^16.3.4",
"tsconfig-paths": "^3.14.2",
"typescript": "^4.8.2",
"web-vitals": "^2.1.4",
"zx": "^7.0.8"
Expand All @@ -67,6 +74,7 @@
"react-dom": ">=17.0.0"
},
"scripts": {
"test": "TS_NODE_PROJECT=tsconfig.tests.json tap --ts --no-check-coverage --test-env=NODE_ENV=test src/**/*.test.ts",
"prebuild": "zx ./scripts/build.mjs",
"prewatch": "zx ./scripts/build.mjs",
"build": "tsc",
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/extensions/react/react-node-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
private renderReactComponent(
component: () => React.ReactElement<unknown> | null
) {
if (process.env.NODE_ENV === "test") return;
if (!this.domRef || !component || !this.portalProviderAPI) {
console.warn("Cannot render node view", this.editor.storage);
console.warn("Cannot render node view");
return;
}

Expand Down
247 changes: 247 additions & 0 deletions packages/editor/src/extensions/task-list/tests/task-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import "@/tests.setup";
import tap from "tap";
import expect from "expect";
import { Editor, AnyExtension, Extensions } from "@tiptap/core";
import { TaskListNode } from "../index";
import { TaskItemNode } from "../../task-item";
import StarterKit from "@tiptap/starter-kit";
import { builders, doc, NodeBuilder, p, eq } from "prosemirror-test-builder";
import { Node, Schema } from "@tiptap/pm/model";
import { countCheckedItems, deleteCheckedItems, sortList } from "../utils";
import { EditorState } from "@tiptap/pm/state";

type Builder<TNodes extends string> = {
scheme: Schema;
} & Record<TNodes, NodeBuilder>;

type EditorOptions<TNodes extends string> = {
extensions: Record<TNodes, AnyExtension>;
initialDoc?: (builder: Builder<TNodes>) => Node;
};

function createEditor<TNodes extends string>(options: EditorOptions<TNodes>) {
const { extensions, initialDoc } = options;
const editor = new Editor({
extensions: [StarterKit, ...(Object.values(extensions) as Extensions)]
});

const builder = builders(editor.schema) as unknown as Builder<TNodes>;

if (initialDoc) {
const doc = initialDoc(builder);
editor.view.updateState(
EditorState.create({
schema: editor.view.state.schema,
doc,
plugins: editor.state.plugins
})
);

return { editor, builder, initialDoc: doc };
}

return { editor, builder, initialDoc: editor.state.doc };
}

tap.test(`count items in a task list`, async () => {
const {
builder: { taskItem, taskList }
} = createEditor({
extensions: {
taskItem: TaskItemNode,
taskList: TaskListNode
}
});

const taskListNode = taskList(
taskItem({ checked: true }, p("Task item 1")),
taskItem({ checked: false }, p("Task item 2")),
taskItem(
{ checked: false },
p("Task item 3"),
taskList(taskItem({ checked: true }, p("Task item 4")))
)
);

const { checked, total } = countCheckedItems(taskListNode);
expect(checked).toBe(2);
expect(total).toBe(4);
});

tap.test(`delete checked items in a task list`, async (t) => {
const { editor } = createEditor({
initialDoc: ({ taskItem, taskList }) =>
doc(
taskList(
taskItem({ checked: true }, p("Task item 1")),
taskItem({ checked: false }, p("Task item 2"))
)
),
extensions: {
taskItem: TaskItemNode,
taskList: TaskListNode
}
});

let { tr } = editor.state;
tr = deleteCheckedItems(tr, 0) || tr;
editor.view.dispatch(tr);

t.matchSnapshot(editor.state.doc.content.toJSON());
});

tap.test(`delete checked items in a nested task list`, async (t) => {
const { editor } = createEditor({
initialDoc: ({ taskItem, taskList }) =>
doc(
taskList(
taskItem({ checked: true }, p("Task item 1")),
taskItem({ checked: false }, p("Task item 2")),
taskItem(
{ checked: false },
p("Task item 3"),
taskList(
taskItem({ checked: true }, p("Task item 4")),
taskItem({ checked: false }, p("Task item 5")),
taskItem({ checked: false }, p("Task item 6")),
taskItem(
{ checked: true },
p("Task item 7"),
taskList(
taskItem({ checked: true }, p("Task item 8")),
taskItem({ checked: true }, p("Task item 9")),
taskItem({ checked: false }, p("Task item 10")),
taskItem({ checked: true }, p("Task item 11"))
)
)
)
)
)
),
extensions: {
taskItem: TaskItemNode,
taskList: TaskListNode
}
});

let { tr } = editor.state;
tr = deleteCheckedItems(tr, 0) || tr;
editor.view.dispatch(tr);

t.matchSnapshot(editor.state.doc.content.toJSON());
});

tap.test(
`delete checked items in a task list with no checked items should do nothing`,
async () => {
const { editor, initialDoc } = createEditor({
initialDoc: ({ taskItem, taskList }) =>
doc(
taskList(
taskItem({ checked: false }, p("Task item 2")),
taskItem(
{ checked: false },
p("Task item 3"),
taskList(
taskItem({ checked: false }, p("Task item 5")),
taskItem({ checked: false }, p("Task item 6")),
taskItem(
{ checked: false },
p("Task item 7"),
taskList(taskItem({ checked: false }, p("Task item 10")))
)
)
)
)
),
extensions: {
taskItem: TaskItemNode,
taskList: TaskListNode
}
});

editor.commands.command(({ tr }) => !!deleteCheckedItems(tr, 0));

expect(eq(editor.state.doc, initialDoc)).toBe(true);
}
);

tap.test(`sort checked items to the bottom of the task list`, async (t) => {
const { editor } = createEditor({
initialDoc: ({ taskItem, taskList }) =>
doc(
taskList(
taskItem({ checked: true }, p("Task item 1")),
taskItem({ checked: false }, p("Task item 2")),
taskItem(
{ checked: false },
p("Task item 3"),
taskList(
taskItem({ checked: true }, p("Task item 4")),
taskItem({ checked: true }, p("Task item 5")),
taskItem({ checked: false }, p("Task item 6")),
taskItem(
{ checked: false },
p("Task item 7"),
taskList(
taskItem({ checked: true }, p("Task item 8")),
taskItem({ checked: true }, p("Task item 9")),
taskItem({ checked: false }, p("Task item 10")),
taskItem({ checked: true }, p("Task item 11"))
)
)
)
)
)
),
extensions: {
taskItem: TaskItemNode,
taskList: TaskListNode
}
});

editor.commands.command(({ tr }) => !!sortList(tr, 0));

t.matchSnapshot(editor.state.doc.content.toJSON());
});

tap.test(
`sorting a task list with no checked items should do nothing`,
async () => {
const { editor, initialDoc } = createEditor({
initialDoc: ({ taskItem, taskList }) =>
doc(
taskList(
taskItem({ checked: false }, p("Task item 1")),
taskItem({ checked: false }, p("Task item 2"))
)
),
extensions: {
taskItem: TaskItemNode,
taskList: TaskListNode
}
});

editor.commands.command(({ tr }) => !!sortList(tr, 0));
expect(eq(editor.state.doc, initialDoc)).toBe(true);
}
);
18 changes: 13 additions & 5 deletions packages/editor/src/extensions/task-list/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ export function deleteCheckedItems(tr: Transaction, pos: number) {
listNode.forEach((node, _, index) => {
if (!node.attrs.checked) children.push(listNode.child(index));
});
// if all items are unchecked, skip
if (children.length === listNode.childCount) continue;

tr.replaceWith(
tr.mapping.map(list.pos),
tr.mapping.map(list.pos + list.node.nodeSize),
listNode.copy(Fragment.from(children))
tr.mapping.map(list.pos + 1),
tr.mapping.map(list.pos + list.node.nodeSize - 1),
Fragment.from(children)
);
}

Expand Down Expand Up @@ -95,10 +97,16 @@ export function sortList(tr: Transaction, pos: number) {
checked: node.attrs.checked ? 1 : 0
});
});
// if every item is checked or unchecked, skip
if (
children.every((a) => a.checked === 1) ||
children.every((a) => a.checked === 0)
)
continue;

tr.replaceWith(
tr.mapping.map(list.pos),
tr.mapping.map(list.pos + list.node.nodeSize),
tr.mapping.map(list.pos + 1),
tr.mapping.map(list.pos + list.node.nodeSize - 1),
Fragment.from(
children
.sort((a, b) => a.checked - b.checked)
Expand Down
Loading

0 comments on commit 16ffec1

Please sign in to comment.