Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,143 @@ function remountEditor(editor: Pick<BlockNoteEditor, "mount" | "unmount">) {
}

describe("runtime extension plugin source", () => {
function createEditor(
options?: Parameters<typeof BlockNoteEditor.create>[0],
) {
const editor = BlockNoteEditor.create(options);
editors.push(editor);
rebuildPlugins(editor);
return editor;
}

it("installs nested plugins in dependency order and retains that order on rebuild", () => {
const syncKey = new PluginKey<number>("sync");
const cursorKey = new PluginKey<number | undefined>("cursor");
const sync = new Plugin({
key: syncKey,
state: { init: () => 42, apply: (_tr, value) => value },
});
const cursor = new Plugin({
key: cursorKey,
state: {
init: (_config, state) => syncKey.getState(state),
apply: (_tr, value) => value,
},
});
const parent = new Plugin({ key: new PluginKey("parent") });
const editor = createEditor();
editor.registerExtension({
key: "parent",
prosemirrorPlugins: [parent],
blockNoteExtensions: [
() => ({ key: "cursor", prosemirrorPlugins: [cursor] }),
() => ({
key: "group",
blockNoteExtensions: [
() => ({
key: "sync",
runsBefore: ["cursor"],
prosemirrorPlugins: [sync],
}),
],
}),
],
});
expect(cursorKey.getState(editor.prosemirrorState)).toBe(42);
const expected = [sync, cursor, parent];
expect(
editor.prosemirrorState.plugins.filter((p) => expected.includes(p)),
).toEqual(expected);
rebuildPlugins(editor);
expect(
editor.prosemirrorState.plugins.filter((p) => expected.includes(p)),
).toEqual(expected);
});

it("exposes earlier registrations to factories and plugin initializers", () => {
const editor = createEditor();
const dependency = { key: "dependency", value: 42 };
const key = new PluginKey<number | undefined>("lookup");
editor.registerExtension([
dependency,
({ editor }) => {
expect(editor.getExtension(dependency.key)).toBe(dependency);
return {
key: "consumer",
prosemirrorPlugins: [
new Plugin({
key,
state: {
init() {
return editor.getExtension<typeof dependency>(dependency.key)
?.value;
},
apply: (_tr, value) => value,
},
}),
],
};
},
]);
expect(key.getState(editor.prosemirrorState)).toBe(42);
});

it("does not silently remove an unrelated plugin with the same key", () => {
const key = new PluginKey("collision");
const original = new Plugin({ key });
const editor = createEditor({
extensions: [() => ({ key: "original", prosemirrorPlugins: [original] })],
});
expect(() =>
editor.registerExtension({
key: "collision",
prosemirrorPlugins: [new Plugin({ key })],
}),
).toThrow(/keyed plugin/);
expect(key.get(editor.prosemirrorState)).toBe(original);
});

it("retains registrations made inside another factory", () => {
const editor = createEditor();
const plugin = new Plugin({});
const inner = { key: "inner", prosemirrorPlugins: [plugin] };
editor.registerExtension(({ editor }) => {
editor.registerExtension(inner);
return { key: "outer" };
});
expect(editor.getExtension(inner.key)).toBe(inner);
expect(editor.prosemirrorState.plugins).toContain(plugin);
rebuildPlugins(editor);
expect(editor.prosemirrorState.plugins).toContain(plugin);
});

it("removes obsolete parent dependency edges before validating a replacement", () => {
const childPlugin = new Plugin({});
const parentPlugin = new Plugin({});
const parent = {
key: "former-parent",
blockNoteExtensions: [
() => ({ key: "former-child", prosemirrorPlugins: [childPlugin] }),
],
};
const editor = createEditor();
editor.registerExtension(parent);
editor.replaceExtension(parent, {
key: parent.key,
runsBefore: ["former-child"],
prosemirrorPlugins: [parentPlugin],
});
// The old child-before-parent edge would create a false cycle.
const expected = [parentPlugin, childPlugin];
expect(
editor.prosemirrorState.plugins.filter((p) => expected.includes(p)),
).toEqual(expected);
rebuildPlugins(editor);
expect(
editor.prosemirrorState.plugins.filter((p) => expected.includes(p)),
).toEqual(expected);
});

it("resets selected plugin state without resetting retained plugins", () => {
const key = new PluginKey<number>("reset");
const retainedKey = new PluginKey<number>("retained");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @vitest-environment jsdom
*/
import { Plugin, PluginKey } from "prosemirror-state";
import { describe, expect, it, vi } from "vite-plus/test";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";

import { createExtension } from "../../BlockNoteExtension.js";
import { BlockNoteEditor } from "../../BlockNoteEditor.js";
Expand All @@ -15,6 +15,130 @@ function createMountedEditor(
return editor;
}

describe("runtime extension lifecycle", () => {
const cleanups: Array<() => void> = [];
afterEach(() => {
for (const cleanup of cleanups.splice(0).reverse()) {
cleanup();
}
});

function createEditor() {
const editor = BlockNoteEditor.create();
const host = document.createElement("div");
document.body.append(host);
cleanups.push(() => {
editor.unmount();
editor._tiptapEditor.destroy();
host.remove();
});
editor.mount(host);
return { editor, host };
}

it("mounts nested plugin-less extensions once and cleans them up", () => {
const { editor, host } = createEditor();
const parentCleanup = vi.fn();
const childCleanup = vi.fn();
const parentMount = vi.fn(() => parentCleanup);
const childMount = vi.fn(() => childCleanup);
const child = { key: "child", mount: childMount };
const parent = {
key: "parent",
mount: parentMount,
blockNoteExtensions: [() => child],
};
editor.registerExtension(parent);
editor.registerExtension(parent);
expect(parentMount).toHaveBeenCalledTimes(1);
expect(childMount).toHaveBeenCalledTimes(1);
editor.unregisterExtension([parent, child]);
expect(parentCleanup).toHaveBeenCalledTimes(1);
expect(childCleanup).toHaveBeenCalledTimes(1);
editor.registerExtension(parent);
editor.unmount();
editor.mount(host);
expect(parentMount).toHaveBeenCalledTimes(3);
expect(childMount).toHaveBeenCalledTimes(3);
});

it("allows cleanup to re-register the same instance after plugin removal", () => {
const { editor, host } = createEditor();
const key = new PluginKey("cleanup-identical");
const plugin = new Plugin({ key });
let resurrect = true;
let registrationError: unknown;
const mount = vi.fn(() => () => {
if (resurrect) {
resurrect = false;
try {
editor.registerExtension(extension);
} catch (error) {
registrationError = error;
}
}
});
const extension = {
key: "cleanup-identical",
prosemirrorPlugins: [plugin],
mount,
};
editor.registerExtension(extension);
editor.unregisterExtension(extension);
expect(registrationError).toBeUndefined();
expect(editor.getExtension(extension.key)).toBe(extension);
expect(mount).toHaveBeenCalledTimes(2);
expect(key.get(editor.prosemirrorState)).toBe(plugin);
editor.unmount();
editor.mount(host);
expect(key.get(editor.prosemirrorState)).toBe(plugin);
});

it("does not mount an extension removed by an earlier mount hook", () => {
const { editor } = createEditor();
const childMount = vi.fn();
const child = { key: "removed-child", mount: childMount };
editor.registerExtension([
{
key: "removing-parent",
mount() {
editor.unregisterExtension(child);
},
},
child,
]);
expect(editor.getExtension(child.key)).toBeUndefined();
expect(childMount).not.toHaveBeenCalled();
});

it("mounts runtime extensions and cleans them up when removed", () => {
let mounts = 0;
let unmounts = 0;
const extension = createExtension(() => ({
key: "runtime-lifecycle",
mount({ signal }: { signal: AbortSignal }) {
expect(signal.aborted).toBe(false);
mounts++;
return () => {
unmounts++;
};
},
}));
const { editor, host } = createEditor();
editor.registerExtension(extension());
expect(mounts).toBe(1);
const instance = editor.getExtension(extension)!;
editor.unregisterExtension(extension);
expect(unmounts).toBe(1);
editor.registerExtension(instance);
expect(mounts).toBe(2);
editor.unmount();
expect(unmounts).toBe(2);
editor.mount(host);
expect(mounts).toBe(3);
});
});

describe("Extension keyboard shortcuts", () => {
it("validates shortcut names at initialization", () => {
expect(() =>
Expand Down
Loading
Loading