vitest-prosemirror 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +38 -0
- package/dist/{vitest-prosemirror.js → renderProseMirror-CE37KmN_.js} +425 -261
- package/dist/setup.d.ts +21 -0
- package/dist/setup.js +165 -0
- package/package.json +19 -6
- package/dist/vitest-prosemirror.d.ts +0 -42
package/README.md
CHANGED
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
[](https://app.codecov.io/gh/marekdedic/vitest-prosemirror)
|
|
6
6
|
[](https://www.npmjs.com/package/vitest-prosemirror)
|
|
7
7
|
[](https://github.com/marekdedic/vitest-prosemirror/blob/master/LICENSE)
|
|
8
|
+
[](https://marekdedic.github.io/vitest-prosemirror/)
|
|
8
9
|
|
|
9
10
|

|
|
10
11
|
|
|
11
12
|
A plugin for Vitest that enables you to write tests using the ProseMirror editor
|
|
12
13
|
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
Full documentation — a getting-started guide and the API reference — is available at **[marekdedic.github.io/vitest-prosemirror](https://marekdedic.github.io/vitest-prosemirror/)**.
|
|
17
|
+
|
|
13
18
|
## How to install
|
|
14
19
|
|
|
15
20
|
```sh
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Command } from 'prosemirror-state';
|
|
2
|
+
import { DirectEditorProps } from 'prosemirror-view';
|
|
3
|
+
import { EditorState } from 'prosemirror-state';
|
|
4
|
+
import { Node as Node_2 } from 'prosemirror-model';
|
|
5
|
+
import { ParseOptions } from 'prosemirror-model';
|
|
6
|
+
import { Schema } from 'prosemirror-model';
|
|
7
|
+
import { Selection as Selection_2 } from 'prosemirror-state';
|
|
8
|
+
|
|
9
|
+
declare interface Clipboard_2 {
|
|
10
|
+
html: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
export { Clipboard_2 as Clipboard }
|
|
14
|
+
|
|
15
|
+
export declare interface Options {
|
|
16
|
+
autoCleanup: boolean;
|
|
17
|
+
editorProps: Omit<DirectEditorProps, "dispatchTransaction" | "state">;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export declare function parseHTML(html: string, schema: Schema, options?: ParseOptions): Node_2;
|
|
21
|
+
|
|
22
|
+
export { ParseOptions }
|
|
23
|
+
|
|
24
|
+
export declare interface PasteContent {
|
|
25
|
+
files?: Array<File>;
|
|
26
|
+
html?: string;
|
|
27
|
+
plainText?: boolean;
|
|
28
|
+
text?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export declare type PasteInput = PasteContent | Node_2 | string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The handle returned by `renderProseMirror`: a live editor mounted in jsdom,
|
|
35
|
+
* exposing readers for its document and rendered DOM plus verbs that drive the
|
|
36
|
+
* ProseMirror input path.
|
|
37
|
+
*/
|
|
38
|
+
export declare interface ProseMirrorEditor {
|
|
39
|
+
/** Dispatch a click at the target selector or captured element. */
|
|
40
|
+
click(target: Element | string): boolean;
|
|
41
|
+
/** Run a ProseMirror command against the current state. */
|
|
42
|
+
command(command: Command): boolean;
|
|
43
|
+
/** Serialise the current selection the way the copy handler would. */
|
|
44
|
+
copy(): Clipboard_2;
|
|
45
|
+
/** Tear down the editor, its DOM node and the global mocks. */
|
|
46
|
+
destroy(): void;
|
|
47
|
+
/** The current document (`state.doc`). */
|
|
48
|
+
readonly doc: Node_2;
|
|
49
|
+
/** The single element matching the selector. */
|
|
50
|
+
element(selector: string): HTMLElement;
|
|
51
|
+
/** Every element matching the selector. */
|
|
52
|
+
elements(selector: string): Array<HTMLElement>;
|
|
53
|
+
/** The rendered `innerHTML` of the editor DOM. */
|
|
54
|
+
readonly html: string;
|
|
55
|
+
/** Paste content through the DOM paste path. */
|
|
56
|
+
paste(content: PasteInput): void;
|
|
57
|
+
/** Set the selection from a {@link TesterSelection}. */
|
|
58
|
+
setSelection(selection: TesterSelection): void;
|
|
59
|
+
/** The current editor state. Live getter - reads the latest dispatch. */
|
|
60
|
+
readonly state: EditorState;
|
|
61
|
+
/** The rendered `textContent` of the editor DOM. */
|
|
62
|
+
readonly text: string;
|
|
63
|
+
/** Type key strings through the DOM input path. */
|
|
64
|
+
type(text: string): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Mount a ProseMirror `EditorView` on a jsdom node and return a
|
|
69
|
+
* {@link ProseMirrorEditor} handle for driving it. Installs the mocks the
|
|
70
|
+
* tester relies on (`MutationObserver`, range rects) and registers the editor
|
|
71
|
+
* for automatic cleanup after each test unless `autoCleanup: false` is passed.
|
|
72
|
+
*/
|
|
73
|
+
export declare const renderProseMirror: (documentRoot: Node_2, options?: Partial<Options>) => ProseMirrorEditor;
|
|
74
|
+
|
|
75
|
+
export declare type TesterSelection = "all" | "end" | "start" | {
|
|
76
|
+
anchor: number | string;
|
|
77
|
+
head: number | string;
|
|
78
|
+
} | (string & {}) | Selection_2 | number;
|
|
79
|
+
|
|
80
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { r as renderProseMirror } from "./renderProseMirror-CE37KmN_.js";
|
|
2
|
+
import { DOMParser } from "prosemirror-model";
|
|
3
|
+
//#region src/parseHTML.ts
|
|
4
|
+
var wrapMap = {
|
|
5
|
+
caption: ["table"],
|
|
6
|
+
col: ["table", "colgroup"],
|
|
7
|
+
colgroup: ["table"],
|
|
8
|
+
tbody: ["table"],
|
|
9
|
+
td: [
|
|
10
|
+
"table",
|
|
11
|
+
"tbody",
|
|
12
|
+
"tr"
|
|
13
|
+
],
|
|
14
|
+
tfoot: ["table"],
|
|
15
|
+
th: [
|
|
16
|
+
"table",
|
|
17
|
+
"tbody",
|
|
18
|
+
"tr"
|
|
19
|
+
],
|
|
20
|
+
thead: ["table"],
|
|
21
|
+
tr: ["table", "tbody"]
|
|
22
|
+
};
|
|
23
|
+
function parseHTML(html, schema, options) {
|
|
24
|
+
return DOMParser.fromSchema(schema).parse(readHTML(html), options);
|
|
25
|
+
}
|
|
26
|
+
function readHTML(rawHtml) {
|
|
27
|
+
const metas = /^(\s*<meta [^>]*>)*/u.exec(rawHtml);
|
|
28
|
+
const stripped = metas ? rawHtml.slice(metas[0].length) : rawHtml;
|
|
29
|
+
const firstTag = /<([a-z][^>\s]+)/iu.exec(stripped);
|
|
30
|
+
const wrap = firstTag ? wrapMap[firstTag[1].toLowerCase()] : void 0;
|
|
31
|
+
const html = wrap === void 0 ? stripped : wrap.map((n) => `<${n}>`).join("") + stripped + wrap.map((n) => `</${n}>`).reverse().join("");
|
|
32
|
+
let elt = document.implementation.createHTMLDocument("title").body;
|
|
33
|
+
elt.innerHTML = html;
|
|
34
|
+
if (wrap) for (const n of wrap) elt = elt.querySelector(n) ?? elt;
|
|
35
|
+
return elt;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { parseHTML, renderProseMirror };
|
|
@@ -1,175 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment, Node, Slice } from "prosemirror-model";
|
|
2
2
|
import { AllSelection, EditorState, Selection, TextSelection } from "prosemirror-state";
|
|
3
3
|
import { EditorView } from "prosemirror-view";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const zeroRect = () => new DOMRect(0, 0, 0, 0);
|
|
8
|
-
const emptyRectList = () => [];
|
|
9
|
-
const rangePrototype = Range.prototype;
|
|
10
|
-
if (typeof rangePrototype.getBoundingClientRect !== "function") Range.prototype.getBoundingClientRect = zeroRect;
|
|
11
|
-
if (typeof rangePrototype.getClientRects !== "function") Range.prototype.getClientRects = emptyRectList;
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/MutationObserverMock.ts
|
|
15
|
-
var MutationObserverMock = class MutationObserverMock {
|
|
16
|
-
static activeObservers = /* @__PURE__ */ new Map();
|
|
17
|
-
callback;
|
|
18
|
-
target;
|
|
19
|
-
constructor(callback) {
|
|
20
|
-
this.callback = callback;
|
|
21
|
-
this.target = void 0;
|
|
22
|
-
}
|
|
23
|
-
static createMutation(target, mutationRecords) {
|
|
24
|
-
const observer = MutationObserverMock.activeObservers.get(target);
|
|
25
|
-
if (observer === void 0) return;
|
|
26
|
-
observer.callback(mutationRecords.map((record) => ({
|
|
27
|
-
addedNodes: [],
|
|
28
|
-
attributeName: null,
|
|
29
|
-
attributeNamespace: null,
|
|
30
|
-
nextSibling: null,
|
|
31
|
-
oldValue: null,
|
|
32
|
-
previousSibling: null,
|
|
33
|
-
removedNodes: [],
|
|
34
|
-
...record
|
|
35
|
-
})), observer);
|
|
36
|
-
}
|
|
37
|
-
disconnect() {
|
|
38
|
-
if (this.target !== void 0) MutationObserverMock.activeObservers.delete(this.target);
|
|
39
|
-
this.target = void 0;
|
|
40
|
-
}
|
|
41
|
-
observe(target) {
|
|
42
|
-
this.target = target;
|
|
43
|
-
MutationObserverMock.activeObservers.set(target, this);
|
|
44
|
-
}
|
|
45
|
-
takeRecords() {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/selection.ts
|
|
51
|
-
var resolveSelection = (doc, selection) => {
|
|
52
|
-
if (selection === "all") return new AllSelection(doc);
|
|
53
|
-
if (typeof selection === "object" && "$anchor" in selection && "$head" in selection) return selection;
|
|
54
|
-
if (typeof selection === "object" && "anchor" in selection && "head" in selection) return TextSelection.between(doc.resolve(selection.anchor), doc.resolve(selection.head));
|
|
55
|
-
if (typeof selection === "object" && "from" in selection && "to" in selection) throw new Error("selectText no longer accepts { from, to } — use { anchor, head } instead. The values map directly: { from: a, to: b } becomes { anchor: a, head: b }.");
|
|
56
|
-
let pos = 0;
|
|
57
|
-
if (selection === "end") pos = doc.nodeSize - 2;
|
|
58
|
-
else if (selection !== "start") pos = selection;
|
|
59
|
-
return TextSelection.near(doc.resolve(pos));
|
|
60
|
-
};
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region src/typing/dom.ts
|
|
63
|
-
var characterDataAt = (node, offset) => {
|
|
64
|
-
if (node instanceof CharacterData) return {
|
|
65
|
-
offset,
|
|
66
|
-
target: node
|
|
67
|
-
};
|
|
68
|
-
const after = node.childNodes[offset];
|
|
69
|
-
if (after instanceof CharacterData) return {
|
|
70
|
-
offset: 0,
|
|
71
|
-
target: after
|
|
72
|
-
};
|
|
73
|
-
return null;
|
|
74
|
-
};
|
|
75
|
-
function deleteText(view, direction) {
|
|
76
|
-
const { selection } = view.state;
|
|
77
|
-
const from = selection.empty && direction === -1 ? selection.from - 1 : selection.from;
|
|
78
|
-
const to = selection.empty && direction === 1 ? selection.to + 1 : selection.to;
|
|
79
|
-
const fromDOM = view.domAtPos(from, -1);
|
|
80
|
-
const target = characterDataAt(fromDOM.node, fromDOM.offset)?.target;
|
|
81
|
-
if (target === void 0) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
82
|
-
const nodeStart = view.posAtDOM(target, 0);
|
|
83
|
-
const fromOffset = from - nodeStart;
|
|
84
|
-
const toOffset = to - nodeStart;
|
|
85
|
-
if (fromOffset < 0 || toOffset > target.data.length) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
86
|
-
const oldValue = target.data;
|
|
87
|
-
target.data = target.data.slice(0, fromOffset) + target.data.slice(toOffset);
|
|
88
|
-
MutationObserverMock.createMutation(view.dom, [{
|
|
89
|
-
oldValue,
|
|
90
|
-
target,
|
|
91
|
-
type: "characterData"
|
|
92
|
-
}]);
|
|
93
|
-
}
|
|
94
|
-
function moveCaret(view, direction) {
|
|
95
|
-
const { selection } = view.state;
|
|
96
|
-
let pos = direction === -1 ? selection.from : selection.to;
|
|
97
|
-
if (selection.empty) pos += direction;
|
|
98
|
-
view.dispatch(view.state.tr.setSelection(Selection.near(view.state.doc.resolve(pos), direction)));
|
|
99
|
-
}
|
|
100
|
-
function typeCharacter(view, character) {
|
|
101
|
-
const { node, offset: domOffset } = view.domAtPos(view.state.selection.from, -1);
|
|
102
|
-
const point = characterDataAt(node, domOffset);
|
|
103
|
-
if (point !== null) {
|
|
104
|
-
const { offset, target } = point;
|
|
105
|
-
const oldValue = target.data;
|
|
106
|
-
target.data = target.data.slice(0, offset) + character + target.data.slice(offset);
|
|
107
|
-
MutationObserverMock.createMutation(view.dom, [{
|
|
108
|
-
oldValue,
|
|
109
|
-
target,
|
|
110
|
-
type: "characterData"
|
|
111
|
-
}]);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const textNode = new Text(character);
|
|
115
|
-
const nextSibling = node.childNodes.item(domOffset);
|
|
116
|
-
node.insertBefore(textNode, nextSibling);
|
|
117
|
-
MutationObserverMock.createMutation(view.dom, [{
|
|
118
|
-
addedNodes: [textNode],
|
|
119
|
-
nextSibling,
|
|
120
|
-
previousSibling: textNode.previousSibling,
|
|
121
|
-
target: node,
|
|
122
|
-
type: "childList"
|
|
123
|
-
}]);
|
|
124
|
-
}
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/typing/keyboardInput.ts
|
|
127
|
-
function tokenizeKeyboardInput(input) {
|
|
128
|
-
const output = [];
|
|
129
|
-
let currentGroupOpener = null;
|
|
130
|
-
let group = "";
|
|
131
|
-
for (const char of input) if (currentGroupOpener !== null) {
|
|
132
|
-
if (["]", "}"].includes(char)) {
|
|
133
|
-
if (group.endsWith("\\") && char === matchingBrace(currentGroupOpener)) group = group.slice(0, -2) + char;
|
|
134
|
-
else if (char === matchingBrace(currentGroupOpener)) {
|
|
135
|
-
if (group.length === 4 && group.startsWith("Key")) output.push(group.slice(3).toLowerCase());
|
|
136
|
-
else output.push(group);
|
|
137
|
-
currentGroupOpener = null;
|
|
138
|
-
group = "";
|
|
139
|
-
} else group += char;
|
|
140
|
-
} else if (group === "" && currentGroupOpener === char) {
|
|
141
|
-
output.push(char);
|
|
142
|
-
currentGroupOpener = null;
|
|
143
|
-
group = "";
|
|
144
|
-
} else group += char;
|
|
145
|
-
} else if (["[", "{"].includes(char)) currentGroupOpener = char;
|
|
146
|
-
else output.push(char);
|
|
147
|
-
output.forEach(assertSupported);
|
|
148
|
-
return output;
|
|
149
|
-
}
|
|
150
|
-
function assertSupported(character) {
|
|
151
|
-
if (/^\/.+/u.exec(character) || /.+>[\d]*\/?$/u.exec(character)) throw new Error("Unsupported keyboard input");
|
|
152
|
-
}
|
|
153
|
-
function matchingBrace(opener) {
|
|
154
|
-
return opener === "{" ? "}" : "]";
|
|
155
|
-
}
|
|
156
|
-
//#endregion
|
|
157
|
-
//#region src/typing/keyChord.ts
|
|
158
|
-
function parseKeyChord(token) {
|
|
159
|
-
const parts = token.split(/-(?!$)/u);
|
|
160
|
-
const key = parts[parts.length - 1] === "Space" ? " " : parts[parts.length - 1];
|
|
161
|
-
const modifiers = {};
|
|
162
|
-
for (const modifier of parts.slice(0, -1)) if (/^(cmd|meta|m)$/iu.test(modifier)) modifiers.metaKey = true;
|
|
163
|
-
else if (/^a(lt)?$/iu.test(modifier)) modifiers.altKey = true;
|
|
164
|
-
else if (/^(c|ctrl|control)$/iu.test(modifier)) modifiers.ctrlKey = true;
|
|
165
|
-
else if (/^s(hift)?$/iu.test(modifier)) modifiers.shiftKey = true;
|
|
166
|
-
else if (/^mod$/iu.test(modifier)) modifiers.ctrlKey = true;
|
|
167
|
-
else throw new Error(`Unrecognized modifier name: ${modifier}`);
|
|
4
|
+
//#region src/clipboard/copy.ts
|
|
5
|
+
var copy = (view) => {
|
|
6
|
+
const { dom, text } = view.serializeForClipboard(view.state.selection.content());
|
|
168
7
|
return {
|
|
169
|
-
|
|
170
|
-
|
|
8
|
+
html: dom.innerHTML,
|
|
9
|
+
text
|
|
171
10
|
};
|
|
172
|
-
}
|
|
11
|
+
};
|
|
173
12
|
//#endregion
|
|
174
13
|
//#region src/typing/keyIdentity.ts
|
|
175
14
|
var namedKeys = new Map(Object.entries({
|
|
@@ -488,6 +327,351 @@ function keyIdentity(key) {
|
|
|
488
327
|
};
|
|
489
328
|
}
|
|
490
329
|
//#endregion
|
|
330
|
+
//#region src/clipboard/ClipboardEventMock.ts
|
|
331
|
+
var ClipboardEventMock = class extends Event {
|
|
332
|
+
clipboardData;
|
|
333
|
+
constructor(type, clipboardData) {
|
|
334
|
+
super(type, {
|
|
335
|
+
bubbles: true,
|
|
336
|
+
cancelable: true,
|
|
337
|
+
composed: true
|
|
338
|
+
});
|
|
339
|
+
this.clipboardData = clipboardData;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/clipboard/DataTransferItemMock.ts
|
|
344
|
+
var DataTransferItemMock = class {
|
|
345
|
+
kind = "file";
|
|
346
|
+
type;
|
|
347
|
+
file;
|
|
348
|
+
constructor(file) {
|
|
349
|
+
this.file = file;
|
|
350
|
+
this.type = file.type;
|
|
351
|
+
}
|
|
352
|
+
getAsFile() {
|
|
353
|
+
return this.file;
|
|
354
|
+
}
|
|
355
|
+
getAsString() {}
|
|
356
|
+
webkitGetAsEntry() {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/clipboard/DataTransferMock.ts
|
|
362
|
+
var normalizeType = (format) => format.toLowerCase() === "text" ? "text/plain" : format.toLowerCase();
|
|
363
|
+
var DataTransferMock = class {
|
|
364
|
+
dropEffect = "none";
|
|
365
|
+
effectAllowed = "uninitialized";
|
|
366
|
+
files;
|
|
367
|
+
items;
|
|
368
|
+
get types() {
|
|
369
|
+
return this.files.length > 0 ? [...this.store.keys(), "Files"] : [...this.store.keys()];
|
|
370
|
+
}
|
|
371
|
+
store = /* @__PURE__ */ new Map();
|
|
372
|
+
constructor(files = []) {
|
|
373
|
+
this.files = Object.assign([...files], { item: (index) => files[index] ?? null });
|
|
374
|
+
this.items = Object.assign(files.map((file) => new DataTransferItemMock(file)), {
|
|
375
|
+
add: () => null,
|
|
376
|
+
clear: () => void 0,
|
|
377
|
+
remove: () => {
|
|
378
|
+
throw new DOMException("The DataTransferItemList is not in the read/write mode.", "InvalidStateError");
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
clearData(format) {
|
|
383
|
+
if (format === void 0) this.store.clear();
|
|
384
|
+
else this.store.delete(normalizeType(format));
|
|
385
|
+
}
|
|
386
|
+
getData(format) {
|
|
387
|
+
return this.store.get(normalizeType(format)) ?? "";
|
|
388
|
+
}
|
|
389
|
+
setData(format, data) {
|
|
390
|
+
this.store.set(normalizeType(format), data);
|
|
391
|
+
}
|
|
392
|
+
setDragImage() {
|
|
393
|
+
throw new Error("setDragImage is not supported: vitest-prosemirror models clipboard paste/copy, not drag-and-drop.");
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/clipboard/paste.ts
|
|
398
|
+
var dispatchShift = (view, type) => {
|
|
399
|
+
const identity = keyIdentity("Shift");
|
|
400
|
+
view.dispatchEvent(new KeyboardEvent(type, {
|
|
401
|
+
bubbles: true,
|
|
402
|
+
cancelable: true,
|
|
403
|
+
code: identity.code,
|
|
404
|
+
composed: true,
|
|
405
|
+
key: identity.key,
|
|
406
|
+
keyCode: identity.keyCode,
|
|
407
|
+
location: identity.location,
|
|
408
|
+
shiftKey: true
|
|
409
|
+
}));
|
|
410
|
+
};
|
|
411
|
+
var normalize = (view, content) => {
|
|
412
|
+
if (typeof content === "string") return { text: content };
|
|
413
|
+
if (content instanceof Node) {
|
|
414
|
+
const { dom, text } = view.serializeForClipboard(new Slice(Fragment.from(content), 0, 0));
|
|
415
|
+
return {
|
|
416
|
+
html: dom.innerHTML,
|
|
417
|
+
text
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return content;
|
|
421
|
+
};
|
|
422
|
+
var paste = (view, content) => {
|
|
423
|
+
const { files, html, plainText, text } = normalize(view, content);
|
|
424
|
+
const data = new DataTransferMock(files);
|
|
425
|
+
if (text !== void 0) data.setData("text/plain", text);
|
|
426
|
+
if (html !== void 0) data.setData("text/html", html);
|
|
427
|
+
if (plainText === true) dispatchShift(view, "keydown");
|
|
428
|
+
view.dom.dispatchEvent(new ClipboardEventMock("paste", data));
|
|
429
|
+
if (plainText === true) dispatchShift(view, "keyup");
|
|
430
|
+
};
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/domInteraction.ts
|
|
433
|
+
var clickHandlerProps = [
|
|
434
|
+
"handleClick",
|
|
435
|
+
"handleClickOn",
|
|
436
|
+
"handleDoubleClick",
|
|
437
|
+
"handleDoubleClickOn",
|
|
438
|
+
"handleTripleClick",
|
|
439
|
+
"handleTripleClickOn"
|
|
440
|
+
];
|
|
441
|
+
var element = (view, selector) => {
|
|
442
|
+
const found = view.dom.querySelector(selector);
|
|
443
|
+
if (found === null) throw new Error(`No element matching "${selector}" was found in the editor DOM.`);
|
|
444
|
+
return found;
|
|
445
|
+
};
|
|
446
|
+
var elements = (view, selector) => [...view.dom.querySelectorAll(selector)];
|
|
447
|
+
var click = (view, target) => {
|
|
448
|
+
if (clickHandlerProps.some((prop) => view.someProp(prop, () => true) === true)) throw new Error("click() cannot reach handleClick / handleClickOn / handleDoubleClick* / handleTripleClick* handlers: jsdom has no layout, so posAtCoords is always null and ProseMirror's mousedown handler bails before calling them. Click a node view's own DOM element instead.");
|
|
449
|
+
const target_ = typeof target === "string" ? element(view, target) : target;
|
|
450
|
+
const event = new MouseEvent("click", {
|
|
451
|
+
bubbles: true,
|
|
452
|
+
cancelable: true
|
|
453
|
+
});
|
|
454
|
+
target_.dispatchEvent(event);
|
|
455
|
+
return event.defaultPrevented;
|
|
456
|
+
};
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/mockRangeRects.ts
|
|
459
|
+
var mockRangeRects = () => {
|
|
460
|
+
const zeroRect = () => new DOMRect(0, 0, 0, 0);
|
|
461
|
+
const emptyRectList = () => [];
|
|
462
|
+
const rangePrototype = Range.prototype;
|
|
463
|
+
if (typeof rangePrototype.getBoundingClientRect !== "function") Range.prototype.getBoundingClientRect = zeroRect;
|
|
464
|
+
if (typeof rangePrototype.getClientRects !== "function") Range.prototype.getClientRects = emptyRectList;
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/MutationObserverMock.ts
|
|
468
|
+
var MutationObserverMock = class MutationObserverMock {
|
|
469
|
+
static activeObservers = /* @__PURE__ */ new Map();
|
|
470
|
+
callback;
|
|
471
|
+
target;
|
|
472
|
+
constructor(callback) {
|
|
473
|
+
this.callback = callback;
|
|
474
|
+
this.target = void 0;
|
|
475
|
+
}
|
|
476
|
+
static createMutation(target, mutationRecords) {
|
|
477
|
+
const observer = MutationObserverMock.activeObservers.get(target);
|
|
478
|
+
if (observer === void 0) return;
|
|
479
|
+
observer.callback(mutationRecords.map((record) => ({
|
|
480
|
+
addedNodes: [],
|
|
481
|
+
attributeName: null,
|
|
482
|
+
attributeNamespace: null,
|
|
483
|
+
nextSibling: null,
|
|
484
|
+
oldValue: null,
|
|
485
|
+
previousSibling: null,
|
|
486
|
+
removedNodes: [],
|
|
487
|
+
...record
|
|
488
|
+
})), observer);
|
|
489
|
+
}
|
|
490
|
+
disconnect() {
|
|
491
|
+
if (this.target !== void 0) MutationObserverMock.activeObservers.delete(this.target);
|
|
492
|
+
this.target = void 0;
|
|
493
|
+
}
|
|
494
|
+
observe(target) {
|
|
495
|
+
this.target = target;
|
|
496
|
+
MutationObserverMock.activeObservers.set(target, this);
|
|
497
|
+
}
|
|
498
|
+
takeRecords() {
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/selection.ts
|
|
504
|
+
var tagsOf = (doc) => doc.tag ?? {};
|
|
505
|
+
var resolvePos = (doc, value) => {
|
|
506
|
+
if (typeof value === "number") return value;
|
|
507
|
+
const tags = tagsOf(doc);
|
|
508
|
+
if (value in tags) return tags[value];
|
|
509
|
+
const available = Object.keys(tags);
|
|
510
|
+
throw new Error(`setSelection: no tag named "${value}" in the document. ${available.length > 0 ? `Available tags: ${available.map((tag) => `"${tag}"`).join(", ")}.` : "The document has no tags."}`);
|
|
511
|
+
};
|
|
512
|
+
var resolveSelection = (doc, selection) => {
|
|
513
|
+
if (typeof selection === "object" && "$anchor" in selection && "$head" in selection) return selection;
|
|
514
|
+
if (typeof selection === "object" && "anchor" in selection && "head" in selection) return TextSelection.between(doc.resolve(resolvePos(doc, selection.anchor)), doc.resolve(resolvePos(doc, selection.head)));
|
|
515
|
+
if (typeof selection === "object" && "from" in selection && "to" in selection) throw new Error("setSelection no longer accepts { from, to } — use { anchor, head } instead. The values map directly: { from: a, to: b } becomes { anchor: a, head: b }.");
|
|
516
|
+
if (typeof selection === "number") return TextSelection.near(doc.resolve(selection));
|
|
517
|
+
const tags = tagsOf(doc);
|
|
518
|
+
if ([
|
|
519
|
+
"all",
|
|
520
|
+
"end",
|
|
521
|
+
"start"
|
|
522
|
+
].includes(selection)) {
|
|
523
|
+
if (selection in tags) throw new Error(`setSelection: "${selection}" is both a reserved selection and a tag in the document. Use { anchor, head } to select the tag, or rename the tag.`);
|
|
524
|
+
if (selection === "all") return new AllSelection(doc);
|
|
525
|
+
return TextSelection.near(doc.resolve(selection === "end" ? doc.nodeSize - 2 : 0));
|
|
526
|
+
}
|
|
527
|
+
return TextSelection.near(doc.resolve(resolvePos(doc, selection)));
|
|
528
|
+
};
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/typing/direction.ts
|
|
531
|
+
var rtl = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Samaritan}\p{Script=Mandaic}]/u;
|
|
532
|
+
function containsRTL(text) {
|
|
533
|
+
return rtl.test(text);
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/typing/dom.ts
|
|
537
|
+
var characterDataAt = (node, offset) => {
|
|
538
|
+
if (node instanceof CharacterData) return {
|
|
539
|
+
offset,
|
|
540
|
+
target: node
|
|
541
|
+
};
|
|
542
|
+
const after = node.childNodes[offset];
|
|
543
|
+
if (after instanceof CharacterData) return {
|
|
544
|
+
offset: 0,
|
|
545
|
+
target: after
|
|
546
|
+
};
|
|
547
|
+
return null;
|
|
548
|
+
};
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/typing/grapheme.ts
|
|
551
|
+
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
552
|
+
var graphemeBoundary = (text, offset, direction) => {
|
|
553
|
+
const segment = segmenter.segment(text).containing(direction > 0 ? offset : offset - 1);
|
|
554
|
+
if (segment === void 0) return null;
|
|
555
|
+
return direction > 0 ? segment.index + segment.segment.length : segment.index;
|
|
556
|
+
};
|
|
557
|
+
function deleteText(view, direction) {
|
|
558
|
+
const { selection } = view.state;
|
|
559
|
+
const from = selection.empty && direction === -1 ? selection.from - 1 : selection.from;
|
|
560
|
+
const to = selection.empty && direction === 1 ? selection.to + 1 : selection.to;
|
|
561
|
+
const fromDOM = view.domAtPos(from, -1);
|
|
562
|
+
const target = characterDataAt(fromDOM.node, fromDOM.offset)?.target;
|
|
563
|
+
if (target === void 0) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
564
|
+
const nodeStart = view.posAtDOM(target, 0);
|
|
565
|
+
const fromOffset = from - nodeStart;
|
|
566
|
+
const toOffset = to - nodeStart;
|
|
567
|
+
if (fromOffset < 0 || toOffset > target.data.length) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
568
|
+
const oldValue = target.data;
|
|
569
|
+
target.data = target.data.slice(0, fromOffset) + target.data.slice(toOffset);
|
|
570
|
+
MutationObserverMock.createMutation(view.dom, [{
|
|
571
|
+
oldValue,
|
|
572
|
+
target,
|
|
573
|
+
type: "characterData"
|
|
574
|
+
}]);
|
|
575
|
+
}
|
|
576
|
+
function moveCaret(view, direction, extend) {
|
|
577
|
+
const { doc, selection } = view.state;
|
|
578
|
+
if (extend) {
|
|
579
|
+
const head = graphemeStep(view, selection.head, direction);
|
|
580
|
+
view.dispatch(view.state.tr.setSelection(TextSelection.between(doc.resolve(selection.anchor), doc.resolve(head), direction)));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
let pos = direction === -1 ? selection.from : selection.to;
|
|
584
|
+
if (selection.empty) pos = graphemeStep(view, pos, direction);
|
|
585
|
+
else if (containsRTL(doc.textBetween(selection.from, selection.to))) throwRTL(direction);
|
|
586
|
+
view.dispatch(view.state.tr.setSelection(Selection.near(doc.resolve(pos), direction)));
|
|
587
|
+
}
|
|
588
|
+
function typeCharacter(view, character) {
|
|
589
|
+
if (!view.state.selection.empty) deleteText(view, -1);
|
|
590
|
+
const { node, offset: domOffset } = view.domAtPos(view.state.selection.from, -1);
|
|
591
|
+
const point = characterDataAt(node, domOffset);
|
|
592
|
+
if (point !== null) {
|
|
593
|
+
const { offset, target } = point;
|
|
594
|
+
const oldValue = target.data;
|
|
595
|
+
target.data = target.data.slice(0, offset) + character + target.data.slice(offset);
|
|
596
|
+
MutationObserverMock.createMutation(view.dom, [{
|
|
597
|
+
oldValue,
|
|
598
|
+
target,
|
|
599
|
+
type: "characterData"
|
|
600
|
+
}]);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const textNode = new Text(character);
|
|
604
|
+
const nextSibling = node.childNodes.item(domOffset);
|
|
605
|
+
node.insertBefore(textNode, nextSibling);
|
|
606
|
+
MutationObserverMock.createMutation(view.dom, [{
|
|
607
|
+
addedNodes: [textNode],
|
|
608
|
+
nextSibling,
|
|
609
|
+
previousSibling: textNode.previousSibling,
|
|
610
|
+
target: node,
|
|
611
|
+
type: "childList"
|
|
612
|
+
}]);
|
|
613
|
+
}
|
|
614
|
+
function graphemeStep(view, pos, direction) {
|
|
615
|
+
const dom = view.domAtPos(pos, direction);
|
|
616
|
+
const point = characterDataAt(dom.node, dom.offset);
|
|
617
|
+
if (point === null) return pos + direction;
|
|
618
|
+
if (containsRTL(point.target.data)) throwRTL(direction);
|
|
619
|
+
const nodeStart = view.posAtDOM(point.target, 0);
|
|
620
|
+
const boundary = graphemeBoundary(point.target.data, pos - nodeStart, direction);
|
|
621
|
+
return boundary === null ? pos + direction : nodeStart + boundary;
|
|
622
|
+
}
|
|
623
|
+
function throwRTL(direction) {
|
|
624
|
+
throw new Error(`Cannot simulate the "${direction === -1 ? "ArrowLeft" : "ArrowRight"}" key in right-to-left text.`);
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/typing/keyboardInput.ts
|
|
628
|
+
function tokenizeKeyboardInput(input) {
|
|
629
|
+
const output = [];
|
|
630
|
+
let currentGroupOpener = null;
|
|
631
|
+
let group = "";
|
|
632
|
+
for (const char of input) if (currentGroupOpener !== null) {
|
|
633
|
+
if (["]", "}"].includes(char)) {
|
|
634
|
+
if (group.endsWith("\\") && char === matchingBrace(currentGroupOpener)) group = group.slice(0, -2) + char;
|
|
635
|
+
else if (char === matchingBrace(currentGroupOpener)) {
|
|
636
|
+
if (group.length === 4 && group.startsWith("Key")) output.push(group.slice(3).toLowerCase());
|
|
637
|
+
else output.push(group);
|
|
638
|
+
currentGroupOpener = null;
|
|
639
|
+
group = "";
|
|
640
|
+
} else group += char;
|
|
641
|
+
} else if (group === "" && currentGroupOpener === char) {
|
|
642
|
+
output.push(char);
|
|
643
|
+
currentGroupOpener = null;
|
|
644
|
+
group = "";
|
|
645
|
+
} else group += char;
|
|
646
|
+
} else if (["[", "{"].includes(char)) currentGroupOpener = char;
|
|
647
|
+
else output.push(char);
|
|
648
|
+
output.forEach(assertSupported);
|
|
649
|
+
return output;
|
|
650
|
+
}
|
|
651
|
+
function assertSupported(character) {
|
|
652
|
+
if (/^\/.+/u.exec(character) || /.+>[\d]*\/?$/u.exec(character)) throw new Error("Unsupported keyboard input");
|
|
653
|
+
}
|
|
654
|
+
function matchingBrace(opener) {
|
|
655
|
+
return opener === "{" ? "}" : "]";
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/typing/keyChord.ts
|
|
659
|
+
function parseKeyChord(token) {
|
|
660
|
+
const parts = token.split(/-(?!$)/u);
|
|
661
|
+
const key = parts[parts.length - 1] === "Space" ? " " : parts[parts.length - 1];
|
|
662
|
+
const modifiers = {};
|
|
663
|
+
for (const modifier of parts.slice(0, -1)) if (/^(cmd|meta|m)$/iu.test(modifier)) modifiers.metaKey = true;
|
|
664
|
+
else if (/^a(lt)?$/iu.test(modifier)) modifiers.altKey = true;
|
|
665
|
+
else if (/^(c|ctrl|control)$/iu.test(modifier)) modifiers.ctrlKey = true;
|
|
666
|
+
else if (/^s(hift)?$/iu.test(modifier)) modifiers.shiftKey = true;
|
|
667
|
+
else if (/^mod$/iu.test(modifier)) modifiers.ctrlKey = true;
|
|
668
|
+
else throw new Error(`Unrecognized modifier name: ${modifier}`);
|
|
669
|
+
return {
|
|
670
|
+
key,
|
|
671
|
+
modifiers
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
491
675
|
//#region src/typing/typing.ts
|
|
492
676
|
var ignoredKeys = /* @__PURE__ */ new Set([
|
|
493
677
|
"Alt",
|
|
@@ -500,7 +684,7 @@ var ignoredKeys = /* @__PURE__ */ new Set([
|
|
|
500
684
|
var hasModifiers = (modifiers) => modifiers !== void 0 && Object.values(modifiers).includes(true);
|
|
501
685
|
var suppressesCharacter = (modifiers) => modifiers?.altKey === true || modifiers?.ctrlKey === true || modifiers?.metaKey === true;
|
|
502
686
|
var isLetter = (key) => /^[a-z]$/iu.test(key);
|
|
503
|
-
function
|
|
687
|
+
function type(view, text) {
|
|
504
688
|
for (const token of tokenizeKeyboardInput(text)) {
|
|
505
689
|
const { key, modifiers } = parseKeyChord(token);
|
|
506
690
|
const character = isLetter(key) && modifiers.shiftKey === true ? key.toUpperCase() : key;
|
|
@@ -523,7 +707,7 @@ function insertText(view, text) {
|
|
|
523
707
|
if (!keydownEvent.defaultPrevented) {
|
|
524
708
|
const action = keyAction(character, modifiers);
|
|
525
709
|
if (action.type === "delete") deleteText(view, action.direction);
|
|
526
|
-
else if (action.type === "moveCaret") moveCaret(view, action.direction);
|
|
710
|
+
else if (action.type === "moveCaret") moveCaret(view, action.direction, action.extend);
|
|
527
711
|
else if (action.type === "type") {
|
|
528
712
|
view.dispatchEvent(new KeyboardEvent("keypress", {
|
|
529
713
|
...eventInit,
|
|
@@ -546,10 +730,19 @@ function keyAction(key, modifiers) {
|
|
|
546
730
|
direction: 1,
|
|
547
731
|
type: "delete"
|
|
548
732
|
};
|
|
549
|
-
if (
|
|
550
|
-
direction
|
|
551
|
-
|
|
552
|
-
|
|
733
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
734
|
+
const direction = key === "ArrowLeft" ? -1 : 1;
|
|
735
|
+
if (!hasModifiers(modifiers)) return {
|
|
736
|
+
direction,
|
|
737
|
+
extend: false,
|
|
738
|
+
type: "moveCaret"
|
|
739
|
+
};
|
|
740
|
+
if (modifiers?.shiftKey === true && !suppressesCharacter(modifiers)) return {
|
|
741
|
+
direction,
|
|
742
|
+
extend: true,
|
|
743
|
+
type: "moveCaret"
|
|
744
|
+
};
|
|
745
|
+
}
|
|
553
746
|
if (key.length === 1) {
|
|
554
747
|
if (suppressesCharacter(modifiers)) return { type: "ignore" };
|
|
555
748
|
return {
|
|
@@ -560,14 +753,14 @@ function keyAction(key, modifiers) {
|
|
|
560
753
|
throw new Error(`Cannot simulate the "${key}" key`);
|
|
561
754
|
}
|
|
562
755
|
//#endregion
|
|
563
|
-
//#region src/
|
|
756
|
+
//#region src/renderProseMirror.ts
|
|
564
757
|
var originalMutationObserver = global.MutationObserver;
|
|
565
|
-
var
|
|
566
|
-
var
|
|
567
|
-
var
|
|
568
|
-
for (const
|
|
758
|
+
var liveEditors = /* @__PURE__ */ new Set();
|
|
759
|
+
var autoCleanupEditors = /* @__PURE__ */ new Set();
|
|
760
|
+
var cleanupEditors = () => {
|
|
761
|
+
for (const editor of [...autoCleanupEditors]) editor.destroy();
|
|
569
762
|
};
|
|
570
|
-
var
|
|
763
|
+
var ProseMirrorEditorImpl = class {
|
|
571
764
|
get doc() {
|
|
572
765
|
this.assertAlive();
|
|
573
766
|
return this.view.state.doc;
|
|
@@ -585,108 +778,79 @@ var ProseMirrorTester = class {
|
|
|
585
778
|
return this.view.dom.textContent;
|
|
586
779
|
}
|
|
587
780
|
destroyed = false;
|
|
588
|
-
|
|
781
|
+
mountPoint;
|
|
589
782
|
view;
|
|
590
783
|
constructor(documentRoot, options = {}) {
|
|
591
784
|
if (typeof document === "undefined") throw new Error("TODO");
|
|
592
|
-
this.
|
|
593
|
-
document.body.append(this.
|
|
785
|
+
this.mountPoint = document.createElement("div");
|
|
786
|
+
document.body.append(this.mountPoint);
|
|
787
|
+
const { autoCleanup = true, editorProps = {} } = options;
|
|
788
|
+
const { plugins = [], ...directEditorProps } = editorProps;
|
|
594
789
|
const state = EditorState.create({
|
|
595
790
|
doc: documentRoot,
|
|
596
|
-
plugins
|
|
791
|
+
plugins
|
|
597
792
|
});
|
|
598
793
|
global.MutationObserver = MutationObserverMock;
|
|
599
794
|
mockRangeRects();
|
|
600
|
-
this.view = new EditorView(this.
|
|
601
|
-
|
|
602
|
-
|
|
795
|
+
this.view = new EditorView(this.mountPoint, {
|
|
796
|
+
state,
|
|
797
|
+
...directEditorProps
|
|
798
|
+
});
|
|
799
|
+
liveEditors.add(this);
|
|
800
|
+
if (autoCleanup) autoCleanupEditors.add(this);
|
|
801
|
+
}
|
|
802
|
+
click(target) {
|
|
803
|
+
this.assertAlive();
|
|
804
|
+
return click(this.view, target);
|
|
805
|
+
}
|
|
806
|
+
command(command) {
|
|
807
|
+
this.assertAlive();
|
|
808
|
+
return command(this.view.state, this.view.dispatch.bind(this.view), this.view);
|
|
809
|
+
}
|
|
810
|
+
copy() {
|
|
811
|
+
this.assertAlive();
|
|
812
|
+
return copy(this.view);
|
|
603
813
|
}
|
|
604
814
|
destroy() {
|
|
605
815
|
if (this.destroyed) return;
|
|
606
816
|
this.destroyed = true;
|
|
607
817
|
this.view.destroy();
|
|
608
|
-
this.
|
|
818
|
+
this.mountPoint.remove();
|
|
609
819
|
document.getSelection()?.removeAllRanges();
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (
|
|
820
|
+
autoCleanupEditors.delete(this);
|
|
821
|
+
liveEditors.delete(this);
|
|
822
|
+
if (liveEditors.size === 0) global.MutationObserver = originalMutationObserver;
|
|
823
|
+
}
|
|
824
|
+
element(selector) {
|
|
825
|
+
this.assertAlive();
|
|
826
|
+
return element(this.view, selector);
|
|
613
827
|
}
|
|
614
|
-
|
|
828
|
+
elements(selector) {
|
|
615
829
|
this.assertAlive();
|
|
616
|
-
|
|
830
|
+
return elements(this.view, selector);
|
|
617
831
|
}
|
|
618
|
-
|
|
832
|
+
paste(content) {
|
|
833
|
+
this.assertAlive();
|
|
834
|
+
paste(this.view, content);
|
|
835
|
+
}
|
|
836
|
+
setSelection(selection) {
|
|
619
837
|
this.assertAlive();
|
|
620
838
|
this.view.dispatch(this.view.state.tr.setSelection(resolveSelection(this.doc, selection)));
|
|
621
839
|
}
|
|
840
|
+
type(text) {
|
|
841
|
+
this.assertAlive();
|
|
842
|
+
type(this.view, text);
|
|
843
|
+
}
|
|
622
844
|
assertAlive() {
|
|
623
|
-
if (this.destroyed) throw new Error("This
|
|
845
|
+
if (this.destroyed) throw new Error("This ProseMirror editor has been destroyed. Editors are destroyed automatically after each test; pass { autoCleanup: false } to keep one alive (e.g. across a beforeAll).");
|
|
624
846
|
}
|
|
625
847
|
};
|
|
848
|
+
/**
|
|
849
|
+
* Mount a ProseMirror `EditorView` on a jsdom node and return a
|
|
850
|
+
* {@link ProseMirrorEditor} handle for driving it. Installs the mocks the
|
|
851
|
+
* tester relies on (`MutationObserver`, range rects) and registers the editor
|
|
852
|
+
* for automatic cleanup after each test unless `autoCleanup: false` is passed.
|
|
853
|
+
*/
|
|
854
|
+
var renderProseMirror = (documentRoot, options = {}) => new ProseMirrorEditorImpl(documentRoot, options);
|
|
626
855
|
//#endregion
|
|
627
|
-
|
|
628
|
-
function stringifyProseMirrorNode(node, indentation = "") {
|
|
629
|
-
if (node.type.spec.toDebugString !== void 0) return `${indentation}${wrapMarks(node.marks, node.type.spec.toDebugString(node))}`;
|
|
630
|
-
if (node.type.name === "text") return `${indentation}${wrapMarks(node.marks, escapeText(node.textContent))}`;
|
|
631
|
-
const type = node.type.name;
|
|
632
|
-
const content = [];
|
|
633
|
-
const nextIndentation = `${indentation} `;
|
|
634
|
-
if (Object.keys(node.attrs).length > 0) content.push(`${nextIndentation}${stringifyObject(node.attrs, {
|
|
635
|
-
indent: " ",
|
|
636
|
-
inlineCharacterLimit: 1e3
|
|
637
|
-
})},`);
|
|
638
|
-
node.content.forEach((item) => {
|
|
639
|
-
content.push(`${stringifyProseMirrorNode(item, nextIndentation)},`);
|
|
640
|
-
});
|
|
641
|
-
let body;
|
|
642
|
-
if (content.length === 0) body = `${type}()`;
|
|
643
|
-
else if (content.length === 1 && node.content.firstChild?.type.name === "text") body = `${type}(${stringifyProseMirrorNode(node.content.firstChild, "")})`;
|
|
644
|
-
else body = `${type}(\n${content.join("\n")}\n${indentation})`;
|
|
645
|
-
return `${indentation}${wrapMarks(node.marks, body)}`;
|
|
646
|
-
}
|
|
647
|
-
function escapeText(text) {
|
|
648
|
-
return `'${text.replace(/[\p{Cc}'\\]/gu, (char) => {
|
|
649
|
-
switch (char) {
|
|
650
|
-
case "\f": return "\\f";
|
|
651
|
-
case "\n": return "\\n";
|
|
652
|
-
case "\r": return "\\r";
|
|
653
|
-
case " ": return "\\t";
|
|
654
|
-
case "\b": return "\\b";
|
|
655
|
-
case "'": return "\\'";
|
|
656
|
-
case "\\": return "\\\\";
|
|
657
|
-
default: return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
658
|
-
}
|
|
659
|
-
})}'`;
|
|
660
|
-
}
|
|
661
|
-
function wrapMarks(marks, origContent) {
|
|
662
|
-
let content = origContent;
|
|
663
|
-
for (const mark of [...marks].reverse()) {
|
|
664
|
-
const hasAttrs = Object.keys(mark.attrs).length > 0;
|
|
665
|
-
const items = [content];
|
|
666
|
-
if (hasAttrs) items.unshift(stringifyObject(mark.attrs, {
|
|
667
|
-
indent: " ",
|
|
668
|
-
inlineCharacterLimit: 1e3
|
|
669
|
-
}));
|
|
670
|
-
content = `${mark.type.name}(${items.join(", ")})`;
|
|
671
|
-
}
|
|
672
|
-
return content;
|
|
673
|
-
}
|
|
674
|
-
//#endregion
|
|
675
|
-
//#region src/index.ts
|
|
676
|
-
afterEach(cleanupTesters);
|
|
677
|
-
expect.extend({ toEqualProseMirrorNode(received, expected) {
|
|
678
|
-
const receivedDoc = `\n${stringifyProseMirrorNode(received)}\n`;
|
|
679
|
-
const expectedDoc = `\n${stringifyProseMirrorNode(expected)}\n`;
|
|
680
|
-
const sameSchema = received.type.schema === expected.type.schema;
|
|
681
|
-
const pass = sameSchema && this.equals(receivedDoc, expectedDoc);
|
|
682
|
-
return {
|
|
683
|
-
message: pass ? () => `${this.utils.matcherHint(".not.toEqualProsemirrorNode")}\n\nExpected value of document to not equal:\n ${this.utils.printExpected(expectedDoc)}\nActual:\n ${this.utils.printReceived(receivedDoc)}` : () => {
|
|
684
|
-
if (!sameSchema && this.equals(receivedDoc, expectedDoc)) return `${this.utils.matcherHint(".toEqualProsemirrorNode")}\n\nThe documents stringify identically but come from different schemas:\n${this.utils.printReceived(receivedDoc)}`;
|
|
685
|
-
const diffString = this.utils.diff(expectedDoc, receivedDoc);
|
|
686
|
-
return `${this.utils.matcherHint(".toEqualProsemirrorNode")}\n\nExpected value of document to equal:\n${this.utils.printExpected(expectedDoc)}\nActual:\n${this.utils.printReceived(receivedDoc)}${diffString === void 0 ? "" : `\n\nDifference:\n\n${diffString}`}`;
|
|
687
|
-
},
|
|
688
|
-
pass
|
|
689
|
-
};
|
|
690
|
-
} });
|
|
691
|
-
//#endregion
|
|
692
|
-
export { ProseMirrorTester };
|
|
856
|
+
export { resolveSelection as a, containsRTL as i, cleanupEditors as n, renderProseMirror as r, ProseMirrorEditorImpl as t };
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Node } from "prosemirror-model";
|
|
2
|
+
|
|
3
|
+
import type { TesterSelection } from "./index.js";
|
|
4
|
+
|
|
5
|
+
import "vitest";
|
|
6
|
+
|
|
7
|
+
export interface CustomMatchers<R = unknown> {
|
|
8
|
+
toEqualProseMirrorNode(expected: Node): R;
|
|
9
|
+
toHaveSelection(expected: TesterSelection): R;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars -- This is an override for vitest matchers; the type parameters must match vitest's Matchers signature exactly */
|
|
13
|
+
|
|
14
|
+
declare module "vitest" {
|
|
15
|
+
interface Matchers<
|
|
16
|
+
R extends Promise<void> | void = Promise<void> | void,
|
|
17
|
+
T = unknown,
|
|
18
|
+
> extends CustomMatchers<R> {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* eslint-enable */
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { a as resolveSelection, i as containsRTL, n as cleanupEditors, t as ProseMirrorEditorImpl } from "./renderProseMirror-CE37KmN_.js";
|
|
2
|
+
import { afterEach, expect } from "vitest";
|
|
3
|
+
import stringifyObject from "stringify-object";
|
|
4
|
+
//#region src/isProseMirrorNode.ts
|
|
5
|
+
var isProseMirrorNode = (val) => {
|
|
6
|
+
if (typeof val !== "object" || val === null) return false;
|
|
7
|
+
const node = val;
|
|
8
|
+
const type = node["type"];
|
|
9
|
+
if (typeof type !== "object" || type === null) return false;
|
|
10
|
+
const typeRecord = type;
|
|
11
|
+
return typeof typeRecord["name"] === "string" && typeof typeRecord["schema"] === "object" && "marks" in node && "attrs" in node && "content" in node;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/isEditorState.ts
|
|
15
|
+
var isEditorState = (val) => {
|
|
16
|
+
if (typeof val !== "object" || val === null) return false;
|
|
17
|
+
const state = val;
|
|
18
|
+
return isProseMirrorNode(state["doc"]) && typeof state["selection"] === "object" && state["selection"] !== null && "storedMarks" in state;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/stringifyMark.ts
|
|
22
|
+
var stringifyObjectOptions = {
|
|
23
|
+
indent: " ",
|
|
24
|
+
inlineCharacterLimit: 1e3
|
|
25
|
+
};
|
|
26
|
+
var stringifyMark = (mark) => Object.keys(mark.attrs).length > 0 ? `${mark.type.name}(${stringifyObject(mark.attrs, stringifyObjectOptions)})` : mark.type.name;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/stringifyProseMirrorNode.ts
|
|
29
|
+
function stringifyProseMirrorNode(node, selection, indentation = "") {
|
|
30
|
+
const markers = /* @__PURE__ */ new Map();
|
|
31
|
+
if (selection !== void 0) {
|
|
32
|
+
if (selection.anchor === selection.head) markers.set(selection.anchor, "<cursor>");
|
|
33
|
+
else {
|
|
34
|
+
markers.set(selection.anchor, "<anchor>");
|
|
35
|
+
markers.set(selection.head, "<head>");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return render(node, markers, indentation, 0);
|
|
39
|
+
}
|
|
40
|
+
function escapeText(text) {
|
|
41
|
+
return text.replace(/[\p{Cc}\p{Cf}'\\]/gu, (char) => {
|
|
42
|
+
switch (char) {
|
|
43
|
+
case "\f": return "\\f";
|
|
44
|
+
case "\n": return "\\n";
|
|
45
|
+
case "\r": return "\\r";
|
|
46
|
+
case " ": return "\\t";
|
|
47
|
+
case "\b": return "\\b";
|
|
48
|
+
case "'": return "\\'";
|
|
49
|
+
case "\\": return "\\\\";
|
|
50
|
+
default: return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function quoteWithMarkers(text, start, markers) {
|
|
55
|
+
const inside = [...markers.entries()].filter(([pos]) => pos >= start && pos <= start + text.length).sort(([a], [b]) => a - b);
|
|
56
|
+
const wrapMarker = containsRTL(text) ? (notation) => `\u2066${notation}\u2069` : (notation) => notation;
|
|
57
|
+
let quoted = "";
|
|
58
|
+
let last = 0;
|
|
59
|
+
for (const [pos, notation] of inside) {
|
|
60
|
+
markers.delete(pos);
|
|
61
|
+
const offset = pos - start;
|
|
62
|
+
quoted += escapeText(text.slice(last, offset)) + wrapMarker(notation);
|
|
63
|
+
last = offset;
|
|
64
|
+
}
|
|
65
|
+
quoted += escapeText(text.slice(last));
|
|
66
|
+
return `'${quoted}'`;
|
|
67
|
+
}
|
|
68
|
+
function render(node, markers, indentation, contentStart) {
|
|
69
|
+
if (node.type.spec.toDebugString !== void 0) return `${indentation}${wrapMarks(node.marks, node.type.spec.toDebugString(node))}`;
|
|
70
|
+
if (node.type.name === "text") return `${indentation}${wrapMarks(node.marks, quoteWithMarkers(node.textContent, contentStart, markers))}`;
|
|
71
|
+
return renderElement(node, markers, indentation, contentStart);
|
|
72
|
+
}
|
|
73
|
+
function renderElement(node, markers, indentation, contentStart) {
|
|
74
|
+
const type = node.type.name;
|
|
75
|
+
const hasAttrs = Object.keys(node.attrs).length > 0;
|
|
76
|
+
const nextIndentation = `${indentation} `;
|
|
77
|
+
const args = [];
|
|
78
|
+
const pushArg = (content) => {
|
|
79
|
+
args.push(`${nextIndentation}${content},`);
|
|
80
|
+
};
|
|
81
|
+
const pushMarkerAt = (pos) => {
|
|
82
|
+
const notation = markers.get(pos);
|
|
83
|
+
if (notation !== void 0) {
|
|
84
|
+
markers.delete(pos);
|
|
85
|
+
pushArg(`'${notation}'`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (hasAttrs) pushArg(stringifyObject(node.attrs, stringifyObjectOptions));
|
|
89
|
+
if (!node.type.isLeaf) {
|
|
90
|
+
if (node.content.childCount === 0) pushMarkerAt(contentStart);
|
|
91
|
+
node.content.forEach((child, offset, index) => {
|
|
92
|
+
const childStart = contentStart + offset;
|
|
93
|
+
const isText = child.type.name === "text";
|
|
94
|
+
if (!(index > 0 && node.content.child(index - 1).type.name === "text") && !isText) pushMarkerAt(childStart);
|
|
95
|
+
if (isText) pushArg(wrapMarks(child.marks, quoteWithMarkers(child.textContent, childStart, markers)));
|
|
96
|
+
else args.push(`${render(child, markers, nextIndentation, childStart + 1)},`);
|
|
97
|
+
});
|
|
98
|
+
if (node.content.lastChild?.type.name !== "text") pushMarkerAt(contentStart + node.content.size);
|
|
99
|
+
}
|
|
100
|
+
const body = !hasAttrs && (node.content.childCount === 0 || node.content.childCount === 1 && node.content.firstChild?.type.name === "text") ? `${type}(${args.map((arg) => arg.slice(nextIndentation.length, -1)).join("")})` : `${type}(\n${args.join("\n")}\n${indentation})`;
|
|
101
|
+
return `${indentation}${wrapMarks(node.marks, body)}`;
|
|
102
|
+
}
|
|
103
|
+
function wrapMarks(marks, origContent) {
|
|
104
|
+
let content = origContent;
|
|
105
|
+
for (const mark of [...marks].reverse()) {
|
|
106
|
+
const hasAttrs = Object.keys(mark.attrs).length > 0;
|
|
107
|
+
const items = [content];
|
|
108
|
+
if (hasAttrs) items.unshift(stringifyObject(mark.attrs, stringifyObjectOptions));
|
|
109
|
+
content = `${mark.type.name}(${items.join(", ")})`;
|
|
110
|
+
}
|
|
111
|
+
return content;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/stringifyEditorState.ts
|
|
115
|
+
function stringifyEditorState(state, indentation = "") {
|
|
116
|
+
const { doc, selection, storedMarks } = state;
|
|
117
|
+
const lines = [stringifyProseMirrorNode(doc, selection, indentation), `${indentation}# selection: ${selection.constructor.name} ${String(selection.from)}..${String(selection.to)}`];
|
|
118
|
+
if (storedMarks !== null) lines.push(`${indentation}# storedMarks: [${storedMarks.map(stringifyMark).join(", ")}]`);
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/setup.ts
|
|
123
|
+
afterEach(cleanupEditors);
|
|
124
|
+
expect.extend({
|
|
125
|
+
toEqualProseMirrorNode(received, expected) {
|
|
126
|
+
const receivedDoc = `\n${stringifyProseMirrorNode(received)}\n`;
|
|
127
|
+
const expectedDoc = `\n${stringifyProseMirrorNode(expected)}\n`;
|
|
128
|
+
const sameSchema = received.type.schema === expected.type.schema;
|
|
129
|
+
const pass = sameSchema && this.equals(receivedDoc, expectedDoc);
|
|
130
|
+
return {
|
|
131
|
+
message: pass ? () => `${this.utils.matcherHint(".not.toEqualProseMirrorNode")}\n\nExpected value of document to not equal:\n ${this.utils.printExpected(expectedDoc)}\nActual:\n ${this.utils.printReceived(receivedDoc)}` : () => {
|
|
132
|
+
if (!sameSchema && this.equals(receivedDoc, expectedDoc)) return `${this.utils.matcherHint(".toEqualProseMirrorNode")}\n\nThe documents stringify identically but come from different schemas:\n${this.utils.printReceived(receivedDoc)}`;
|
|
133
|
+
const diffString = this.utils.diff(expectedDoc, receivedDoc);
|
|
134
|
+
return `${this.utils.matcherHint(".toEqualProseMirrorNode")}\n\nExpected value of document to equal:\n${this.utils.printExpected(expectedDoc)}\nActual:\n${this.utils.printReceived(receivedDoc)}${diffString === void 0 ? "" : `\n\nDifference:\n\n${diffString}`}`;
|
|
135
|
+
},
|
|
136
|
+
pass
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
toHaveSelection(received, expected) {
|
|
140
|
+
const state = received instanceof ProseMirrorEditorImpl ? received.state : received;
|
|
141
|
+
const receivedSelection = state.selection;
|
|
142
|
+
const expectedSelection = resolveSelection(state.doc, expected);
|
|
143
|
+
const receivedDoc = `\n${stringifyProseMirrorNode(state.doc, receivedSelection)}\n`;
|
|
144
|
+
const expectedDoc = `\n${stringifyProseMirrorNode(state.doc, expectedSelection)}\n`;
|
|
145
|
+
const pass = receivedSelection.eq(expectedSelection);
|
|
146
|
+
return {
|
|
147
|
+
message: pass ? () => `${this.utils.matcherHint(".not.toHaveSelection")}\n\nExpected selection to not equal:\n${this.utils.printExpected(expectedDoc)}\nActual:\n${this.utils.printReceived(receivedDoc)}` : () => {
|
|
148
|
+
const diffString = this.utils.diff(expectedDoc, receivedDoc);
|
|
149
|
+
return `${this.utils.matcherHint(".toHaveSelection")}\n\nExpected selection to equal (${this.utils.printExpected(expectedSelection.constructor.name)}):\n${this.utils.printExpected(expectedDoc)}\nActual (${this.utils.printReceived(receivedSelection.constructor.name)}):\n${this.utils.printReceived(receivedDoc)}${diffString === void 0 ? "" : `\n\nDifference:\n\n${diffString}`}`;
|
|
150
|
+
},
|
|
151
|
+
pass
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
expect.addSnapshotSerializer({
|
|
156
|
+
serialize: (val, _config, indentation) => stringifyProseMirrorNode(val, void 0, indentation).slice(indentation.length),
|
|
157
|
+
test: isProseMirrorNode
|
|
158
|
+
});
|
|
159
|
+
expect.addSnapshotSerializer({
|
|
160
|
+
serialize: (val, _config, indentation) => {
|
|
161
|
+
return stringifyEditorState(val instanceof ProseMirrorEditorImpl ? val.state : val, indentation).slice(indentation.length);
|
|
162
|
+
},
|
|
163
|
+
test: (val) => val instanceof ProseMirrorEditorImpl || isEditorState(val)
|
|
164
|
+
});
|
|
165
|
+
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-prosemirror",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A plugin for Vitest that enables you to write tests using the ProseMirror editor",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vitest",
|
|
@@ -17,16 +17,22 @@
|
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"author": "Marek Dědič",
|
|
20
|
-
"sideEffects":
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"./dist/setup.js"
|
|
22
|
+
],
|
|
21
23
|
"type": "module",
|
|
22
24
|
"exports": {
|
|
23
25
|
".": {
|
|
24
|
-
"types": "./dist/
|
|
25
|
-
"import": "./dist/
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./setup": {
|
|
30
|
+
"types": "./dist/setup.d.ts",
|
|
31
|
+
"import": "./dist/setup.js"
|
|
26
32
|
}
|
|
27
33
|
},
|
|
28
|
-
"module": "dist/
|
|
29
|
-
"types": "dist/
|
|
34
|
+
"module": "dist/index.js",
|
|
35
|
+
"types": "dist/index.d.ts",
|
|
30
36
|
"files": [
|
|
31
37
|
"dist"
|
|
32
38
|
],
|
|
@@ -36,6 +42,9 @@
|
|
|
36
42
|
"check": "FORCE_COLOR=1 run-p -c --aggregate-output check:*",
|
|
37
43
|
"check:js-compat": "es-check es2023 --module --checkFeatures 'dist/**/*.js'",
|
|
38
44
|
"clean": "rimraf dist/*",
|
|
45
|
+
"docs:build": "vitepress build docs",
|
|
46
|
+
"docs:dev": "vitepress dev docs",
|
|
47
|
+
"docs:preview": "vitepress preview docs",
|
|
39
48
|
"lint": "FORCE_COLOR=1 run-p -c --aggregate-output lint:*",
|
|
40
49
|
"lint:eslint": "eslint",
|
|
41
50
|
"lint:ts": "FORCE_COLOR=1 run-p -c --aggregate-output lint:ts:*",
|
|
@@ -63,6 +72,7 @@
|
|
|
63
72
|
"@types/stringify-object": "^4.0.5",
|
|
64
73
|
"@vitest/coverage-v8": "^5.0.0",
|
|
65
74
|
"@vitest/eslint-plugin": "^1.3.26",
|
|
75
|
+
"blns": "^2.0.4",
|
|
66
76
|
"es-check": "^9.6.4",
|
|
67
77
|
"eslint": "^10.0.2",
|
|
68
78
|
"eslint-config-prettier": "^10.0.1",
|
|
@@ -70,6 +80,7 @@
|
|
|
70
80
|
"eslint-plugin-perfectionist": "^5.0.0",
|
|
71
81
|
"eslint-plugin-prefer-arrow-functions": "^3.9.1",
|
|
72
82
|
"eslint-plugin-prettier": "^5.1.3",
|
|
83
|
+
"jiti": "^2.7.0",
|
|
73
84
|
"jsdom": "^30.0.0",
|
|
74
85
|
"npm-run-all2": "^9.0.3",
|
|
75
86
|
"prettier": "^3.3.0",
|
|
@@ -77,11 +88,13 @@
|
|
|
77
88
|
"prosemirror-inputrules": "^1.5.0",
|
|
78
89
|
"prosemirror-keymap": "^1.2.3",
|
|
79
90
|
"prosemirror-schema-basic": "^1.2.3",
|
|
91
|
+
"prosemirror-test-builder": "^1.1.1",
|
|
80
92
|
"rimraf": "^6.0.1",
|
|
81
93
|
"typescript": "^6.0.2",
|
|
82
94
|
"typescript-eslint": "^8.58.0",
|
|
83
95
|
"unplugin-dts": "^1.0.1",
|
|
84
96
|
"vite": "^8.0.0",
|
|
97
|
+
"vitepress": "^1.6.4",
|
|
85
98
|
"vitest": "^5.0.0",
|
|
86
99
|
"w3c-keyname": "^2.2.8"
|
|
87
100
|
},
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import 'vitest';
|
|
2
|
-
import { EditorState } from 'prosemirror-state';
|
|
3
|
-
import { Node as Node_2 } from 'prosemirror-model';
|
|
4
|
-
import { Plugin as Plugin_2 } from 'prosemirror-state';
|
|
5
|
-
import { Selection as Selection_2 } from 'prosemirror-state';
|
|
6
|
-
|
|
7
|
-
export declare interface CustomMatchers<R = unknown> {
|
|
8
|
-
toEqualProseMirrorNode(expected: Node_2): R;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export declare interface Options {
|
|
12
|
-
autoCleanup: boolean;
|
|
13
|
-
plugins: Array<Plugin_2>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export declare class ProseMirrorTester {
|
|
17
|
-
get doc(): Node_2;
|
|
18
|
-
get html(): string;
|
|
19
|
-
get state(): EditorState;
|
|
20
|
-
get text(): string;
|
|
21
|
-
private destroyed;
|
|
22
|
-
private readonly element;
|
|
23
|
-
private readonly view;
|
|
24
|
-
constructor(documentRoot: Node_2, options?: Partial<Options>);
|
|
25
|
-
destroy(): void;
|
|
26
|
-
insertText(text: string): void;
|
|
27
|
-
selectText(selection: TesterSelection): void;
|
|
28
|
-
private assertAlive;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export declare type TesterSelection = "all" | "end" | "start" | {
|
|
32
|
-
anchor: number;
|
|
33
|
-
head: number;
|
|
34
|
-
} | Selection_2 | number;
|
|
35
|
-
|
|
36
|
-
export { }
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
declare module "vitest" {
|
|
40
|
-
interface Matchers<R extends Promise<void> | void = Promise<void> | void, T = unknown> extends CustomMatchers<R> {
|
|
41
|
-
}
|
|
42
|
-
}
|