Skip to content

Testing your editor

prosekit/core/test provides a small layer for unit-testing extensions and editors without spinning up a browser.

createTestEditor(options) is a drop-in alternative to createEditor that returns a TestEditor. It exposes the same API as Editor, plus helpers for setting the document and selection from a builder syntax.

import { unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
import { createTestEditorfunction createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>
@public
} from 'prosekit/core/test'
import { defineDocfunction defineDoc(): DocExtension
@public
} from 'prosekit/extensions/doc'
import { defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
} from 'prosekit/extensions/paragraph'
import { defineTextfunction defineText(): TextExtension
@public
} from 'prosekit/extensions/text'
const editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>> = createTestEditorcreateTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>
@public
({
extensionEditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension]>
The extension to use when creating the editor.
: unionunion<readonly [DocExtension, TextExtension, ParagraphExtension]>(exts_0: DocExtension, exts_1: TextExtension, exts_2: ParagraphExtension): Union<readonly [DocExtension, TextExtension, ParagraphExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(defineDocfunction defineDoc(): DocExtension
@public
(), defineTextfunction defineText(): TextExtension
@public
(), defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
()),
})

You don't call editor.mount(...). The test editor manages a detached view internally.

editor.nodes.<name>(...) and editor.marks.<name>(...) are typed factories generated from your schema. Use them to build a document and hand it to editor.set(...).

import { unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
import { createTestEditorfunction createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>
@public
} from 'prosekit/core/test'
import { defineBoldfunction defineBold(): BoldExtension
@public
} from 'prosekit/extensions/bold'
import { defineDocfunction defineDoc(): DocExtension
@public
} from 'prosekit/extensions/doc'
import { defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
} from 'prosekit/extensions/paragraph'
import { defineTextfunction defineText(): TextExtension
@public
} from 'prosekit/extensions/text'
const editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>> = createTestEditorcreateTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>
@public
({
extensionEditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>
The extension to use when creating the editor.
: unionunion<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>(exts_0: DocExtension, exts_1: TextExtension, exts_2: ParagraphExtension, exts_3: BoldExtension): Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(
defineDocfunction defineDoc(): DocExtension
@public
(),
defineTextfunction defineText(): TextExtension
@public
(),
defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
(),
defineBoldfunction defineBold(): BoldExtension
@public
(),
), }) const n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
= editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.nodes
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.nodes: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
All {@link NodeAction } s defined by the editor.
const m
const m: ToMarkAction<SimplifyDeeper<{
    bold: {
        readonly [x: string]: any;
    };
}>>
= editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.marks
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.marks: ToMarkAction<SimplifyDeeper<{
    bold: {
        readonly [x: string]: any;
    };
}>>
All {@link MarkAction } s defined by the editor.
const docconst doc: Node = n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
.doc
doc: NodeAction
(attrs: {
    readonly [x: string]: any;
} | null, ...children: NodeChild[]) => Node (+1 overload)
Creates a node with attributes and any number of children.
(
n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
.paragraph
paragraph: NodeAction
(...children: NodeChild[]) => Node (+1 overload)
Creates a node with any number of children.
('Hello, ', m
const m: ToMarkAction<SimplifyDeeper<{
    bold: {
        readonly [x: string]: any;
    };
}>>
.bold
bold: MarkAction
(...children: NodeChild[]) => Node[] (+1 overload)
Applies a mark with any number of children.
('world'), '!'),
) editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.setTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.set(doc: Node): void
Set the editor state to the given document. You can use special tokens `<a>` and `<b>` to set the anchor and head positions of the selection.
@example```ts const editor = createTestEditor({ extension }) const n = editor.nodes const doc = n.doc(n.paragraph('<a>Hello<b> world!')) editor.set(doc) // "Hello" is selected. ```
(docconst doc: Node)

The set helper recognizes two special tokens in text content:

  • <a> marks the start of the selection.
  • <b> marks the end of the selection.
import { unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
import { createTestEditorfunction createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>
@public
} from 'prosekit/core/test'
import { defineDocfunction defineDoc(): DocExtension
@public
} from 'prosekit/extensions/doc'
import { defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
} from 'prosekit/extensions/paragraph'
import { defineTextfunction defineText(): TextExtension
@public
} from 'prosekit/extensions/text'
const editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>> = createTestEditorcreateTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>
@public
({
extensionEditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension]>
The extension to use when creating the editor.
: unionunion<readonly [DocExtension, TextExtension, ParagraphExtension]>(exts_0: DocExtension, exts_1: TextExtension, exts_2: ParagraphExtension): Union<readonly [DocExtension, TextExtension, ParagraphExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(defineDocfunction defineDoc(): DocExtension
@public
(), defineTextfunction defineText(): TextExtension
@public
(), defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
()),
}) const n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
= editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.nodes
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.nodes: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
All {@link NodeAction } s defined by the editor.
const docconst doc: Node = n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
.doc
doc: NodeAction
(attrs: {
    readonly [x: string]: any;
} | null, ...children: NodeChild[]) => Node (+1 overload)
Creates a node with attributes and any number of children.
(n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
.paragraph
paragraph: NodeAction
(...children: NodeChild[]) => Node (+1 overload)
Creates a node with any number of children.
('<a>Hello<b> world!'))
editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.setTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.set(doc: Node): void
Set the editor state to the given document. You can use special tokens `<a>` and `<b>` to set the anchor and head positions of the selection.
@example```ts const editor = createTestEditor({ extension }) const n = editor.nodes const doc = n.doc(n.paragraph('<a>Hello<b> world!')) editor.set(doc) // "Hello" is selected. ```
(docconst doc: Node)
// "Hello" is now selected.
import { unionfunction union<const E extends readonly Extension[]>(...exts: E): Union<E> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
} from 'prosekit/core'
import { createTestEditorfunction createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>
@public
} from 'prosekit/core/test'
import { defineBoldfunction defineBold(): BoldExtension
@public
} from 'prosekit/extensions/bold'
import { defineDocfunction defineDoc(): DocExtension
@public
} from 'prosekit/extensions/doc'
import { defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
} from 'prosekit/extensions/paragraph'
import { defineTextfunction defineText(): TextExtension
@public
} from 'prosekit/extensions/text'
import { describeconst describe: SuiteAPI
Creates a suite of tests, allowing for grouping and hierarchical organization of tests. Suites can contain both tests and other suites, enabling complex test structures.
@paramname - The name of the suite, used for identification and reporting.@paramfn - A function that defines the tests and suites within this suite.@example```ts // Define a suite with two tests describe('Math operations', () => { test('should add two numbers', () => { expect(add(1, 2)).toBe(3); }); test('should subtract two numbers', () => { expect(subtract(5, 2)).toBe(3); }); }); ```@example```ts // Define nested suites describe('String operations', () => { describe('Trimming', () => { test('should trim whitespace from start and end', () => { expect(' hello '.trim()).toBe('hello'); }); }); describe('Concatenation', () => { test('should concatenate two strings', () => { expect('hello' + ' ' + 'world').toBe('hello world'); }); }); }); ```
, expectconst expect: ExpectStatic, itconst it: TestAPI
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
@paramname - The name of the test or a function that will be used as a test name.@paramoptionsOrFn - Optional. The test options or the test function if no explicit name is provided.@paramoptionsOrTest - Optional. The test function or options, depending on the previous parameters.@throws{Error} If called inside another test function.@example```ts // Define a simple test it('adds two numbers', () => { expect(add(1, 2)).toBe(3); }); ```@example```ts // Define a test with options it('subtracts two numbers', { retry: 3 }, () => { expect(subtract(5, 2)).toBe(3); }); ```
} from 'vitest'
describedescribe<object>(name: string | Function, fn?: SuiteFactory<object> | undefined, options?: number): SuiteCollector<object> (+1 overload)
Creates a suite of tests, allowing for grouping and hierarchical organization of tests. Suites can contain both tests and other suites, enabling complex test structures.
@paramname - The name of the suite, used for identification and reporting.@paramfn - A function that defines the tests and suites within this suite.@example```ts // Define a suite with two tests describe('Math operations', () => { test('should add two numbers', () => { expect(add(1, 2)).toBe(3); }); test('should subtract two numbers', () => { expect(subtract(5, 2)).toBe(3); }); }); ```@example```ts // Define nested suites describe('String operations', () => { describe('Trimming', () => { test('should trim whitespace from start and end', () => { expect(' hello '.trim()).toBe('hello'); }); }); describe('Concatenation', () => { test('should concatenate two strings', () => { expect('hello' + ' ' + 'world').toBe('hello world'); }); }); }); ```
('toggleBold', () => {
itit<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
@paramname - The name of the test or a function that will be used as a test name.@paramoptionsOrFn - Optional. The test options or the test function if no explicit name is provided.@paramoptionsOrTest - Optional. The test function or options, depending on the previous parameters.@throws{Error} If called inside another test function.@example```ts // Define a simple test it('adds two numbers', () => { expect(add(1, 2)).toBe(3); }); ```@example```ts // Define a test with options it('subtracts two numbers', { retry: 3 }, () => { expect(subtract(5, 2)).toBe(3); }); ```
('wraps the selection in a bold mark', () => {
const editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>> = createTestEditorcreateTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>
@public
({
extensionEditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>
The extension to use when creating the editor.
: unionunion<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>(exts_0: DocExtension, exts_1: TextExtension, exts_2: ParagraphExtension, exts_3: BoldExtension): Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]> (+1 overload)
Merges multiple extensions into one. You can pass multiple extensions as arguments or a single array containing multiple extensions.
@throwsIf no extensions are provided.@example```ts function defineFancyNodes() { return union( defineFancyParagraph(), defineFancyHeading(), ) } ```@example```ts function defineFancyNodes() { return union([ defineFancyParagraph(), defineFancyHeading(), ]) } ```@public
(
defineDocfunction defineDoc(): DocExtension
@public
(),
defineTextfunction defineText(): TextExtension
@public
(),
defineParagraphfunction defineParagraph(): ParagraphExtension
@publicDefines a paragraph node. The paragraph node spec has the highest priority, because it should be the default block node for most cases.
(),
defineBoldfunction defineBold(): BoldExtension
@public
(),
), }) const n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
= editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.nodes
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.nodes: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
All {@link NodeAction } s defined by the editor.
editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.setTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.set(doc: Node): void
Set the editor state to the given document. You can use special tokens `<a>` and `<b>` to set the anchor and head positions of the selection.
@example```ts const editor = createTestEditor({ extension }) const n = editor.nodes const doc = n.doc(n.paragraph('<a>Hello<b> world!')) editor.set(doc) // "Hello" is selected. ```
(n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
.doc
doc: NodeAction
(attrs: {
    readonly [x: string]: any;
} | null, ...children: NodeChild[]) => Node (+1 overload)
Creates a node with attributes and any number of children.
(n
const n: ToNodeAction<SimplifyDeeper<{
    doc: {
        readonly [x: string]: any;
    };
    paragraph: {
        readonly [x: string]: any;
    };
    text: {
        readonly [x: string]: any;
    };
}>>
.paragraph
paragraph: NodeAction
(...children: NodeChild[]) => Node (+1 overload)
Creates a node with any number of children.
('<a>hi<b>')))
expectexpect<boolean>(actual: boolean, message?: string): Assertion<boolean> (+1 overload)(editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.commands
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.commands: ToCommandAction<{
    toggleBold: [];
    setParagraph: [];
}>
All {@link CommandAction } s defined by the editor.
.toggleBoldtoggleBold: CommandAction<[]>.canExecCommandAction<[]>.canExec(): boolean
Check if the current command can be executed. Return `true` if the command can be executed, otherwise `false`.
()).toBeJestAssertion<boolean>.toBe: <boolean>(expected: boolean) => void
Checks that a value is what you expect. It calls `Object.is` to compare values. Don't use `toBe` with floating-point numbers.
@exampleexpect(result).toBe(42); expect(status).toBe(true);
(true)
editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.commands
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.commands: ToCommandAction<{
    toggleBold: [];
    setParagraph: [];
}>
All {@link CommandAction } s defined by the editor.
.toggleBold
toggleBold: CommandAction
() => boolean
Execute the current command. Return `true` if the command was successfully executed, otherwise `false`.
()
expectexpect<boolean>(actual: boolean, message?: string): Assertion<boolean> (+1 overload)(editorconst editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.marks
Editor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.marks: ToMarkAction<SimplifyDeeper<{
    bold: {
        readonly [x: string]: any;
    };
}>>
All {@link MarkAction } s defined by the editor.
.bold
bold: MarkAction<{
    readonly [x: string]: any;
}>
.isActive
MarkAction<{ readonly [x: string]: any; }>.isActive: (attrs?: {
    readonly [x: string]: any;
} | undefined) => boolean
Checks if the mark is active in the current editor selection. If the optional `attrs` parameter is provided, it will check if the mark is active with the given attributes.
()).toBeJestAssertion<boolean>.toBe: <boolean>(expected: boolean) => void
Checks that a value is what you expect. It calls `Object.is` to compare values. Don't use `toBe` with floating-point numbers.
@exampleexpect(result).toBe(42); expect(status).toBe(true);
(true)
}) })