@elabs-ai/components-editor 4.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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the paste-embed feature (onEmbedAsset hook).
|
|
3
|
+
*
|
|
4
|
+
* Two test layers:
|
|
5
|
+
*
|
|
6
|
+
* 1. Headless Milkdown (no React) — same pattern as insert-directive.test.ts.
|
|
7
|
+
* Boots a real ProseMirror editor with the paste-embed plugin, calls
|
|
8
|
+
* `embedAsset()` directly against the live view, and asserts the document
|
|
9
|
+
* serializes correctly. This is the reliable path: no DOM-event synthesis,
|
|
10
|
+
* no symbol-based view extraction.
|
|
11
|
+
*
|
|
12
|
+
* 2. React + @testing-library — smoke-tests that the prop wires through the
|
|
13
|
+
* React component tree (MarkdownEditor mounts without onEmbedAsset; mounts
|
|
14
|
+
* with it; the editor still shows its content).
|
|
15
|
+
*
|
|
16
|
+
* We mock `@elabs-ai/components-ui` to intercept `toast.error` calls (the Toaster is not
|
|
17
|
+
* mounted in jsdom; the toast call is a side-effect, not the only error cue).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// vi.hoisted() ensures the variable is initialized before the hoisted vi.mock() call.
|
|
21
|
+
const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn() }));
|
|
22
|
+
vi.mock("@elabs-ai/components-ui", async (importOriginal) => {
|
|
23
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
toast: Object.assign(vi.fn(), { error: mockToastError, success: vi.fn() }),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
import { Editor, defaultValueCtx, editorViewCtx, rootCtx } from "@milkdown/kit/core";
|
|
31
|
+
import { commonmark } from "@milkdown/kit/preset/commonmark";
|
|
32
|
+
import { gfm } from "@milkdown/kit/preset/gfm";
|
|
33
|
+
import { getMarkdown } from "@milkdown/kit/utils";
|
|
34
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
35
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
36
|
+
|
|
37
|
+
import { MarkdownEditor } from "./markdown-editor";
|
|
38
|
+
import { embedAsset, pasteEmbedPlugin } from "./paste-embed";
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
cleanup();
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Headless Milkdown helper (mirrors insert-directive.test.ts)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/** Boot a headless Milkdown editor with the paste-embed plugin wired. */
|
|
50
|
+
async function makeEditor(
|
|
51
|
+
onEmbedAsset?: (file: File) => Promise<string>,
|
|
52
|
+
initial = "",
|
|
53
|
+
): Promise<Editor> {
|
|
54
|
+
const root = document.createElement("div");
|
|
55
|
+
document.body.appendChild(root);
|
|
56
|
+
const editor = await Editor.make()
|
|
57
|
+
.config((ctx) => {
|
|
58
|
+
ctx.set(rootCtx, root);
|
|
59
|
+
ctx.set(defaultValueCtx, initial);
|
|
60
|
+
})
|
|
61
|
+
.use(commonmark)
|
|
62
|
+
.use(gfm)
|
|
63
|
+
.use(pasteEmbedPlugin(onEmbedAsset))
|
|
64
|
+
.create();
|
|
65
|
+
return editor;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Serialize the editor's current doc to markdown. */
|
|
69
|
+
function serialize(editor: Editor): string {
|
|
70
|
+
return editor.action(getMarkdown());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get the live ProseMirror view from a Milkdown editor. */
|
|
74
|
+
function getView(editor: Editor) {
|
|
75
|
+
return editor.action((ctx) => ctx.get(editorViewCtx));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// 1. INSERT PATH: onEmbedAsset resolves → image node in document
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
describe("insert path (headless, resolve)", () => {
|
|
82
|
+
let editor: Editor;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
editor = await makeEditor(undefined, "Hello world.\n");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
await editor.destroy();
|
|
90
|
+
document.body.innerHTML = "";
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("embedAsset inserts an image node that serializes to markdown", async () => {
|
|
94
|
+
const resolvedPath = "/assets/test-image.png";
|
|
95
|
+
const onEmbedAsset = vi.fn().mockResolvedValue(resolvedPath);
|
|
96
|
+
const view = getView(editor);
|
|
97
|
+
const file = new File(["px"], "photo.png", { type: "image/png" });
|
|
98
|
+
|
|
99
|
+
embedAsset(view, file, view.state.selection.from, onEmbedAsset);
|
|
100
|
+
|
|
101
|
+
// Wait for the async resolve to settle.
|
|
102
|
+
await vi.waitFor(
|
|
103
|
+
() => {
|
|
104
|
+
const md = serialize(editor);
|
|
105
|
+
expect(md).toContain("![");
|
|
106
|
+
expect(md).toContain(resolvedPath);
|
|
107
|
+
},
|
|
108
|
+
{ timeout: 5000 },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(onEmbedAsset).toHaveBeenCalledWith(file);
|
|
112
|
+
// The alt text is derived from the filename stem.
|
|
113
|
+
expect(serialize(editor)).toContain("photo");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("placeholder decoration is added synchronously then removed after resolve", async () => {
|
|
117
|
+
let resolveUpload!: (path: string) => void;
|
|
118
|
+
const onEmbedAsset = vi.fn(
|
|
119
|
+
() =>
|
|
120
|
+
new Promise<string>((res) => {
|
|
121
|
+
resolveUpload = res;
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const view = getView(editor);
|
|
126
|
+
const file = new File(["px"], "banner.png", { type: "image/png" });
|
|
127
|
+
|
|
128
|
+
embedAsset(view, file, view.state.selection.from, onEmbedAsset);
|
|
129
|
+
|
|
130
|
+
// Placeholder chip DOM node is rendered by ProseMirror as a widget decoration
|
|
131
|
+
// inside the editor root div (which the headless test appended to document.body).
|
|
132
|
+
await vi.waitFor(
|
|
133
|
+
() => {
|
|
134
|
+
expect(document.body.querySelector('[role="status"]')).toBeTruthy();
|
|
135
|
+
},
|
|
136
|
+
{ timeout: 3000 },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Resolve the upload.
|
|
140
|
+
resolveUpload("/uploads/banner.png");
|
|
141
|
+
|
|
142
|
+
// After resolve: placeholder gone, image in doc.
|
|
143
|
+
await vi.waitFor(
|
|
144
|
+
() => {
|
|
145
|
+
expect(document.body.querySelector('[role="status"]')).toBeFalsy();
|
|
146
|
+
expect(serialize(editor)).toContain("![");
|
|
147
|
+
},
|
|
148
|
+
{ timeout: 3000 },
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("image node alt is the filename stem (without extension)", async () => {
|
|
153
|
+
const onEmbedAsset = vi.fn().mockResolvedValue("/cdn/my-photo.png");
|
|
154
|
+
const view = getView(editor);
|
|
155
|
+
const file = new File(["px"], "my-photo.png", { type: "image/png" });
|
|
156
|
+
|
|
157
|
+
embedAsset(view, file, view.state.selection.from, onEmbedAsset);
|
|
158
|
+
|
|
159
|
+
await vi.waitFor(() => expect(serialize(editor)).toContain("my-photo"), { timeout: 5000 });
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 2. FAILURE PATH: onEmbedAsset rejects → no broken image, error chip shown
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
describe("failure path (headless, reject)", () => {
|
|
167
|
+
let editor: Editor;
|
|
168
|
+
|
|
169
|
+
beforeEach(async () => {
|
|
170
|
+
editor = await makeEditor(undefined, "Hello world.\n");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterEach(async () => {
|
|
174
|
+
await editor.destroy();
|
|
175
|
+
document.body.innerHTML = "";
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("on reject: no broken image in doc, error chip shown, toast.error called", async () => {
|
|
179
|
+
const onEmbedAsset = vi.fn().mockRejectedValue(new Error("Network timeout"));
|
|
180
|
+
const view = getView(editor);
|
|
181
|
+
const file = new File(["px"], "fail.png", { type: "image/png" });
|
|
182
|
+
|
|
183
|
+
embedAsset(view, file, view.state.selection.from, onEmbedAsset);
|
|
184
|
+
|
|
185
|
+
await vi.waitFor(
|
|
186
|
+
() => {
|
|
187
|
+
// Error chip must appear.
|
|
188
|
+
expect(document.body.querySelector('[role="alert"]')).toBeTruthy();
|
|
189
|
+
},
|
|
190
|
+
{ timeout: 5000 },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// No `![` in the serialized markdown — broken image was never inserted.
|
|
194
|
+
expect(serialize(editor)).not.toContain("![");
|
|
195
|
+
|
|
196
|
+
// toast.error was called with the error message.
|
|
197
|
+
expect(mockToastError).toHaveBeenCalledWith(expect.stringContaining("Network timeout"));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("error chip carries role=alert and aria-live=assertive", async () => {
|
|
201
|
+
const onEmbedAsset = vi.fn().mockRejectedValue(new Error("403 Forbidden"));
|
|
202
|
+
const view = getView(editor);
|
|
203
|
+
const file = new File(["px"], "denied.png", { type: "image/png" });
|
|
204
|
+
|
|
205
|
+
embedAsset(view, file, view.state.selection.from, onEmbedAsset);
|
|
206
|
+
|
|
207
|
+
await vi.waitFor(() => expect(document.body.querySelector('[role="alert"]')).toBeTruthy(), {
|
|
208
|
+
timeout: 5000,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const chip = document.body.querySelector('[role="alert"]') as HTMLElement | null;
|
|
212
|
+
expect(chip).not.toBeNull();
|
|
213
|
+
expect(chip!.getAttribute("aria-live")).toBe("assertive");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("no error chip leaks from a previous test (isolation)", async () => {
|
|
217
|
+
// Each test gets a fresh editor; afterEach calls document.body.innerHTML = ""
|
|
218
|
+
// so chips from prior tests are gone. Confirm the DOM starts clean.
|
|
219
|
+
expect(document.body.querySelector('[role="alert"]')).toBeFalsy();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// 3. MULTIPLE FILES: each gets its own placeholder → each resolves independently
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
describe("multiple files", () => {
|
|
227
|
+
let editor: Editor;
|
|
228
|
+
|
|
229
|
+
beforeEach(async () => {
|
|
230
|
+
editor = await makeEditor(undefined, "");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
afterEach(async () => {
|
|
234
|
+
await editor.destroy();
|
|
235
|
+
document.body.innerHTML = "";
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("two simultaneous embeds each insert their own image", async () => {
|
|
239
|
+
const onEmbedAsset = vi
|
|
240
|
+
.fn()
|
|
241
|
+
.mockResolvedValueOnce("/cdn/alpha.png")
|
|
242
|
+
.mockResolvedValueOnce("/cdn/beta.png");
|
|
243
|
+
|
|
244
|
+
const view = getView(editor);
|
|
245
|
+
const fileA = new File(["px"], "alpha.png", { type: "image/png" });
|
|
246
|
+
const fileB = new File(["px"], "beta.png", { type: "image/png" });
|
|
247
|
+
|
|
248
|
+
embedAsset(view, fileA, view.state.selection.from, onEmbedAsset);
|
|
249
|
+
embedAsset(view, fileB, view.state.selection.from, onEmbedAsset);
|
|
250
|
+
|
|
251
|
+
await vi.waitFor(
|
|
252
|
+
() => {
|
|
253
|
+
const md = serialize(editor);
|
|
254
|
+
expect(md).toContain("/cdn/alpha.png");
|
|
255
|
+
expect(md).toContain("/cdn/beta.png");
|
|
256
|
+
},
|
|
257
|
+
{ timeout: 5000 },
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// 4. REACT PROP CONTRACT: onEmbedAsset is accepted + forwarded correctly
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
describe("React component prop contract", () => {
|
|
266
|
+
test("MarkdownEditor mounts normally without onEmbedAsset prop", async () => {
|
|
267
|
+
render(<MarkdownEditor defaultValue="# No embed" />);
|
|
268
|
+
await waitFor(() => expect(screen.getByText("No embed")).toBeInTheDocument(), {
|
|
269
|
+
timeout: 8000,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("MarkdownEditor accepts onEmbedAsset prop without type errors", async () => {
|
|
274
|
+
const onEmbedAsset = vi.fn().mockResolvedValue("/path/to/image.png");
|
|
275
|
+
render(<MarkdownEditor defaultValue="# With embed" onEmbedAsset={onEmbedAsset} />);
|
|
276
|
+
await waitFor(() => expect(screen.getByText("With embed")).toBeInTheDocument(), {
|
|
277
|
+
timeout: 8000,
|
|
278
|
+
});
|
|
279
|
+
// onEmbedAsset is set but not triggered (no paste/drop in this test).
|
|
280
|
+
expect(onEmbedAsset).not.toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// 5. UNIT: pasteEmbedPlugin is a no-op when onEmbedAsset is undefined
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
describe("pasteEmbedPlugin no-op without hook", () => {
|
|
288
|
+
test("plugin does not intercept when onEmbedAsset is not provided", async () => {
|
|
289
|
+
const editor = await makeEditor(undefined, "plain text\n");
|
|
290
|
+
// handlePaste returns false for image files when no onEmbedAsset is provided —
|
|
291
|
+
// we verify indirectly: the doc is unchanged and no decoration is added.
|
|
292
|
+
const md = serialize(editor);
|
|
293
|
+
expect(md).toContain("plain text");
|
|
294
|
+
await editor.destroy();
|
|
295
|
+
document.body.innerHTML = "";
|
|
296
|
+
});
|
|
297
|
+
});
|