@surrealdb/ui 1.1.1 → 1.2.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/.zed/settings.json +36 -0
- package/AGENTS.md +10 -5
- package/README.md +30 -0
- package/REVIEW.md +1 -1
- package/dist/assets/0d2c2f665b0f41ed.woff2 +0 -0
- package/dist/assets/12b57c6beacdbca0.woff2 +0 -0
- package/dist/assets/23645aad5ccc2b92.woff2 +0 -0
- package/dist/assets/8bbfa6e01a9e6a0f.woff2 +0 -0
- package/dist/assets/93fc40a807be6880.woff2 +0 -0
- package/dist/assets/9c9751ca111e97c2.woff2 +0 -0
- package/dist/assets/9ff55a8a9670220d.woff2 +0 -0
- package/dist/assets/a865edea076e0166.woff2 +0 -0
- package/dist/assets/b921df26851c5aca.woff2 +0 -0
- package/dist/assets/c6a3f4e555097159.woff2 +0 -0
- package/dist/assets/c6c31cb1350b2544.woff2 +0 -0
- package/dist/fonts.css +1 -0
- package/dist/{yoopta.d.ts → fonts.d.ts} +6 -6
- package/dist/fonts.js +2 -0
- package/dist/fonts.js.map +1 -0
- package/dist/icons.d.ts +6 -6
- package/dist/ui.css +1 -1
- package/dist/ui.d.ts +544 -531
- package/dist/ui.js +16325 -14683
- package/dist/ui.js.map +1 -1
- package/package.json +22 -24
- package/tests/_setup/e2e-helpers.tsx +169 -0
- package/tests/_setup/markdown-classes.ts +3 -0
- package/tests/_setup/portable-stories.ts +10 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-continues-an-ordered-list-when-Enter-is-pressed-at-the-end-of-a-numbered-line-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-diagram-for-the-fenced-mermaid-block-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-horizontal-rule-as-a-separator-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-renders-a-read-only-table-preview-when-the-table-block-is-inactive-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-blockquote-text-from-the-sample-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/content-blocks.test.tsx/MarkdownEditor---content-blocks-shows-fenced-TypeScript-sample-code-in-the-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-auto-continues-a-bullet-list-when-Enter-is-pressed-at-the-end-of-a-list-item-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-expands-triple-backtick-into-a-fenced-code-block-with-the-caret-on-the-empty-body-line-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/edits.test.tsx/MarkdownEditor---edits-reflects-typed-characters-in-the-underlying-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/heading-fold.test.tsx/MarkdownEditor---heading-folds-viewer--folds-and-unfolds-a-heading-section-via-the-margin-control-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-clicking-a-callout-header-focuses-the-editor-and-parks-the-caret-inside-the-callout--REASONING--4--1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-keeps-list-bullets-consistent-after-hopping-the-caret-across-items-several-times-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-reveals-heading-marks-when-the-caret-enters-the-heading-line-and-hides-them-when-it-leaves-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---hybrid-widgets-shows-the-right-callout-title-when-moving-focus-between-consecutive-callouts-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-clicking-a-callout-header-focuses-the-editor-with-the-caret-in-that-callout-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-keeps-list-bullets-consistent-after-hopping-the-caret-across-items-several-times-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/hybrid-widgets.test.tsx/MarkdownEditor---preview-widgets-shows-the-right-callout-title-when-moving-focus-between-consecutive-callouts-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---block-JSX-components-clicking-the-edit-source-action-selects-the-component-source-range-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---block-JSX-components-renders-block-components-via-JsxBlockWidget-with-an-edit-source-action-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-block-content.test.tsx/MarkdownEditor---inline-JSX-rendering-block-content-keeps-multiple-inline-JSX-widgets-on-the-same-line-side-by-side-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-highlight.test.tsx/MarkdownEditor---JSX-attribute-highlighting-does-not-let-the-nested-HTML-parser-mis-highlight-attributes-after-an-expression-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/jsx-selection.test.tsx/MarkdownEditor---JSX-selection-reveal-shows-JSX-widget-when-inactive-and-raw-source-when-caret-is-inside-the-tag-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-images-and-videos-do-not-exceed-the-editor-content-width-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/media-sizing.test.tsx/MarkdownEditor---media-sizing-matches-MarkdownViewer-image-width-in-the-split-playground-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-standalone-MarkdownViewer-is-not-a-CodeMirror-surface-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/modes.test.tsx/MarkdownEditor---modes-toggling-the-SegmentedControl-swaps-mode-without-remounting-the-editor-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-repeated-checkbox-toggles-do-not-duplicate-or-drift-task-markers-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-scroll-position-is-preserved-when-the-document-is-edited--chat-92584463--1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-task-checkbox-toggles-between-checked-and-unchecked-in-the-source-document-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/regressions.test.tsx/MarkdownEditor---regressions-toggle-via-checkbox-preserves-task-marker-widgets-visible-from-the-previous-caret-position-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-dismisses-on-Escape-and-leaves-the-slash-as-literal-text-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-highlights-the-slash-and-shows-an-Enter-command-placeholder-until-text-is-typed-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/slash-commands.test.tsx/MarkdownEditor---slash-commands-opens-a-filtered--keyboard-navigable-menu-and-inserts-boilerplate-on-Enter-1.png +0 -0
- package/tests/e2e/MarkdownEditor/__screenshots__/undo-redo.test.tsx/MarkdownEditor---undo-and-redo-restores-undone-document-edits-via-redo-1.png +0 -0
- package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +152 -0
- package/tests/e2e/MarkdownEditor/edits.test.tsx +111 -0
- package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +44 -0
- package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +192 -0
- package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +242 -0
- package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +68 -0
- package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +59 -0
- package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +43 -0
- package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +67 -0
- package/tests/e2e/MarkdownEditor/media-align.test.tsx +57 -0
- package/tests/e2e/MarkdownEditor/media-edit.test.tsx +63 -0
- package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +123 -0
- package/tests/e2e/MarkdownEditor/modes.test.tsx +93 -0
- package/tests/e2e/MarkdownEditor/regressions.test.tsx +182 -0
- package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +99 -0
- package/tests/e2e/MarkdownEditor/table-click.test.tsx +47 -0
- package/tests/e2e/MarkdownEditor/table-controls.test.tsx +56 -0
- package/tests/e2e/MarkdownEditor/table-format.test.tsx +41 -0
- package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +38 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-computed-font-size-on-first-heading-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-counts-for-shared-structural-classes-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/parity.test.tsx/MarkdownViewer---editor-parity-matches-visible-text-between-preview-editor--blurred--and-MarkdownViewer-1.png +0 -0
- package/tests/e2e/MarkdownViewer/__screenshots__/render.test.tsx/MarkdownViewer---render-exercises-shared-preview-class-names-without-mounting-CodeMirror-1.png +0 -0
- package/tests/e2e/MarkdownViewer/parity.test.tsx +190 -0
- package/tests/e2e/MarkdownViewer/render.test.tsx +35 -0
- package/tests/unit/Editor/helpers.test.ts +42 -0
- package/tests/unit/MarkdownEditor/code-info.test.ts +63 -0
- package/tests/unit/MarkdownEditor/decorations.test.ts +488 -0
- package/tests/unit/MarkdownEditor/editor-ready.test.ts +36 -0
- package/tests/unit/MarkdownEditor/html-descriptors.test.ts +94 -0
- package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +115 -0
- package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +88 -0
- package/tests/unit/MarkdownEditor/list-indent.test.ts +95 -0
- package/tests/unit/MarkdownEditor/slash-commands.test.ts +213 -0
- package/tests/unit/MarkdownEditor/table-format.test.ts +83 -0
- package/tests/unit/MarkdownEditor/table.test.ts +119 -0
- package/tests/unit/MarkdownEditor/triggers.test.ts +244 -0
- package/tests/unit/MarkdownEditor/widget-store.test.ts +105 -0
- package/tests/unit/MarkdownViewer/code-title.test.tsx +62 -0
- package/tests/unit/MarkdownViewer/features.test.tsx +110 -0
- package/tests/unit/MarkdownViewer/headings.test.tsx +40 -0
- package/tests/unit/MarkdownViewer/jsx.test.tsx +211 -0
- package/tests/unit/MarkdownViewer/list-bullets.test.tsx +49 -0
- package/tests/unit/MarkdownViewer/list-code.test.tsx +65 -0
- package/tests/unit/MarkdownViewer/renderers.test.tsx +79 -0
- package/tests/unit/MarkdownViewer/runnable.test.tsx +69 -0
- package/tests/unit/MarkdownViewer/ssr.test.tsx +93 -0
- package/dist/yoopta.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surrealdb/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"./styles.css": {
|
|
16
16
|
"import": "./dist/ui.css"
|
|
17
17
|
},
|
|
18
|
-
"./
|
|
19
|
-
"import": "./dist/
|
|
18
|
+
"./fonts.css": {
|
|
19
|
+
"import": "./dist/fonts.css"
|
|
20
20
|
},
|
|
21
21
|
"./mixins": {
|
|
22
22
|
"import": "./res/_mixins.scss"
|
|
@@ -31,16 +31,25 @@
|
|
|
31
31
|
"qau": "biome check . --write --unsafe",
|
|
32
32
|
"qts": "tsc --noEmit",
|
|
33
33
|
"qtsw": "tsc --noEmit --watch",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:unit": "vitest run --project=unit",
|
|
36
|
+
"test:e2e": "vitest run --project=e2e",
|
|
37
|
+
"test:storybook": "vitest run --project=storybook",
|
|
38
|
+
"test:watch": "vitest",
|
|
34
39
|
"icons": "bun run tools/icon-parser.ts"
|
|
35
40
|
},
|
|
36
41
|
"dependencies": {
|
|
42
|
+
"@codemirror/lang-markdown": "^6.5.0",
|
|
37
43
|
"@codemirror/legacy-modes": "^6.4.2",
|
|
44
|
+
"@fontsource-variable/geist": "^5.2.9",
|
|
45
|
+
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
38
46
|
"@lezer/common": "^1.2.1",
|
|
39
47
|
"@lezer/go": "^1.0.0",
|
|
40
48
|
"@lezer/highlight": "^1.2.1",
|
|
41
49
|
"@lezer/html": "^1.3.10",
|
|
42
50
|
"@lezer/javascript": "^1.4.17",
|
|
43
51
|
"@lezer/json": "^1.0.3",
|
|
52
|
+
"@lezer/markdown": "^1.6.3",
|
|
44
53
|
"@lezer/php": "^1.0.2",
|
|
45
54
|
"@lezer/python": "^1.1.14",
|
|
46
55
|
"@lezer/rust": "^1.0.2",
|
|
@@ -52,7 +61,7 @@
|
|
|
52
61
|
"vite-tsconfig-paths": "^6.0.5"
|
|
53
62
|
},
|
|
54
63
|
"devDependencies": {
|
|
55
|
-
"@biomejs/biome": "2.
|
|
64
|
+
"@biomejs/biome": "2.4.12",
|
|
56
65
|
"@chromatic-com/storybook": "^4.1.3",
|
|
57
66
|
"@laynezh/vite-plugin-lib-assets": "^2.1.3",
|
|
58
67
|
"@storybook/addon-a11y": "^10.1.11",
|
|
@@ -69,16 +78,8 @@
|
|
|
69
78
|
"@vitejs/plugin-react": "^5.0.2",
|
|
70
79
|
"@vitest/browser-playwright": "^4.0.16",
|
|
71
80
|
"@vitest/coverage-v8": "^4.0.16",
|
|
72
|
-
"@yoopta/blockquote": "^6.0.1",
|
|
73
|
-
"@yoopta/callout": "^6.0.1",
|
|
74
|
-
"@yoopta/divider": "^6.0.1",
|
|
75
|
-
"@yoopta/editor": "^6.0.1",
|
|
76
|
-
"@yoopta/headings": "^6.0.1",
|
|
77
|
-
"@yoopta/lists": "^6.0.1",
|
|
78
|
-
"@yoopta/marks": "^6.0.1",
|
|
79
|
-
"@yoopta/paragraph": "^6.0.1",
|
|
80
|
-
"@yoopta/ui": "^6.0.1",
|
|
81
81
|
"jsdom": "^28.0.0",
|
|
82
|
+
"mermaid": "^11.14.0",
|
|
82
83
|
"playwright": "^1.57.0",
|
|
83
84
|
"sass-embedded": "^1.92.1",
|
|
84
85
|
"slate": "^0.120.0",
|
|
@@ -100,20 +101,17 @@
|
|
|
100
101
|
"@codemirror/search": "^6.5.6",
|
|
101
102
|
"@codemirror/state": "^6.4.1",
|
|
102
103
|
"@codemirror/view": "^6.24.1",
|
|
103
|
-
"@mantine/core": "^9.
|
|
104
|
-
"@mantine/hooks": "^9.
|
|
105
|
-
"
|
|
106
|
-
"@yoopta/callout": "^6",
|
|
107
|
-
"@yoopta/divider": "^6",
|
|
108
|
-
"@yoopta/editor": "^6",
|
|
109
|
-
"@yoopta/headings": "^6",
|
|
110
|
-
"@yoopta/lists": "^6",
|
|
111
|
-
"@yoopta/marks": "^6",
|
|
112
|
-
"@yoopta/paragraph": "^6",
|
|
113
|
-
"@yoopta/ui": "^6",
|
|
104
|
+
"@mantine/core": "^9.2.2",
|
|
105
|
+
"@mantine/hooks": "^9.2.2",
|
|
106
|
+
"mermaid": "^11",
|
|
114
107
|
"react": "^19",
|
|
115
108
|
"slate": "^0.120.0",
|
|
116
109
|
"slate-dom": "^0.119.0",
|
|
117
110
|
"slate-react": "^0.120.0"
|
|
111
|
+
},
|
|
112
|
+
"peerDependenciesMeta": {
|
|
113
|
+
"mermaid": {
|
|
114
|
+
"optional": true
|
|
115
|
+
}
|
|
118
116
|
}
|
|
119
117
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
import { page, userEvent } from "@vitest/browser/context";
|
|
3
|
+
import { type ComponentType, createElement, type ReactElement } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach } from "vitest";
|
|
6
|
+
|
|
7
|
+
interface MountedStory {
|
|
8
|
+
container: HTMLElement;
|
|
9
|
+
unmount: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const activeMounts = new Set<MountedStory>();
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const mount of activeMounts) {
|
|
16
|
+
mount.unmount();
|
|
17
|
+
}
|
|
18
|
+
activeMounts.clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Mount a composed Storybook story (or any React component) into a fresh
|
|
23
|
+
* detached container appended to `document.body`. Cleanup happens after
|
|
24
|
+
* each test automatically; tests can still call `unmount()` early.
|
|
25
|
+
*/
|
|
26
|
+
export function mountStory(node: ReactElement | ComponentType): MountedStory {
|
|
27
|
+
const container = document.createElement("div");
|
|
28
|
+
container.dataset.testRoot = "true";
|
|
29
|
+
document.body.appendChild(container);
|
|
30
|
+
|
|
31
|
+
let root: Root | null = createRoot(container);
|
|
32
|
+
const element = isReactElement(node) ? node : createElement(node);
|
|
33
|
+
root.render(element);
|
|
34
|
+
|
|
35
|
+
const mount: MountedStory = {
|
|
36
|
+
container,
|
|
37
|
+
unmount: () => {
|
|
38
|
+
if (!root) return;
|
|
39
|
+
root.unmount();
|
|
40
|
+
root = null;
|
|
41
|
+
container.remove();
|
|
42
|
+
activeMounts.delete(mount);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
activeMounts.add(mount);
|
|
46
|
+
return mount;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isReactElement(node: ReactElement | ComponentType): node is ReactElement {
|
|
50
|
+
return typeof node !== "function" && typeof node === "object";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wait for the CodeMirror editor to mount and return the underlying
|
|
55
|
+
* `EditorView`. Polls `EditorView.findFromDOM` so callers can read the
|
|
56
|
+
* authoritative state (selection, doc, mode) directly inside assertions.
|
|
57
|
+
*/
|
|
58
|
+
export async function getEditorView(timeoutMs = 5000): Promise<EditorView> {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
while (Date.now() - start < timeoutMs) {
|
|
61
|
+
const dom = document.querySelector<HTMLElement>(".cm-editor");
|
|
62
|
+
if (dom) {
|
|
63
|
+
const view = EditorView.findFromDOM(dom);
|
|
64
|
+
if (view) return view;
|
|
65
|
+
}
|
|
66
|
+
await sleep(20);
|
|
67
|
+
}
|
|
68
|
+
throw new Error("Timed out waiting for the markdown EditorView to mount");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Returns the `.cm-content` editable element once it's in the DOM. */
|
|
72
|
+
export async function getContentElement(timeoutMs = 5000): Promise<HTMLElement> {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
while (Date.now() - start < timeoutMs) {
|
|
75
|
+
const dom = document.querySelector<HTMLElement>(".cm-content");
|
|
76
|
+
if (dom) return dom;
|
|
77
|
+
await sleep(20);
|
|
78
|
+
}
|
|
79
|
+
throw new Error("Timed out waiting for .cm-content");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Focus the underlying CodeMirror `EditorView` without moving the caret
|
|
84
|
+
* (unlike `userEvent.click`, which relocates the caret to the click
|
|
85
|
+
* target). Useful when the test has already positioned the selection via
|
|
86
|
+
* `view.dispatch({ selection: ... })`.
|
|
87
|
+
*/
|
|
88
|
+
export async function focusEditor(): Promise<EditorView> {
|
|
89
|
+
const view = await getEditorView();
|
|
90
|
+
view.focus();
|
|
91
|
+
await getContentElement();
|
|
92
|
+
return view;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Type at the currently focused element. Caller should call `focusEditor()` first. */
|
|
96
|
+
export async function typeText(text: string): Promise<void> {
|
|
97
|
+
await userEvent.keyboard(text);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Convenience: focus the editor and type at the current caret. */
|
|
101
|
+
export async function focusAndType(text: string): Promise<void> {
|
|
102
|
+
await focusEditor();
|
|
103
|
+
await userEvent.keyboard(text);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve when the next animation frame fires. Useful between dispatch
|
|
108
|
+
* and DOM assertions so CodeMirror's measure cycle has time to run.
|
|
109
|
+
*/
|
|
110
|
+
export function nextFrame(): Promise<void> {
|
|
111
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Sleep helper for the polling loops above. */
|
|
115
|
+
export function sleep(ms: number): Promise<void> {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the current markdown source out of the live EditorView.
|
|
121
|
+
*/
|
|
122
|
+
export async function readDoc(): Promise<string> {
|
|
123
|
+
const view = await getEditorView();
|
|
124
|
+
return view.state.doc.toString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Wait until the editor reports `data-markdown-mode="preview"`. */
|
|
128
|
+
export async function ensurePreviewMode(timeoutMs = 5000): Promise<void> {
|
|
129
|
+
const start = Date.now();
|
|
130
|
+
while (Date.now() - start < timeoutMs) {
|
|
131
|
+
const mode = document.querySelector(".cm-editor")?.getAttribute("data-markdown-mode");
|
|
132
|
+
if (mode === "preview") return;
|
|
133
|
+
await nextFrame();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const preview = page.getByText("Preview", { exact: true });
|
|
137
|
+
|
|
138
|
+
if (preview.elements().length > 0) {
|
|
139
|
+
await preview.click();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const retryStart = Date.now();
|
|
143
|
+
while (Date.now() - retryStart < timeoutMs) {
|
|
144
|
+
const mode = document.querySelector(".cm-editor")?.getAttribute("data-markdown-mode");
|
|
145
|
+
if (mode === "preview") return;
|
|
146
|
+
await nextFrame();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new Error("Timed out waiting for preview mode");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Mount helper: editor view + preview mode + focus. */
|
|
153
|
+
export async function preparePreviewEditor(): Promise<EditorView> {
|
|
154
|
+
const view = await getEditorView();
|
|
155
|
+
await ensurePreviewMode();
|
|
156
|
+
view.focus();
|
|
157
|
+
await nextFrame();
|
|
158
|
+
return view;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Scroll the editor so `pos` is centred in the viewport. */
|
|
162
|
+
export async function scrollEditorTo(view: EditorView, pos: number): Promise<void> {
|
|
163
|
+
view.dispatch({
|
|
164
|
+
effects: EditorView.scrollIntoView(pos, { y: "center" }),
|
|
165
|
+
});
|
|
166
|
+
await nextFrame();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { page, userEvent };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
|
2
|
+
import { setProjectAnnotations } from "@storybook/react-vite";
|
|
3
|
+
import * as projectAnnotations from "../../.storybook/preview";
|
|
4
|
+
|
|
5
|
+
// Calling `setProjectAnnotations` here ensures any `composeStories(...)` invocation
|
|
6
|
+
// inside the e2e suite gets the same Mantine provider, theme, font links, and a11y
|
|
7
|
+
// decorators that real Storybook stories receive in the iframe. The storybook
|
|
8
|
+
// vitest plugin does this for in-story `play` tests; the e2e project does it
|
|
9
|
+
// here so cross-feature browser tests can mount existing stories verbatim.
|
|
10
|
+
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { EditorSelection } from "@codemirror/state";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
|
|
4
|
+
import { composeStories } from "@storybook/react-vite";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
focusAndType,
|
|
8
|
+
mountStory,
|
|
9
|
+
nextFrame,
|
|
10
|
+
preparePreviewEditor,
|
|
11
|
+
scrollEditorTo,
|
|
12
|
+
sleep,
|
|
13
|
+
} from "../../_setup/e2e-helpers";
|
|
14
|
+
import { md } from "../../_setup/markdown-classes";
|
|
15
|
+
|
|
16
|
+
const Stories = composeStories(MarkdownStories);
|
|
17
|
+
|
|
18
|
+
describe("MarkdownEditor / content blocks", () => {
|
|
19
|
+
it("continues an ordered list when Enter is pressed at the end of a numbered line", async () => {
|
|
20
|
+
mountStory(<Stories.Editor />);
|
|
21
|
+
const view = await preparePreviewEditor();
|
|
22
|
+
const doc = view.state.doc.toString();
|
|
23
|
+
|
|
24
|
+
const line = "4. Fourth step";
|
|
25
|
+
const idx = doc.indexOf(line);
|
|
26
|
+
expect(idx).toBeGreaterThan(-1);
|
|
27
|
+
const eol = idx + line.length;
|
|
28
|
+
|
|
29
|
+
await scrollEditorTo(view, idx);
|
|
30
|
+
view.dispatch({ selection: EditorSelection.cursor(eol) });
|
|
31
|
+
await nextFrame();
|
|
32
|
+
|
|
33
|
+
await focusAndType("{Enter}fifth step");
|
|
34
|
+
await nextFrame();
|
|
35
|
+
|
|
36
|
+
expect(view.state.doc.toString()).toMatch(/\n5\.\s*fifth step/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("shows blockquote text from the sample document", async () => {
|
|
40
|
+
mountStory(<Stories.Editor />);
|
|
41
|
+
const view = await preparePreviewEditor();
|
|
42
|
+
const doc = view.state.doc.toString();
|
|
43
|
+
expect(doc).toContain("> A blockquote");
|
|
44
|
+
|
|
45
|
+
const quotePos = doc.indexOf("> A blockquote");
|
|
46
|
+
await scrollEditorTo(view, quotePos);
|
|
47
|
+
await nextFrame();
|
|
48
|
+
|
|
49
|
+
expect(document.querySelector(`.${md.quote}`)?.textContent ?? "").toMatch(/blockquote/i);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders a horizontal rule as a separator", async () => {
|
|
53
|
+
mountStory(<Stories.Editor />);
|
|
54
|
+
const view = await preparePreviewEditor();
|
|
55
|
+
const hrPos = view.state.doc.toString().indexOf("***");
|
|
56
|
+
expect(hrPos).toBeGreaterThan(-1);
|
|
57
|
+
await scrollEditorTo(view, hrPos);
|
|
58
|
+
view.dispatch({ selection: EditorSelection.cursor(0) });
|
|
59
|
+
await nextFrame();
|
|
60
|
+
|
|
61
|
+
let hrs = 0;
|
|
62
|
+
for (let i = 0; i < 40; i++) {
|
|
63
|
+
hrs = document.querySelectorAll(`hr.${md.hr}[aria-label="Horizontal rule"]`).length;
|
|
64
|
+
if (hrs > 0) break;
|
|
65
|
+
await nextFrame();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expect(hrs).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders a read-only table preview when the table block is inactive", async () => {
|
|
72
|
+
mountStory(<Stories.Editor />);
|
|
73
|
+
const view = await preparePreviewEditor();
|
|
74
|
+
const doc = view.state.doc.toString();
|
|
75
|
+
const tablePos = doc.indexOf("| Feature | Status");
|
|
76
|
+
const afterTable = doc.indexOf("## HTML table");
|
|
77
|
+
expect(tablePos).toBeGreaterThan(-1);
|
|
78
|
+
expect(afterTable).toBeGreaterThan(-1);
|
|
79
|
+
view.dispatch({
|
|
80
|
+
selection: EditorSelection.cursor(afterTable),
|
|
81
|
+
effects: EditorView.scrollIntoView(tablePos, { y: "center" }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let tables = 0;
|
|
85
|
+
for (let i = 0; i < 40; i++) {
|
|
86
|
+
tables = document.querySelectorAll(
|
|
87
|
+
`.cm-editor [data-md-widget="TableWidget"] table.${md.table}`,
|
|
88
|
+
).length;
|
|
89
|
+
if (tables > 0) break;
|
|
90
|
+
await nextFrame();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
expect(tables).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows GFM table source when the table block is active", async () => {
|
|
97
|
+
mountStory(<Stories.Editor />);
|
|
98
|
+
const view = await preparePreviewEditor();
|
|
99
|
+
const doc = view.state.doc.toString();
|
|
100
|
+
const marker = "| Feature | Status";
|
|
101
|
+
const idx = doc.indexOf(marker);
|
|
102
|
+
expect(idx).toBeGreaterThan(-1);
|
|
103
|
+
|
|
104
|
+
await scrollEditorTo(view, idx);
|
|
105
|
+
view.dispatch({ selection: EditorSelection.cursor(idx + 2) });
|
|
106
|
+
await nextFrame();
|
|
107
|
+
|
|
108
|
+
expect(document.querySelector(`.${md.tableWidget}`)).toBeNull();
|
|
109
|
+
expect(view.state.doc.sliceString(idx, idx + marker.length)).toBe(marker);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("shows fenced TypeScript sample code in the document", async () => {
|
|
113
|
+
mountStory(<Stories.Editor />);
|
|
114
|
+
const view = await preparePreviewEditor();
|
|
115
|
+
const doc = view.state.doc.toString();
|
|
116
|
+
expect(doc).toContain("function add");
|
|
117
|
+
|
|
118
|
+
const codePos = doc.indexOf("function add");
|
|
119
|
+
await scrollEditorTo(view, codePos);
|
|
120
|
+
await nextFrame();
|
|
121
|
+
|
|
122
|
+
expect(document.querySelector(".cm-editor")?.textContent ?? "").toContain("function add");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("exposes clickable links with an href matching the Markdown target", async () => {
|
|
126
|
+
mountStory(<Stories.Editor />);
|
|
127
|
+
const view = await preparePreviewEditor();
|
|
128
|
+
const linkPos = view.state.doc.toString().indexOf("[SurrealDB website]");
|
|
129
|
+
expect(linkPos).toBeGreaterThan(-1);
|
|
130
|
+
await scrollEditorTo(view, linkPos);
|
|
131
|
+
view.dispatch({ selection: EditorSelection.cursor(0) });
|
|
132
|
+
await nextFrame();
|
|
133
|
+
|
|
134
|
+
expect(document.querySelector(`.${md.link}[data-href*='surrealdb.com']`)).not.toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("renders a diagram for the fenced mermaid block", async () => {
|
|
138
|
+
mountStory(<Stories.Editor />);
|
|
139
|
+
const view = await preparePreviewEditor();
|
|
140
|
+
const mermaidPos = view.state.doc.toString().indexOf("```mermaid");
|
|
141
|
+
expect(mermaidPos).toBeGreaterThan(-1);
|
|
142
|
+
await scrollEditorTo(view, mermaidPos);
|
|
143
|
+
|
|
144
|
+
let found = false;
|
|
145
|
+
for (let i = 0; i < 60; i++) {
|
|
146
|
+
found = !!document.querySelector('.cm-editor [aria-label="Mermaid diagram"] svg');
|
|
147
|
+
if (found) break;
|
|
148
|
+
await sleep(250);
|
|
149
|
+
}
|
|
150
|
+
expect(found).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { EditorSelection } from "@codemirror/state";
|
|
2
|
+
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
|
|
3
|
+
import { composeStories } from "@storybook/react-vite";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
focusAndType,
|
|
7
|
+
getEditorView,
|
|
8
|
+
mountStory,
|
|
9
|
+
nextFrame,
|
|
10
|
+
readDoc,
|
|
11
|
+
} from "../../_setup/e2e-helpers";
|
|
12
|
+
|
|
13
|
+
const Stories = composeStories(MarkdownStories);
|
|
14
|
+
|
|
15
|
+
describe("MarkdownEditor / edits", () => {
|
|
16
|
+
it("reflects typed characters in the underlying document", async () => {
|
|
17
|
+
mountStory(<Stories.Editor />);
|
|
18
|
+
const view = await getEditorView();
|
|
19
|
+
|
|
20
|
+
view.dispatch({ selection: EditorSelection.cursor(view.state.doc.length) });
|
|
21
|
+
view.focus();
|
|
22
|
+
|
|
23
|
+
await focusAndType("\nfresh content");
|
|
24
|
+
await nextFrame();
|
|
25
|
+
|
|
26
|
+
const doc = await readDoc();
|
|
27
|
+
expect(doc.endsWith("\nfresh content")).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("mirrors the source preview block in the story when the document changes", async () => {
|
|
31
|
+
mountStory(<Stories.Editor />);
|
|
32
|
+
const view = await getEditorView();
|
|
33
|
+
|
|
34
|
+
view.dispatch({
|
|
35
|
+
selection: EditorSelection.cursor(view.state.doc.length),
|
|
36
|
+
changes: { from: view.state.doc.length, insert: "\n\nMirror this line." },
|
|
37
|
+
});
|
|
38
|
+
await nextFrame();
|
|
39
|
+
await nextFrame();
|
|
40
|
+
|
|
41
|
+
const previewBlock = document.querySelector<HTMLPreElement>("pre");
|
|
42
|
+
expect(previewBlock).not.toBeNull();
|
|
43
|
+
expect(previewBlock?.textContent ?? "").toContain("Mirror this line.");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("expands triple backtick into a fenced block with caret on the empty content line", async () => {
|
|
47
|
+
mountStory(<Stories.Editor />);
|
|
48
|
+
const view = await getEditorView();
|
|
49
|
+
|
|
50
|
+
view.focus();
|
|
51
|
+
const insertAt = view.state.doc.length;
|
|
52
|
+
view.dispatch({
|
|
53
|
+
changes: { from: insertAt, insert: "\n\n``" },
|
|
54
|
+
selection: EditorSelection.cursor(insertAt + 4),
|
|
55
|
+
});
|
|
56
|
+
await nextFrame();
|
|
57
|
+
|
|
58
|
+
await focusAndType("`");
|
|
59
|
+
await nextFrame();
|
|
60
|
+
|
|
61
|
+
const doc = await readDoc();
|
|
62
|
+
expect(doc.endsWith("\n\n```\n\n```")).toBe(true);
|
|
63
|
+
|
|
64
|
+
const main = view.state.selection.main;
|
|
65
|
+
expect(main.empty).toBe(true);
|
|
66
|
+
const contentLine = view.state.doc.lineAt(main.head);
|
|
67
|
+
expect(contentLine.text).toBe("");
|
|
68
|
+
expect(main.head).toBe(contentLine.from);
|
|
69
|
+
expect(view.state.doc.line(contentLine.number - 1).text).toBe("```");
|
|
70
|
+
expect(view.state.doc.line(contentLine.number + 1).text).toBe("```");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("wraps a non-empty selection with backticks when ` is typed", async () => {
|
|
74
|
+
mountStory(<Stories.Editor />);
|
|
75
|
+
const view = await getEditorView();
|
|
76
|
+
|
|
77
|
+
const target = "First item";
|
|
78
|
+
const start = view.state.doc.toString().indexOf(target);
|
|
79
|
+
expect(start).toBeGreaterThan(-1);
|
|
80
|
+
|
|
81
|
+
view.focus();
|
|
82
|
+
view.dispatch({ selection: EditorSelection.range(start, start + target.length) });
|
|
83
|
+
await nextFrame();
|
|
84
|
+
|
|
85
|
+
await focusAndType("`");
|
|
86
|
+
await nextFrame();
|
|
87
|
+
|
|
88
|
+
const doc = await readDoc();
|
|
89
|
+
expect(doc).toContain("`First item`");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("auto-continues a bullet list when Enter is pressed at the end of a list item", async () => {
|
|
93
|
+
mountStory(<Stories.Editor />);
|
|
94
|
+
const view = await getEditorView();
|
|
95
|
+
|
|
96
|
+
const target = "- First item";
|
|
97
|
+
const start = view.state.doc.toString().indexOf(target);
|
|
98
|
+
expect(start).toBeGreaterThan(-1);
|
|
99
|
+
const eol = start + target.length;
|
|
100
|
+
|
|
101
|
+
view.focus();
|
|
102
|
+
view.dispatch({ selection: EditorSelection.cursor(eol) });
|
|
103
|
+
await nextFrame();
|
|
104
|
+
|
|
105
|
+
await focusAndType("{Enter}continued");
|
|
106
|
+
await nextFrame();
|
|
107
|
+
|
|
108
|
+
const doc = await readDoc();
|
|
109
|
+
expect(doc).toContain("- First item\n- continued\n- Second item");
|
|
110
|
+
});
|
|
111
|
+
});
|