@joinezco/markdown-editor 0.0.3 → 0.0.4
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/.turbo/turbo-build.log +4 -5
- package/dist/editor/extensions/codeblock.js +70 -29
- package/dist/editor/index.js +23 -0
- package/dist/editor/styles.js +8 -0
- package/package.json +3 -3
- package/public/fonts/UbuntuMonoNerdFont-Regular.ttf +0 -0
- package/public/snapshot.bin +0 -0
- package/src/lib/editor/extensions/codeblock.ts +77 -32
- package/src/lib/editor/index.ts +23 -0
- package/src/lib/editor/styles.ts +8 -0
- package/src/test/multiview-sync.test.ts +137 -0
- package/TEST_README.md +0 -359
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-be-focusable-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-create-an-editor-instance-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-have-initial-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-render-in-the-DOM-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-copy-and-paste-operations-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-undo-and-redo-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-maintain-state-across-content-changes-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-trigger-update-callbacks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-invalid-markdown-gracefully-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-very-long-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-B-for-bold-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-I-for-italic-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-get-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-handle-empty-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-set-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-bold-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-headings-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-italic-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-task-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-handle-cursor-positioning-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-set-and-get-selection-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-line-breaks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-typing-at-different-positions-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-insert-text-at-cursor-position-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-code-blocks-without-language-specification-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-different-programming-languages-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-render-code-blocks-with-syntax-highlighting-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-handle-multiple-extensions-working-together-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-maintain-editor-state-across-complex-operations-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-handle-file-references-in-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-maintain-file-system-state-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-auto-detect-URLs-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-handle-email-links-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-render-links-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-provide-markdown-storage-interface-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-sync-markdown-content-with-storage-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-handle-heading-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-trigger-slash-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-handle-table-navigation-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-render-tables-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-handle-nested-task-lists-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-render-task-lists-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-toggle-task-completion-1.png +0 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
|
|
2
|
+
> @joinezco/markdown-editor@0.0.3 build /home/theo/dev/mono/src/typescript/markdown-editor
|
|
3
|
+
> tsc -p tsconfig.lib.json
|
|
4
|
+
|
|
@@ -147,36 +147,36 @@ export const ExtendedCodeblock = Node.create({
|
|
|
147
147
|
},
|
|
148
148
|
// Register input rules (e.g., ``` or ~~~ at the start of a line)
|
|
149
149
|
addInputRules() {
|
|
150
|
+
const parseLanguageAttributes = (input) => {
|
|
151
|
+
if (!input)
|
|
152
|
+
return { language: 'markdown' };
|
|
153
|
+
// If input contains a dot, treat it as a filename
|
|
154
|
+
if (input.includes('.')) {
|
|
155
|
+
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
156
|
+
const lang = extOrLanguageToLanguageId[ext] || 'markdown';
|
|
157
|
+
return { file: input, language: lang };
|
|
158
|
+
}
|
|
159
|
+
// Otherwise, check if it's a language name
|
|
160
|
+
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
161
|
+
return lang.includes(input) || ext === input;
|
|
162
|
+
});
|
|
163
|
+
if (matchingLanguage) {
|
|
164
|
+
return { language: matchingLanguage[1], file: null };
|
|
165
|
+
}
|
|
166
|
+
return { language: 'markdown' };
|
|
167
|
+
};
|
|
150
168
|
return [
|
|
169
|
+
// ```language + space — more specific, checked first
|
|
151
170
|
textblockTypeInputRule({
|
|
152
|
-
find: /^```([^\s`]+)
|
|
171
|
+
find: /^```([^\s`]+)\s$/,
|
|
153
172
|
type: this.type,
|
|
154
|
-
getAttributes: match =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const lang = extOrLanguageToLanguageId[ext] || 'markdown';
|
|
162
|
-
return {
|
|
163
|
-
file: input,
|
|
164
|
-
language: lang,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
// Otherwise, check if it's a language name
|
|
168
|
-
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
169
|
-
return lang.includes(input) || ext === input;
|
|
170
|
-
});
|
|
171
|
-
if (matchingLanguage) {
|
|
172
|
-
return {
|
|
173
|
-
language: matchingLanguage[1],
|
|
174
|
-
file: null,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
// If no language match found, default to markdown
|
|
178
|
-
return { language: 'markdown' };
|
|
179
|
-
},
|
|
173
|
+
getAttributes: match => parseLanguageAttributes(match[1]?.trim()),
|
|
174
|
+
}),
|
|
175
|
+
// ``` alone — triggers immediately on the third backtick
|
|
176
|
+
textblockTypeInputRule({
|
|
177
|
+
find: /^```$/,
|
|
178
|
+
type: this.type,
|
|
179
|
+
getAttributes: () => ({ language: '' }),
|
|
180
180
|
}),
|
|
181
181
|
];
|
|
182
182
|
},
|
|
@@ -236,6 +236,14 @@ export const ExtendedCodeblock = Node.create({
|
|
|
236
236
|
main = state.doc.lineAt(main.head);
|
|
237
237
|
if (dir < 0 ? main.from > 0 : main.to < state.doc.length)
|
|
238
238
|
return false;
|
|
239
|
+
// ArrowUp from first line: focus toolbar instead of escaping to ProseMirror
|
|
240
|
+
if (dir < 0) {
|
|
241
|
+
const toolbarInput = cm.dom.querySelector('.cm-toolbar-input');
|
|
242
|
+
if (toolbarInput) {
|
|
243
|
+
toolbarInput.focus();
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
239
247
|
// @ts-ignore
|
|
240
248
|
let targetPos = getPos() + (dir < 0 ? 0 : node.nodeSize);
|
|
241
249
|
let selection = Selection.near(view.state.doc.resolve(targetPos), dir);
|
|
@@ -302,10 +310,34 @@ export const ExtendedCodeblock = Node.create({
|
|
|
302
310
|
// Reassign z-indexes for all codeblocks whenever a new one is created
|
|
303
311
|
// This ensures proper stacking order based on DOM position
|
|
304
312
|
reassignZIndexes();
|
|
313
|
+
// Handle ArrowUp from toolbar to escape to ProseMirror above
|
|
314
|
+
dom.addEventListener('keydown', (e) => {
|
|
315
|
+
const target = e.target;
|
|
316
|
+
if (target.classList.contains('cm-toolbar-input') && e.key === 'ArrowUp') {
|
|
317
|
+
// Check if the dropdown is open — if so, let toolbar handle it
|
|
318
|
+
const dropdown = dom.querySelector('.cm-search-results');
|
|
319
|
+
if (dropdown && dropdown.children.length > 0)
|
|
320
|
+
return;
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
e.stopPropagation();
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
const pos = getPos();
|
|
325
|
+
if (pos !== undefined) {
|
|
326
|
+
let selection = Selection.near(view.state.doc.resolve(pos), -1);
|
|
327
|
+
let tr = view.state.tr.setSelection(selection).scrollIntoView();
|
|
328
|
+
view.dispatch(tr);
|
|
329
|
+
view.focus();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}, true);
|
|
305
333
|
// Track whether this codeblock was created empty (e.g. via ``` input rule)
|
|
306
334
|
const wasCreatedEmpty = !node.textContent && !node.attrs.file;
|
|
307
|
-
//
|
|
308
|
-
|
|
335
|
+
// Use the editor's filesystem if available (so codeblocks can resolve
|
|
336
|
+
// file references seeded into the same filesystem), otherwise fall back
|
|
337
|
+
// to a standalone worker.
|
|
338
|
+
const editorFs = editor.storage.persistence?.options?.fs;
|
|
339
|
+
const fsPromise = editorFs ? Promise.resolve(editorFs) : getFileSystemWorker();
|
|
340
|
+
fsPromise.then(fs => {
|
|
309
341
|
fsWorker = fs;
|
|
310
342
|
SearchIndex.get(fsWorker, '.codeblock/index.json').then(index => {
|
|
311
343
|
// Reconfigure with codeblock extension once fs is ready
|
|
@@ -344,6 +376,15 @@ export const ExtendedCodeblock = Node.create({
|
|
|
344
376
|
return {
|
|
345
377
|
dom,
|
|
346
378
|
setSelection(anchor, head) {
|
|
379
|
+
// If the codeblock wasn't focused (entering from outside),
|
|
380
|
+
// direct to the toolbar input for keyboard navigation
|
|
381
|
+
if (!cm.hasFocus) {
|
|
382
|
+
const toolbarInput = cm.dom.querySelector('.cm-toolbar-input');
|
|
383
|
+
if (toolbarInput) {
|
|
384
|
+
toolbarInput.focus();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
347
388
|
cm.focus();
|
|
348
389
|
updating = true;
|
|
349
390
|
cm.dispatch({ selection: { anchor, head } });
|
package/dist/editor/index.js
CHANGED
|
@@ -11,6 +11,28 @@ import { ExtendedLink } from './extensions/link';
|
|
|
11
11
|
import { SlashCommands } from './extensions/slash-commands';
|
|
12
12
|
import { defaultSlashCommands } from './commands';
|
|
13
13
|
import { StyleModule } from 'style-mod';
|
|
14
|
+
// Override native caret blink speed on browsers that support caret-animation (Firefox 130+/Zen)
|
|
15
|
+
let caretBlinkInjected = false;
|
|
16
|
+
function injectCaretBlink() {
|
|
17
|
+
if (caretBlinkInjected)
|
|
18
|
+
return;
|
|
19
|
+
caretBlinkInjected = true;
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.textContent = `
|
|
22
|
+
@supports (caret-animation: manual) {
|
|
23
|
+
.ezco-mde .ProseMirror {
|
|
24
|
+
caret-animation: manual;
|
|
25
|
+
}
|
|
26
|
+
.ezco-mde .ProseMirror:focus {
|
|
27
|
+
animation: ezco-mde-caret-blink 1s step-end infinite;
|
|
28
|
+
}
|
|
29
|
+
@keyframes ezco-mde-caret-blink {
|
|
30
|
+
from, 50% { caret-color: currentColor; }
|
|
31
|
+
50.1%, to { caret-color: transparent; }
|
|
32
|
+
}
|
|
33
|
+
}`;
|
|
34
|
+
document.head.appendChild(style);
|
|
35
|
+
}
|
|
14
36
|
/**
|
|
15
37
|
* Create a Markdown-ready Tiptap Editor with default extensions and options.
|
|
16
38
|
*
|
|
@@ -63,6 +85,7 @@ export function createEditor(options = {}) {
|
|
|
63
85
|
editor.view.dom.classList.add('ezco-mde');
|
|
64
86
|
if (typeof document !== 'undefined') {
|
|
65
87
|
StyleModule.mount(document, styleModule);
|
|
88
|
+
injectCaretBlink();
|
|
66
89
|
}
|
|
67
90
|
return editor;
|
|
68
91
|
}
|
package/dist/editor/styles.js
CHANGED
|
@@ -230,5 +230,13 @@ export const styleModule = new StyleModule({
|
|
|
230
230
|
flex: 1
|
|
231
231
|
}
|
|
232
232
|
},
|
|
233
|
+
// Make task checkboxes visible when selected (Ctrl-A)
|
|
234
|
+
// Checkboxes don't natively show selection highlighting,
|
|
235
|
+
// so add an outline using the system Highlight color
|
|
236
|
+
'& ul[data-type="taskList"] li > label > input[type="checkbox"]': {
|
|
237
|
+
'&::selection': {
|
|
238
|
+
background: 'Highlight',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
233
241
|
}
|
|
234
242
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joinezco/markdown-editor",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@codemirror/state": "^6.5.2",
|
|
18
|
-
"@codemirror/view": "6.36.7",
|
|
18
|
+
"@codemirror/view": "^6.36.7",
|
|
19
19
|
"@tiptap/core": "^3.4.1",
|
|
20
20
|
"@tiptap/extension-link": "^3.4.1",
|
|
21
21
|
"@tiptap/extension-table": "^3.4.1",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"tippy.js": "^6.3.7",
|
|
36
36
|
"tiptap-markdown": "^0.8.10",
|
|
37
37
|
"vite-plugin-node-polyfills": "^0.24.0",
|
|
38
|
-
"@joinezco/codeblock": "0.0.
|
|
38
|
+
"@joinezco/codeblock": "0.0.10"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/js": "^9.22.0",
|
|
Binary file
|
package/public/snapshot.bin
CHANGED
|
Binary file
|
|
@@ -170,39 +170,40 @@ export const ExtendedCodeblock = Node.create({
|
|
|
170
170
|
|
|
171
171
|
// Register input rules (e.g., ``` or ~~~ at the start of a line)
|
|
172
172
|
addInputRules() {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (input.includes('.')) {
|
|
183
|
-
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
184
|
-
const lang = extOrLanguageToLanguageId[ext as ExtensionOrLanguage] || 'markdown'
|
|
185
|
-
return {
|
|
186
|
-
file: input,
|
|
187
|
-
language: lang,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
173
|
+
const parseLanguageAttributes = (input: string | undefined) => {
|
|
174
|
+
if (!input) return { language: 'markdown' };
|
|
175
|
+
|
|
176
|
+
// If input contains a dot, treat it as a filename
|
|
177
|
+
if (input.includes('.')) {
|
|
178
|
+
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
179
|
+
const lang = extOrLanguageToLanguageId[ext as ExtensionOrLanguage] || 'markdown'
|
|
180
|
+
return { file: input, language: lang };
|
|
181
|
+
}
|
|
190
182
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
183
|
+
// Otherwise, check if it's a language name
|
|
184
|
+
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
185
|
+
return lang.includes(input) || ext === input;
|
|
186
|
+
})
|
|
195
187
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
file: null,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
188
|
+
if (matchingLanguage) {
|
|
189
|
+
return { language: matchingLanguage[1], file: null };
|
|
190
|
+
}
|
|
202
191
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
192
|
+
return { language: 'markdown' };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return [
|
|
196
|
+
// ```language + space — more specific, checked first
|
|
197
|
+
textblockTypeInputRule({
|
|
198
|
+
find: /^```([^\s`]+)\s$/,
|
|
199
|
+
type: this.type,
|
|
200
|
+
getAttributes: match => parseLanguageAttributes(match[1]?.trim()),
|
|
201
|
+
}),
|
|
202
|
+
// ``` alone — triggers immediately on the third backtick
|
|
203
|
+
textblockTypeInputRule({
|
|
204
|
+
find: /^```$/,
|
|
205
|
+
type: this.type,
|
|
206
|
+
getAttributes: () => ({ language: '' }),
|
|
206
207
|
}),
|
|
207
208
|
];
|
|
208
209
|
},
|
|
@@ -269,6 +270,16 @@ export const ExtendedCodeblock = Node.create({
|
|
|
269
270
|
if (!main.empty) return false
|
|
270
271
|
if (unit == "line") main = state.doc.lineAt(main.head)
|
|
271
272
|
if (dir < 0 ? main.from > 0 : main.to < state.doc.length) return false
|
|
273
|
+
|
|
274
|
+
// ArrowUp from first line: focus toolbar instead of escaping to ProseMirror
|
|
275
|
+
if (dir < 0) {
|
|
276
|
+
const toolbarInput = cm.dom.querySelector<HTMLInputElement>('.cm-toolbar-input');
|
|
277
|
+
if (toolbarInput) {
|
|
278
|
+
toolbarInput.focus();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
272
283
|
// @ts-ignore
|
|
273
284
|
let targetPos = getPos() + (dir < 0 ? 0 : node.nodeSize)
|
|
274
285
|
let selection = Selection.near(view.state.doc.resolve(targetPos), dir)
|
|
@@ -342,11 +353,36 @@ export const ExtendedCodeblock = Node.create({
|
|
|
342
353
|
// This ensures proper stacking order based on DOM position
|
|
343
354
|
reassignZIndexes();
|
|
344
355
|
|
|
356
|
+
// Handle ArrowUp from toolbar to escape to ProseMirror above
|
|
357
|
+
dom.addEventListener('keydown', (e) => {
|
|
358
|
+
const target = e.target as HTMLElement;
|
|
359
|
+
if (target.classList.contains('cm-toolbar-input') && e.key === 'ArrowUp') {
|
|
360
|
+
// Check if the dropdown is open — if so, let toolbar handle it
|
|
361
|
+
const dropdown = dom.querySelector('.cm-search-results');
|
|
362
|
+
if (dropdown && dropdown.children.length > 0) return;
|
|
363
|
+
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
e.stopPropagation();
|
|
366
|
+
// @ts-ignore
|
|
367
|
+
const pos = getPos();
|
|
368
|
+
if (pos !== undefined) {
|
|
369
|
+
let selection = Selection.near(view.state.doc.resolve(pos), -1);
|
|
370
|
+
let tr = view.state.tr.setSelection(selection).scrollIntoView();
|
|
371
|
+
view.dispatch(tr);
|
|
372
|
+
view.focus();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}, true);
|
|
376
|
+
|
|
345
377
|
// Track whether this codeblock was created empty (e.g. via ``` input rule)
|
|
346
378
|
const wasCreatedEmpty = !node.textContent && !node.attrs.file;
|
|
347
379
|
|
|
348
|
-
//
|
|
349
|
-
|
|
380
|
+
// Use the editor's filesystem if available (so codeblocks can resolve
|
|
381
|
+
// file references seeded into the same filesystem), otherwise fall back
|
|
382
|
+
// to a standalone worker.
|
|
383
|
+
const editorFs = editor.storage.persistence?.options?.fs;
|
|
384
|
+
const fsPromise = editorFs ? Promise.resolve(editorFs) : getFileSystemWorker();
|
|
385
|
+
fsPromise.then(fs => {
|
|
350
386
|
fsWorker = fs;
|
|
351
387
|
SearchIndex.get(fsWorker, '.codeblock/index.json').then(index => {
|
|
352
388
|
// Reconfigure with codeblock extension once fs is ready
|
|
@@ -388,6 +424,15 @@ export const ExtendedCodeblock = Node.create({
|
|
|
388
424
|
return {
|
|
389
425
|
dom,
|
|
390
426
|
setSelection(anchor, head) {
|
|
427
|
+
// If the codeblock wasn't focused (entering from outside),
|
|
428
|
+
// direct to the toolbar input for keyboard navigation
|
|
429
|
+
if (!cm.hasFocus) {
|
|
430
|
+
const toolbarInput = cm.dom.querySelector<HTMLInputElement>('.cm-toolbar-input');
|
|
431
|
+
if (toolbarInput) {
|
|
432
|
+
toolbarInput.focus();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
391
436
|
cm.focus()
|
|
392
437
|
updating = true
|
|
393
438
|
cm.dispatch({ selection: { anchor, head } })
|
package/src/lib/editor/index.ts
CHANGED
|
@@ -13,6 +13,28 @@ import { SlashCommands } from './extensions/slash-commands';
|
|
|
13
13
|
import { defaultSlashCommands } from './commands';
|
|
14
14
|
import { StyleModule } from 'style-mod';
|
|
15
15
|
|
|
16
|
+
// Override native caret blink speed on browsers that support caret-animation (Firefox 130+/Zen)
|
|
17
|
+
let caretBlinkInjected = false;
|
|
18
|
+
function injectCaretBlink() {
|
|
19
|
+
if (caretBlinkInjected) return;
|
|
20
|
+
caretBlinkInjected = true;
|
|
21
|
+
const style = document.createElement('style');
|
|
22
|
+
style.textContent = `
|
|
23
|
+
@supports (caret-animation: manual) {
|
|
24
|
+
.ezco-mde .ProseMirror {
|
|
25
|
+
caret-animation: manual;
|
|
26
|
+
}
|
|
27
|
+
.ezco-mde .ProseMirror:focus {
|
|
28
|
+
animation: ezco-mde-caret-blink 1s step-end infinite;
|
|
29
|
+
}
|
|
30
|
+
@keyframes ezco-mde-caret-blink {
|
|
31
|
+
from, 50% { caret-color: currentColor; }
|
|
32
|
+
50.1%, to { caret-color: transparent; }
|
|
33
|
+
}
|
|
34
|
+
}`;
|
|
35
|
+
document.head.appendChild(style);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
export type MarkdownEditorOptions = Partial<EditorOptions> & {
|
|
17
39
|
extensions?: Extension[];
|
|
18
40
|
fs?: FileSystemOptions;
|
|
@@ -77,6 +99,7 @@ export function createEditor(options: MarkdownEditorOptions = {}): MarkdownEdito
|
|
|
77
99
|
|
|
78
100
|
if (typeof document !== 'undefined') {
|
|
79
101
|
StyleModule.mount(document, styleModule);
|
|
102
|
+
injectCaretBlink();
|
|
80
103
|
}
|
|
81
104
|
return editor as MarkdownEditor;
|
|
82
105
|
}
|
package/src/lib/editor/styles.ts
CHANGED
|
@@ -247,5 +247,13 @@ export const styleModule: StyleModule = new StyleModule({
|
|
|
247
247
|
flex: 1
|
|
248
248
|
}
|
|
249
249
|
},
|
|
250
|
+
// Make task checkboxes visible when selected (Ctrl-A)
|
|
251
|
+
// Checkboxes don't natively show selection highlighting,
|
|
252
|
+
// so add an outline using the system Highlight color
|
|
253
|
+
'& ul[data-type="taskList"] li > label > input[type="checkbox"]': {
|
|
254
|
+
'&::selection': {
|
|
255
|
+
background: 'Highlight',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
250
258
|
}
|
|
251
259
|
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { fileChangeBus } from '@joinezco/codeblock'
|
|
3
|
+
import { EditorView } from '@codemirror/view'
|
|
4
|
+
import { EditorState } from '@codemirror/state'
|
|
5
|
+
|
|
6
|
+
describe('FileChangeBus', () => {
|
|
7
|
+
let viewA: EditorView
|
|
8
|
+
let viewB: EditorView
|
|
9
|
+
let containerA: HTMLElement
|
|
10
|
+
let containerB: HTMLElement
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
containerA = document.createElement('div')
|
|
14
|
+
containerB = document.createElement('div')
|
|
15
|
+
document.body.appendChild(containerA)
|
|
16
|
+
document.body.appendChild(containerB)
|
|
17
|
+
|
|
18
|
+
viewA = new EditorView({
|
|
19
|
+
state: EditorState.create({ doc: 'initial' }),
|
|
20
|
+
parent: containerA,
|
|
21
|
+
})
|
|
22
|
+
viewB = new EditorView({
|
|
23
|
+
state: EditorState.create({ doc: 'initial' }),
|
|
24
|
+
parent: containerB,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
viewA.destroy()
|
|
30
|
+
viewB.destroy()
|
|
31
|
+
containerA.remove()
|
|
32
|
+
containerB.remove()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should notify other subscribers but not the source', () => {
|
|
36
|
+
const received: { view: string; content: string }[] = []
|
|
37
|
+
|
|
38
|
+
fileChangeBus.subscribe('test.txt', viewA, (content) => {
|
|
39
|
+
received.push({ view: 'A', content })
|
|
40
|
+
})
|
|
41
|
+
fileChangeBus.subscribe('test.txt', viewB, (content) => {
|
|
42
|
+
received.push({ view: 'B', content })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Notify from view A — only B should receive
|
|
46
|
+
fileChangeBus.notify('test.txt', 'hello from A', viewA)
|
|
47
|
+
|
|
48
|
+
expect(received).toEqual([{ view: 'B', content: 'hello from A' }])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should not notify after unsubscribe', () => {
|
|
52
|
+
const received: string[] = []
|
|
53
|
+
|
|
54
|
+
const unsub = fileChangeBus.subscribe('test.txt', viewA, (content) => {
|
|
55
|
+
received.push(content)
|
|
56
|
+
})
|
|
57
|
+
fileChangeBus.subscribe('test.txt', viewB, () => {})
|
|
58
|
+
|
|
59
|
+
unsub()
|
|
60
|
+
fileChangeBus.notify('test.txt', 'hello', viewB)
|
|
61
|
+
|
|
62
|
+
expect(received).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should handle multiple files independently', () => {
|
|
66
|
+
const received: string[] = []
|
|
67
|
+
|
|
68
|
+
fileChangeBus.subscribe('a.txt', viewA, (content) => {
|
|
69
|
+
received.push('a:' + content)
|
|
70
|
+
})
|
|
71
|
+
fileChangeBus.subscribe('b.txt', viewA, (content) => {
|
|
72
|
+
received.push('b:' + content)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
fileChangeBus.notify('a.txt', 'one', viewB)
|
|
76
|
+
fileChangeBus.notify('b.txt', 'two', viewB)
|
|
77
|
+
|
|
78
|
+
expect(received).toEqual(['a:one', 'b:two'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should sync document content between views via the bus', () => {
|
|
82
|
+
// Simulate two views on the same file using the bus to sync
|
|
83
|
+
|
|
84
|
+
const unsubA = fileChangeBus.subscribe('shared.txt', viewA, (content) => {
|
|
85
|
+
if (viewA.state.doc.toString() !== content) {
|
|
86
|
+
viewA.dispatch({ changes: { from: 0, to: viewA.state.doc.length, insert: content } })
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
const unsubB = fileChangeBus.subscribe('shared.txt', viewB, (content) => {
|
|
90
|
+
if (viewB.state.doc.toString() !== content) {
|
|
91
|
+
viewB.dispatch({ changes: { from: 0, to: viewB.state.doc.length, insert: content } })
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Edit view A and "save" (notify the bus)
|
|
96
|
+
viewA.dispatch({ changes: { from: 0, to: viewA.state.doc.length, insert: 'updated content' } })
|
|
97
|
+
fileChangeBus.notify('shared.txt', 'updated content', viewA)
|
|
98
|
+
|
|
99
|
+
// View B should have received the update
|
|
100
|
+
expect(viewB.state.doc.toString()).toBe('updated content')
|
|
101
|
+
// View A should NOT have been re-dispatched (it was the source)
|
|
102
|
+
expect(viewA.state.doc.toString()).toBe('updated content')
|
|
103
|
+
|
|
104
|
+
unsubA()
|
|
105
|
+
unsubB()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should not create infinite loops when both views subscribe', () => {
|
|
109
|
+
let dispatchCountA = 0
|
|
110
|
+
let dispatchCountB = 0
|
|
111
|
+
|
|
112
|
+
fileChangeBus.subscribe('shared.txt', viewA, (content) => {
|
|
113
|
+
if (viewA.state.doc.toString() !== content) {
|
|
114
|
+
dispatchCountA++
|
|
115
|
+
viewA.dispatch({ changes: { from: 0, to: viewA.state.doc.length, insert: content } })
|
|
116
|
+
// In the real codeblockView, this dispatch would NOT trigger save because
|
|
117
|
+
// receivingExternalUpdate is true. So we do NOT re-notify.
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
fileChangeBus.subscribe('shared.txt', viewB, (content) => {
|
|
121
|
+
if (viewB.state.doc.toString() !== content) {
|
|
122
|
+
dispatchCountB++
|
|
123
|
+
viewB.dispatch({ changes: { from: 0, to: viewB.state.doc.length, insert: content } })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Simulate save from A
|
|
128
|
+
viewA.dispatch({ changes: { from: 0, to: viewA.state.doc.length, insert: 'final' } })
|
|
129
|
+
fileChangeBus.notify('shared.txt', 'final', viewA)
|
|
130
|
+
|
|
131
|
+
// Only B should have dispatched once
|
|
132
|
+
expect(dispatchCountA).toBe(0)
|
|
133
|
+
expect(dispatchCountB).toBe(1)
|
|
134
|
+
expect(viewA.state.doc.toString()).toBe('final')
|
|
135
|
+
expect(viewB.state.doc.toString()).toBe('final')
|
|
136
|
+
})
|
|
137
|
+
})
|
package/TEST_README.md
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
# Markdown Editor Testing Guide
|
|
2
|
-
|
|
3
|
-
This document explains how to use the testing setup for the `@joinezco/markdown-editor` library.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The testing setup uses **Vitest** with **browser testing capabilities** to test the markdown editor in a real browser environment. This allows us to test DOM interactions, keyboard events, and the actual rendering behavior of the editor.
|
|
8
|
-
|
|
9
|
-
## Setup
|
|
10
|
-
|
|
11
|
-
### Dependencies
|
|
12
|
-
|
|
13
|
-
The following testing dependencies are included:
|
|
14
|
-
|
|
15
|
-
- `vitest` - Fast unit test framework
|
|
16
|
-
- `@vitest/browser` - Browser testing support
|
|
17
|
-
- `@vitest/ui` - Web UI for test results
|
|
18
|
-
- `playwright` - Browser automation for testing
|
|
19
|
-
- `jsdom` - DOM implementation for Node.js
|
|
20
|
-
- `webdriverio` - WebDriver implementation
|
|
21
|
-
|
|
22
|
-
### Configuration
|
|
23
|
-
|
|
24
|
-
The testing is configured in [`vitest.config.ts`](./vitest.config.ts) with:
|
|
25
|
-
|
|
26
|
-
- **Browser testing enabled** using Playwright with Chromium
|
|
27
|
-
- **Test environment** set up with proper DOM mocking
|
|
28
|
-
- **Coverage reporting** with v8 provider
|
|
29
|
-
- **Custom setup file** for browser environment preparation
|
|
30
|
-
|
|
31
|
-
## Running Tests
|
|
32
|
-
|
|
33
|
-
### Available Scripts
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# Run tests in watch mode
|
|
37
|
-
npm run test
|
|
38
|
-
|
|
39
|
-
# Run tests with UI
|
|
40
|
-
npm run test:ui
|
|
41
|
-
|
|
42
|
-
# Run tests in browser mode
|
|
43
|
-
npm run test:browser
|
|
44
|
-
|
|
45
|
-
# Run tests once and exit
|
|
46
|
-
npm run test:run
|
|
47
|
-
|
|
48
|
-
# Run tests with coverage
|
|
49
|
-
npm run test:coverage
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Test Files
|
|
53
|
-
|
|
54
|
-
Tests are located in the `src/test/` directory:
|
|
55
|
-
|
|
56
|
-
- [`setup.ts`](./src/test/setup.ts) - Global test setup and browser mocks
|
|
57
|
-
- [`utils.ts`](./src/test/utils.ts) - Testing utilities for markdown editor
|
|
58
|
-
- [`editor.test.ts`](./src/test/editor.test.ts) - Core editor functionality tests
|
|
59
|
-
- [`extensions.test.ts`](./src/test/extensions.test.ts) - Extension-specific tests
|
|
60
|
-
|
|
61
|
-
## Testing Utilities
|
|
62
|
-
|
|
63
|
-
The [`utils.ts`](./src/test/utils.ts) file provides comprehensive utilities for testing the markdown editor:
|
|
64
|
-
|
|
65
|
-
### DOM Management
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// Create a test container
|
|
69
|
-
const container = createTestContainer()
|
|
70
|
-
|
|
71
|
-
// Create and initialize editor
|
|
72
|
-
const editor = await createTestEditor(container)
|
|
73
|
-
await waitForEditor(editor)
|
|
74
|
-
|
|
75
|
-
// Cleanup after test
|
|
76
|
-
cleanupEditor(editor, container)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Content Management
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
// Get current markdown content
|
|
83
|
-
const content = getMarkdownContent(editor)
|
|
84
|
-
|
|
85
|
-
// Set new markdown content
|
|
86
|
-
setMarkdownContent(editor, '# New Content')
|
|
87
|
-
|
|
88
|
-
// Get HTML output
|
|
89
|
-
const html = getHTMLContent(editor)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### User Interactions
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
// Focus the editor
|
|
96
|
-
focusEditor(editor)
|
|
97
|
-
|
|
98
|
-
// Type text
|
|
99
|
-
typeText(editor, 'Hello, World!')
|
|
100
|
-
|
|
101
|
-
// Simulate key presses
|
|
102
|
-
pressKey(editor, 'b', { ctrl: true }) // Ctrl+B for bold
|
|
103
|
-
|
|
104
|
-
// Set cursor position
|
|
105
|
-
setSelection(editor, 0, 10) // Select characters 0-10
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Async Operations
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// Wait for conditions
|
|
112
|
-
await waitFor(() => editor.isFocused, 5000)
|
|
113
|
-
|
|
114
|
-
// Wait for editor to be ready
|
|
115
|
-
await waitForEditor(editor)
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Test Categories
|
|
119
|
-
|
|
120
|
-
### Basic Editor Functionality
|
|
121
|
-
|
|
122
|
-
Tests core editor features:
|
|
123
|
-
- Editor initialization and DOM rendering
|
|
124
|
-
- Content getting/setting
|
|
125
|
-
- Focus management
|
|
126
|
-
- Selection handling
|
|
127
|
-
|
|
128
|
-
### Markdown Content Management
|
|
129
|
-
|
|
130
|
-
Tests markdown processing:
|
|
131
|
-
- Content conversion between markdown and HTML
|
|
132
|
-
- Handling of various markdown syntax
|
|
133
|
-
- Content validation and error handling
|
|
134
|
-
|
|
135
|
-
### Text Input and Editing
|
|
136
|
-
|
|
137
|
-
Tests user input:
|
|
138
|
-
- Text insertion at cursor position
|
|
139
|
-
- Line breaks and formatting
|
|
140
|
-
- Keyboard shortcuts (Ctrl+B, Ctrl+I, etc.)
|
|
141
|
-
|
|
142
|
-
### Extension Testing
|
|
143
|
-
|
|
144
|
-
Tests specific editor extensions:
|
|
145
|
-
- **Task Lists** - Checkbox rendering and interaction
|
|
146
|
-
- **Tables** - Table rendering and navigation
|
|
147
|
-
- **Links** - Link detection and rendering
|
|
148
|
-
- **Code Blocks** - Syntax highlighting and language support
|
|
149
|
-
- **Slash Commands** - Command menu functionality
|
|
150
|
-
|
|
151
|
-
### Browser-specific Features
|
|
152
|
-
|
|
153
|
-
Tests browser interactions:
|
|
154
|
-
- Copy/paste operations
|
|
155
|
-
- Undo/redo functionality
|
|
156
|
-
- Keyboard event handling
|
|
157
|
-
- DOM event simulation
|
|
158
|
-
|
|
159
|
-
## Writing New Tests
|
|
160
|
-
|
|
161
|
-
### Basic Test Structure
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
165
|
-
import { MarkdownEditor } from '../lib/editor'
|
|
166
|
-
import {
|
|
167
|
-
createTestContainer,
|
|
168
|
-
createTestEditor,
|
|
169
|
-
waitForEditor,
|
|
170
|
-
cleanupEditor,
|
|
171
|
-
} from './utils'
|
|
172
|
-
|
|
173
|
-
describe('My Feature', () => {
|
|
174
|
-
let container: HTMLElement
|
|
175
|
-
let editor: MarkdownEditor
|
|
176
|
-
|
|
177
|
-
beforeEach(async () => {
|
|
178
|
-
container = createTestContainer()
|
|
179
|
-
editor = await createTestEditor(container)
|
|
180
|
-
await waitForEditor(editor)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
afterEach(() => {
|
|
184
|
-
cleanupEditor(editor, container)
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('should do something', () => {
|
|
188
|
-
// Your test code here
|
|
189
|
-
expect(editor).toBeDefined()
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Testing Editor State
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
it('should update content correctly', () => {
|
|
198
|
-
setMarkdownContent(editor, '# Test Heading')
|
|
199
|
-
const content = getMarkdownContent(editor)
|
|
200
|
-
expect(content).toContain('# Test Heading')
|
|
201
|
-
|
|
202
|
-
const html = getHTMLContent(editor)
|
|
203
|
-
expect(html).toContain('<h1>Test Heading</h1>')
|
|
204
|
-
})
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Testing User Interactions
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
it('should handle keyboard shortcuts', () => {
|
|
211
|
-
focusEditor(editor)
|
|
212
|
-
typeText(editor, 'bold text')
|
|
213
|
-
setSelection(editor, 0, 9) // Select "bold text"
|
|
214
|
-
|
|
215
|
-
pressKey(editor, 'b', { ctrl: true })
|
|
216
|
-
|
|
217
|
-
const content = getMarkdownContent(editor)
|
|
218
|
-
expect(content).toContain('**bold text**')
|
|
219
|
-
})
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Testing Async Operations
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
it('should handle async updates', async () => {
|
|
226
|
-
let updateTriggered = false
|
|
227
|
-
|
|
228
|
-
const testEditor = await createTestEditor(container, {
|
|
229
|
-
onUpdate: () => { updateTriggered = true }
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
typeText(testEditor, 'New content')
|
|
233
|
-
|
|
234
|
-
await waitFor(() => updateTriggered, 2000)
|
|
235
|
-
expect(updateTriggered).toBe(true)
|
|
236
|
-
})
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## Browser Testing Features
|
|
240
|
-
|
|
241
|
-
### Real DOM Environment
|
|
242
|
-
|
|
243
|
-
Tests run in a real browser environment (Chromium via Playwright), providing:
|
|
244
|
-
- Accurate DOM rendering
|
|
245
|
-
- Real event handling
|
|
246
|
-
- Proper CSS layout
|
|
247
|
-
- Browser-specific behaviors
|
|
248
|
-
|
|
249
|
-
### Visual Testing
|
|
250
|
-
|
|
251
|
-
While not implemented in the current setup, the browser environment supports:
|
|
252
|
-
- Screenshot comparison testing
|
|
253
|
-
- Visual regression testing
|
|
254
|
-
- Layout testing
|
|
255
|
-
|
|
256
|
-
### Performance Testing
|
|
257
|
-
|
|
258
|
-
The browser environment allows for:
|
|
259
|
-
- Measuring render times
|
|
260
|
-
- Testing with large documents
|
|
261
|
-
- Memory usage monitoring
|
|
262
|
-
|
|
263
|
-
## Debugging Tests
|
|
264
|
-
|
|
265
|
-
### Using the UI
|
|
266
|
-
|
|
267
|
-
Run tests with the UI for better debugging:
|
|
268
|
-
|
|
269
|
-
```bash
|
|
270
|
-
npm run test:ui
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
This opens a web interface showing:
|
|
274
|
-
- Test results and failures
|
|
275
|
-
- Test execution timeline
|
|
276
|
-
- Code coverage reports
|
|
277
|
-
- Interactive test running
|
|
278
|
-
|
|
279
|
-
### Browser DevTools
|
|
280
|
-
|
|
281
|
-
When running browser tests, you can:
|
|
282
|
-
- Set `headless: false` in `vitest.config.ts`
|
|
283
|
-
- Use browser DevTools for debugging
|
|
284
|
-
- Inspect the actual DOM during tests
|
|
285
|
-
|
|
286
|
-
### Console Logging
|
|
287
|
-
|
|
288
|
-
Add debug logging in tests:
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
it('should debug editor state', () => {
|
|
292
|
-
console.log('Editor state:', editor.state)
|
|
293
|
-
console.log('Current content:', getMarkdownContent(editor))
|
|
294
|
-
// ... test code
|
|
295
|
-
})
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
## Best Practices
|
|
299
|
-
|
|
300
|
-
### Test Isolation
|
|
301
|
-
|
|
302
|
-
- Always use `beforeEach`/`afterEach` for setup/cleanup
|
|
303
|
-
- Create fresh editor instances for each test
|
|
304
|
-
- Clean up DOM elements after tests
|
|
305
|
-
|
|
306
|
-
### Async Handling
|
|
307
|
-
|
|
308
|
-
- Use `await waitForEditor()` after creating editors
|
|
309
|
-
- Use `waitFor()` for conditional waiting
|
|
310
|
-
- Handle async operations properly
|
|
311
|
-
|
|
312
|
-
### Realistic Testing
|
|
313
|
-
|
|
314
|
-
- Test actual user interactions (typing, clicking)
|
|
315
|
-
- Use real markdown content in tests
|
|
316
|
-
- Test edge cases and error conditions
|
|
317
|
-
|
|
318
|
-
### Performance
|
|
319
|
-
|
|
320
|
-
- Keep tests focused and fast
|
|
321
|
-
- Use appropriate timeouts
|
|
322
|
-
- Clean up resources properly
|
|
323
|
-
|
|
324
|
-
## Troubleshooting
|
|
325
|
-
|
|
326
|
-
### Common Issues
|
|
327
|
-
|
|
328
|
-
1. **Editor not ready**: Always use `await waitForEditor(editor)` after creation
|
|
329
|
-
2. **DOM not found**: Ensure container is created and editor is initialized
|
|
330
|
-
3. **Async timing**: Use `waitFor()` for conditions that may take time
|
|
331
|
-
4. **Memory leaks**: Always call `cleanupEditor()` in `afterEach`
|
|
332
|
-
|
|
333
|
-
### Browser Issues
|
|
334
|
-
|
|
335
|
-
1. **Headless failures**: Set `headless: false` for debugging
|
|
336
|
-
2. **Timeout errors**: Increase timeout values in config
|
|
337
|
-
3. **Worker issues**: Check that worker files are accessible
|
|
338
|
-
|
|
339
|
-
## Contributing
|
|
340
|
-
|
|
341
|
-
When adding new tests:
|
|
342
|
-
|
|
343
|
-
1. Follow the existing test structure
|
|
344
|
-
2. Add utilities to `utils.ts` for reusable functionality
|
|
345
|
-
3. Group related tests in describe blocks
|
|
346
|
-
4. Use descriptive test names
|
|
347
|
-
5. Include both positive and negative test cases
|
|
348
|
-
6. Test browser-specific behaviors when relevant
|
|
349
|
-
|
|
350
|
-
## Future Enhancements
|
|
351
|
-
|
|
352
|
-
Potential improvements to the testing setup:
|
|
353
|
-
|
|
354
|
-
- Visual regression testing with screenshot comparison
|
|
355
|
-
- Performance benchmarking tests
|
|
356
|
-
- Accessibility testing integration
|
|
357
|
-
- Cross-browser testing support
|
|
358
|
-
- Integration with CI/CD pipelines
|
|
359
|
-
- Test data generation utilities
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|