Testing your editor
prosekit/core/test provides a small layer for unit-testing extensions and editors without spinning up a browser.
createTestEditor
Section titled “createTestEditor”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 { union function 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 { createTestEditor function createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>@public } from 'prosekit/core/test'
import { defineDoc function defineDoc(): DocExtension@public } from 'prosekit/extensions/doc'
import { defineParagraph function 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 { defineText function defineText(): TextExtension@public } from 'prosekit/extensions/text'
const editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>> = createTestEditor createTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>@public ({
extension EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension]>The extension to use when creating the editor. : union union<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 (defineDoc function defineDoc(): DocExtension@public (), defineText function defineText(): TextExtension@public (), defineParagraph function 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.
Using the builder syntax
Section titled “Using the builder syntax”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 { union function 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 { createTestEditor function createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>@public } from 'prosekit/core/test'
import { defineBold function defineBold(): BoldExtension@public } from 'prosekit/extensions/bold'
import { defineDoc function defineDoc(): DocExtension@public } from 'prosekit/extensions/doc'
import { defineParagraph function 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 { defineText function defineText(): TextExtension@public } from 'prosekit/extensions/text'
const editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>> = createTestEditor createTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>@public ({
extension EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>The extension to use when creating the editor. : union union<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 (
defineDoc function defineDoc(): DocExtension@public (),
defineText function defineText(): TextExtension@public (),
defineParagraph function 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. (),
defineBold function defineBold(): BoldExtension@public (),
),
})
const n const n: ToNodeAction<SimplifyDeeper<{
doc: {
readonly [x: string]: any;
};
paragraph: {
readonly [x: string]: any;
};
text: {
readonly [x: string]: any;
};
}>>
= editor const 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;
};
}>>
= editor const 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 doc const 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'), '!'),
)
editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>> .set TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.set(doc: Node): voidSet 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.
``` (doc const doc: Node )Selection markers
Section titled “Selection markers”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 { union function 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 { createTestEditor function createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>@public } from 'prosekit/core/test'
import { defineDoc function defineDoc(): DocExtension@public } from 'prosekit/extensions/doc'
import { defineParagraph function 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 { defineText function defineText(): TextExtension@public } from 'prosekit/extensions/text'
const editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>> = createTestEditor createTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>@public ({
extension EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension]>The extension to use when creating the editor. : union union<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 (defineDoc function defineDoc(): DocExtension@public (), defineText function defineText(): TextExtension@public (), defineParagraph function 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;
};
}>>
= editor const 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 doc const 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!'))
editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>> .set TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension]>>.set(doc: Node): voidSet 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.
``` (doc const doc: Node )
// "Hello" is now selected.A complete unit test
Section titled “A complete unit test”import { union function 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 { createTestEditor function createTestEditor<E extends Extension>(options: EditorOptions<E>): TestEditor<E>@public } from 'prosekit/core/test'
import { defineBold function defineBold(): BoldExtension@public } from 'prosekit/extensions/bold'
import { defineDoc function defineDoc(): DocExtension@public } from 'prosekit/extensions/doc'
import { defineParagraph function 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 { defineText function defineText(): TextExtension@public } from 'prosekit/extensions/text'
import { describe const describe: SuiteAPICreates 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');
});
});
});
``` , expect const expect: ExpectStatic , it const it: TestAPIDefines 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'
describe describe<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', () => {
it it<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 editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>> = createTestEditor createTestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>(options: EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>): TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>@public ({
extension EditorOptions<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.extension: Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>The extension to use when creating the editor. : union union<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 (
defineDoc function defineDoc(): DocExtension@public (),
defineText function defineText(): TextExtension@public (),
defineParagraph function 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. (),
defineBold function defineBold(): BoldExtension@public (),
),
})
const n const n: ToNodeAction<SimplifyDeeper<{
doc: {
readonly [x: string]: any;
};
paragraph: {
readonly [x: string]: any;
};
text: {
readonly [x: string]: any;
};
}>>
= editor const 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.
editor const editor: TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>> .set TestEditor<Union<readonly [DocExtension, TextExtension, ParagraphExtension, BoldExtension]>>.set(doc: Node): voidSet 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>')))
expect expect<boolean>(actual: boolean, message?: string): Assertion<boolean> (+1 overload) (editor const 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<[]> .canExec CommandAction<[]>.canExec(): booleanCheck if the current command can be executed. Return `true` if the command
can be executed, otherwise `false`. ()).toBe JestAssertion<boolean>.toBe: <boolean>(expected: boolean) => voidChecks 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)
editor const 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`. ()
expect expect<boolean>(actual: boolean, message?: string): Assertion<boolean> (+1 overload) (editor const 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. ()).toBe JestAssertion<boolean>.toBe: <boolean>(expected: boolean) => voidChecks 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)
})
})