@surrealdb/ui 1.2.11 → 1.2.13

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.
Files changed (101) hide show
  1. package/.claude/launch.json +17 -0
  2. package/.oxfmtrc.json +27 -0
  3. package/.oxlintrc.json +23 -0
  4. package/.zed/settings.json +34 -34
  5. package/AGENTS.md +21 -19
  6. package/README.md +15 -10
  7. package/dist/assets/245b1ccaa4927818.svg +3 -0
  8. package/dist/assets/333e1fe6e67ca4a2.svg +10 -0
  9. package/dist/assets/4b0e10c655814886.svg +21 -0
  10. package/dist/assets/9932da008ea5dd32.svg +3 -0
  11. package/dist/assets/a372168d5789daa2.svg +4 -0
  12. package/dist/assets/b140e44ee4ae7e35.svg +5 -0
  13. package/dist/assets/e028075817d928e4.svg +3 -0
  14. package/dist/assets/ee1065ad5a8f7f14.svg +6 -0
  15. package/dist/ui.css +1 -1
  16. package/dist/ui.d.ts +579 -1
  17. package/dist/ui.js +11553 -8988
  18. package/dist/ui.js.map +1 -1
  19. package/doc/code-style.md +264 -0
  20. package/doc/stacker-plan.md +219 -0
  21. package/package.json +119 -115
  22. package/res/_mixins.scss +15 -11
  23. package/tests/_setup/e2e-helpers.tsx +85 -85
  24. package/tests/_setup/portable-stories.ts +11 -10
  25. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +140 -139
  26. package/tests/e2e/MarkdownEditor/edits.test.tsx +101 -100
  27. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +35 -34
  28. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +178 -177
  29. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +50 -49
  30. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +195 -194
  31. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +47 -46
  32. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +35 -34
  33. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +39 -38
  34. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +28 -27
  35. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +49 -48
  36. package/tests/e2e/MarkdownEditor/media-align.test.tsx +40 -39
  37. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +49 -48
  38. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +108 -107
  39. package/tests/e2e/MarkdownEditor/modes.test.tsx +79 -78
  40. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +29 -28
  41. package/tests/e2e/MarkdownEditor/regressions.test.tsx +194 -193
  42. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +81 -80
  43. package/tests/e2e/MarkdownEditor/table-click.test.tsx +36 -35
  44. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +44 -43
  45. package/tests/e2e/MarkdownEditor/table-format.test.tsx +28 -27
  46. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +22 -21
  47. package/tests/e2e/MarkdownViewer/parity.test.tsx +144 -143
  48. package/tests/e2e/MarkdownViewer/render.test.tsx +20 -19
  49. package/tests/e2e/Stacker/stacker.test.tsx +304 -0
  50. package/tests/unit/Editor/helpers.test.ts +26 -26
  51. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +26 -25
  52. package/tests/unit/MarkdownConformance/indentation.test.tsx +20 -19
  53. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +255 -0
  54. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +39 -38
  55. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +122 -122
  56. package/tests/unit/MarkdownConformance/links.test.tsx +78 -54
  57. package/tests/unit/MarkdownConformance/render-helpers.tsx +60 -60
  58. package/tests/unit/MarkdownEditor/code-info.test.ts +58 -58
  59. package/tests/unit/MarkdownEditor/decorations.test.ts +423 -421
  60. package/tests/unit/MarkdownEditor/editor-ready.test.ts +22 -22
  61. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +70 -70
  62. package/tests/unit/MarkdownEditor/indented-fence.test.ts +80 -80
  63. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +87 -87
  64. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +51 -51
  65. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +63 -63
  66. package/tests/unit/MarkdownEditor/list-indent.test.ts +67 -67
  67. package/tests/unit/MarkdownEditor/slash-commands.test.ts +150 -150
  68. package/tests/unit/MarkdownEditor/table-format.test.ts +67 -67
  69. package/tests/unit/MarkdownEditor/table.test.ts +96 -96
  70. package/tests/unit/MarkdownEditor/triggers.test.ts +167 -167
  71. package/tests/unit/MarkdownEditor/widget-store.test.ts +90 -90
  72. package/tests/unit/MarkdownViewer/callout.test.tsx +42 -42
  73. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +41 -41
  74. package/tests/unit/MarkdownViewer/code-title.test.tsx +45 -45
  75. package/tests/unit/MarkdownViewer/features.test.tsx +129 -129
  76. package/tests/unit/MarkdownViewer/headings.test.tsx +28 -28
  77. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +53 -53
  78. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +137 -137
  79. package/tests/unit/MarkdownViewer/jsx.test.tsx +188 -188
  80. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +31 -31
  81. package/tests/unit/MarkdownViewer/list-code.test.tsx +45 -45
  82. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +109 -109
  83. package/tests/unit/MarkdownViewer/read-time.test.ts +13 -13
  84. package/tests/unit/MarkdownViewer/renderers.test.tsx +75 -75
  85. package/tests/unit/MarkdownViewer/runnable.test.tsx +51 -51
  86. package/tests/unit/MarkdownViewer/ssr.test.tsx +56 -55
  87. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +43 -42
  88. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +120 -120
  89. package/tests/unit/MarkdownViewer/tabs.test.tsx +83 -82
  90. package/tests/unit/Stacker/drag.test.ts +21 -0
  91. package/tests/unit/Stacker/helpers.test.ts +105 -0
  92. package/tests/unit/Stacker/reducer.test.ts +107 -0
  93. package/tools/duplicate-vector-mappings.json +141 -141
  94. package/tools/glass-export-overrides.json +6 -6
  95. package/tools/icon-parser.ts +41 -40
  96. package/tools/integration-gradient-exports.json +55 -55
  97. package/tools/picto-compare.ts +32 -32
  98. package/tools/picto-merge-urls.ts +19 -19
  99. package/tools/picto-sync.ts +543 -543
  100. package/tools/picto-verify-gradients.ts +50 -50
  101. package/tools/vector-export-nodes.json +975 -975
@@ -3,150 +3,151 @@ import { EditorView } from "@codemirror/view";
3
3
  import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
4
4
  import { composeStories } from "@storybook/react-vite";
5
5
  import { describe, expect, it } from "vitest";
6
+
6
7
  import {
7
- focusAndType,
8
- mountStory,
9
- nextFrame,
10
- preparePreviewEditor,
11
- scrollEditorTo,
12
- sleep,
8
+ focusAndType,
9
+ mountStory,
10
+ nextFrame,
11
+ preparePreviewEditor,
12
+ scrollEditorTo,
13
+ sleep,
13
14
  } from "../../_setup/e2e-helpers";
14
15
  import { md } from "../../_setup/markdown-classes";
15
16
 
16
17
  const Stories = composeStories(MarkdownStories);
17
18
 
18
19
  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
- });
20
+ it("continues an ordered list when Enter is pressed at the end of a numbered line", async () => {
21
+ mountStory(<Stories.Editor />);
22
+ const view = await preparePreviewEditor();
23
+ const doc = view.state.doc.toString();
24
+
25
+ const line = "4. Fourth step";
26
+ const idx = doc.indexOf(line);
27
+ expect(idx).toBeGreaterThan(-1);
28
+ const eol = idx + line.length;
29
+
30
+ await scrollEditorTo(view, idx);
31
+ view.dispatch({ selection: EditorSelection.cursor(eol) });
32
+ await nextFrame();
33
+
34
+ await focusAndType("{Enter}fifth step");
35
+ await nextFrame();
36
+
37
+ expect(view.state.doc.toString()).toMatch(/\n5\.\s*fifth step/);
38
+ });
39
+
40
+ it("shows blockquote text from the sample document", async () => {
41
+ mountStory(<Stories.Editor />);
42
+ const view = await preparePreviewEditor();
43
+ const doc = view.state.doc.toString();
44
+ expect(doc).toContain("> A blockquote");
45
+
46
+ const quotePos = doc.indexOf("> A blockquote");
47
+ await scrollEditorTo(view, quotePos);
48
+ await nextFrame();
49
+
50
+ expect(document.querySelector(`.${md.quote}`)?.textContent ?? "").toMatch(/blockquote/i);
51
+ });
52
+
53
+ it("renders a horizontal rule as a separator", async () => {
54
+ mountStory(<Stories.Editor />);
55
+ const view = await preparePreviewEditor();
56
+ const hrPos = view.state.doc.toString().indexOf("***");
57
+ expect(hrPos).toBeGreaterThan(-1);
58
+ await scrollEditorTo(view, hrPos);
59
+ view.dispatch({ selection: EditorSelection.cursor(0) });
60
+ await nextFrame();
61
+
62
+ let hrs = 0;
63
+ for (let i = 0; i < 40; i++) {
64
+ hrs = document.querySelectorAll(`hr.${md.hr}[aria-label="Horizontal rule"]`).length;
65
+ if (hrs > 0) break;
66
+ await nextFrame();
67
+ }
68
+
69
+ expect(hrs).toBeGreaterThan(0);
70
+ });
71
+
72
+ it("renders a read-only table preview when the table block is inactive", async () => {
73
+ mountStory(<Stories.Editor />);
74
+ const view = await preparePreviewEditor();
75
+ const doc = view.state.doc.toString();
76
+ const tablePos = doc.indexOf("| Feature | Status");
77
+ const afterTable = doc.indexOf("## HTML table");
78
+ expect(tablePos).toBeGreaterThan(-1);
79
+ expect(afterTable).toBeGreaterThan(-1);
80
+ view.dispatch({
81
+ selection: EditorSelection.cursor(afterTable),
82
+ effects: EditorView.scrollIntoView(tablePos, { y: "center" }),
83
+ });
84
+
85
+ let tables = 0;
86
+ for (let i = 0; i < 40; i++) {
87
+ tables = document.querySelectorAll(
88
+ `.cm-editor [data-md-widget="TableWidget"] table.${md.table}`,
89
+ ).length;
90
+ if (tables > 0) break;
91
+ await nextFrame();
92
+ }
93
+
94
+ expect(tables).toBeGreaterThan(0);
95
+ });
96
+
97
+ it("shows GFM table source when the table block is active", async () => {
98
+ mountStory(<Stories.Editor />);
99
+ const view = await preparePreviewEditor();
100
+ const doc = view.state.doc.toString();
101
+ const marker = "| Feature | Status";
102
+ const idx = doc.indexOf(marker);
103
+ expect(idx).toBeGreaterThan(-1);
104
+
105
+ await scrollEditorTo(view, idx);
106
+ view.dispatch({ selection: EditorSelection.cursor(idx + 2) });
107
+ await nextFrame();
108
+
109
+ expect(document.querySelector(`.${md.tableWidget}`)).toBeNull();
110
+ expect(view.state.doc.sliceString(idx, idx + marker.length)).toBe(marker);
111
+ });
112
+
113
+ it("shows fenced TypeScript sample code in the document", async () => {
114
+ mountStory(<Stories.Editor />);
115
+ const view = await preparePreviewEditor();
116
+ const doc = view.state.doc.toString();
117
+ expect(doc).toContain("function add");
118
+
119
+ const codePos = doc.indexOf("function add");
120
+ await scrollEditorTo(view, codePos);
121
+ await nextFrame();
122
+
123
+ expect(document.querySelector(".cm-editor")?.textContent ?? "").toContain("function add");
124
+ });
125
+
126
+ it("exposes clickable links with an href matching the Markdown target", async () => {
127
+ mountStory(<Stories.Editor />);
128
+ const view = await preparePreviewEditor();
129
+ const linkPos = view.state.doc.toString().indexOf("[SurrealDB website]");
130
+ expect(linkPos).toBeGreaterThan(-1);
131
+ await scrollEditorTo(view, linkPos);
132
+ view.dispatch({ selection: EditorSelection.cursor(0) });
133
+ await nextFrame();
134
+
135
+ expect(document.querySelector(`.${md.link}[data-href*='surrealdb.com']`)).not.toBeNull();
136
+ });
137
+
138
+ it("renders a diagram for the fenced mermaid block", async () => {
139
+ mountStory(<Stories.Editor />);
140
+ const view = await preparePreviewEditor();
141
+ const mermaidPos = view.state.doc.toString().indexOf("```mermaid");
142
+ expect(mermaidPos).toBeGreaterThan(-1);
143
+ await scrollEditorTo(view, mermaidPos);
144
+
145
+ let found = false;
146
+ for (let i = 0; i < 60; i++) {
147
+ found = !!document.querySelector('.cm-editor [aria-label="Mermaid diagram"] svg');
148
+ if (found) break;
149
+ await sleep(250);
150
+ }
151
+ expect(found).toBe(true);
152
+ });
152
153
  });
@@ -2,110 +2,111 @@ import { EditorSelection } from "@codemirror/state";
2
2
  import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
3
3
  import { composeStories } from "@storybook/react-vite";
4
4
  import { describe, expect, it } from "vitest";
5
+
5
6
  import {
6
- focusAndType,
7
- getEditorView,
8
- mountStory,
9
- nextFrame,
10
- readDoc,
7
+ focusAndType,
8
+ getEditorView,
9
+ mountStory,
10
+ nextFrame,
11
+ readDoc,
11
12
  } from "../../_setup/e2e-helpers";
12
13
 
13
14
  const Stories = composeStories(MarkdownStories);
14
15
 
15
16
  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
- });
17
+ it("reflects typed characters in the underlying document", async () => {
18
+ mountStory(<Stories.Editor />);
19
+ const view = await getEditorView();
20
+
21
+ view.dispatch({ selection: EditorSelection.cursor(view.state.doc.length) });
22
+ view.focus();
23
+
24
+ await focusAndType("\nfresh content");
25
+ await nextFrame();
26
+
27
+ const doc = await readDoc();
28
+ expect(doc.endsWith("\nfresh content")).toBe(true);
29
+ });
30
+
31
+ it("mirrors the source preview block in the story when the document changes", async () => {
32
+ mountStory(<Stories.Editor />);
33
+ const view = await getEditorView();
34
+
35
+ view.dispatch({
36
+ selection: EditorSelection.cursor(view.state.doc.length),
37
+ changes: { from: view.state.doc.length, insert: "\n\nMirror this line." },
38
+ });
39
+ await nextFrame();
40
+ await nextFrame();
41
+
42
+ const previewBlock = document.querySelector<HTMLPreElement>("pre");
43
+ expect(previewBlock).not.toBeNull();
44
+ expect(previewBlock?.textContent ?? "").toContain("Mirror this line.");
45
+ });
46
+
47
+ it("expands triple backtick into a fenced block with caret on the empty content line", async () => {
48
+ mountStory(<Stories.Editor />);
49
+ const view = await getEditorView();
50
+
51
+ view.focus();
52
+ const insertAt = view.state.doc.length;
53
+ view.dispatch({
54
+ changes: { from: insertAt, insert: "\n\n``" },
55
+ selection: EditorSelection.cursor(insertAt + 4),
56
+ });
57
+ await nextFrame();
58
+
59
+ await focusAndType("`");
60
+ await nextFrame();
61
+
62
+ const doc = await readDoc();
63
+ expect(doc.endsWith("\n\n```\n\n```")).toBe(true);
64
+
65
+ const main = view.state.selection.main;
66
+ expect(main.empty).toBe(true);
67
+ const contentLine = view.state.doc.lineAt(main.head);
68
+ expect(contentLine.text).toBe("");
69
+ expect(main.head).toBe(contentLine.from);
70
+ expect(view.state.doc.line(contentLine.number - 1).text).toBe("```");
71
+ expect(view.state.doc.line(contentLine.number + 1).text).toBe("```");
72
+ });
73
+
74
+ it("wraps a non-empty selection with backticks when ` is typed", async () => {
75
+ mountStory(<Stories.Editor />);
76
+ const view = await getEditorView();
77
+
78
+ const target = "First item";
79
+ const start = view.state.doc.toString().indexOf(target);
80
+ expect(start).toBeGreaterThan(-1);
81
+
82
+ view.focus();
83
+ view.dispatch({ selection: EditorSelection.range(start, start + target.length) });
84
+ await nextFrame();
85
+
86
+ await focusAndType("`");
87
+ await nextFrame();
88
+
89
+ const doc = await readDoc();
90
+ expect(doc).toContain("`First item`");
91
+ });
92
+
93
+ it("auto-continues a bullet list when Enter is pressed at the end of a list item", async () => {
94
+ mountStory(<Stories.Editor />);
95
+ const view = await getEditorView();
96
+
97
+ const target = "- First item";
98
+ const start = view.state.doc.toString().indexOf(target);
99
+ expect(start).toBeGreaterThan(-1);
100
+ const eol = start + target.length;
101
+
102
+ view.focus();
103
+ view.dispatch({ selection: EditorSelection.cursor(eol) });
104
+ await nextFrame();
105
+
106
+ await focusAndType("{Enter}continued");
107
+ await nextFrame();
108
+
109
+ const doc = await readDoc();
110
+ expect(doc).toContain("- First item\n- continued\n- Second item");
111
+ });
111
112
  });
@@ -1,44 +1,45 @@
1
1
  import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
2
2
  import { composeStories } from "@storybook/react-vite";
3
3
  import { describe, expect, it } from "vitest";
4
+
4
5
  import { getEditorView, mountStory, nextFrame } from "../../_setup/e2e-helpers";
5
6
  import { md } from "../../_setup/markdown-classes";
6
7
 
7
8
  const Stories = composeStories(MarkdownStories);
8
9
 
9
10
  describe("MarkdownEditor / heading folds", () => {
10
- it("MarkdownViewer: heading fold control collapses the first section", async () => {
11
- mountStory(<Stories.Viewer />);
12
- await nextFrame();
13
- await nextFrame();
14
-
15
- const folds = document.querySelectorAll<HTMLButtonElement>(
16
- `.${md.headingFold}[data-level="1"]`,
17
- );
18
- expect(folds.length).toBeGreaterThan(0);
19
-
20
- const h1Fold = folds[0];
21
- if (!h1Fold) throw new Error("expected an H1 fold control");
22
-
23
- expect(h1Fold.getAttribute("data-folded")).toBe("false");
24
- expect(h1Fold.getAttribute("aria-label")).toBe("Collapse section");
25
-
26
- h1Fold.click();
27
- await nextFrame();
28
- await nextFrame();
29
-
30
- const folded = document.querySelector<HTMLButtonElement>(
31
- `.${md.headingFold}[data-level="1"]`,
32
- );
33
- expect(folded?.getAttribute("data-folded")).toBe("true");
34
- expect(folded?.getAttribute("aria-label")).toBe("Expand section");
35
- });
36
-
37
- it("preview mode does not render heading fold controls in the editor", async () => {
38
- mountStory(<Stories.Editor />);
39
- await getEditorView();
40
- await nextFrame();
41
-
42
- expect(document.querySelectorAll(`.${md.headingFold}`).length).toBe(0);
43
- });
11
+ it("MarkdownViewer: heading fold control collapses the first section", async () => {
12
+ mountStory(<Stories.Viewer />);
13
+ await nextFrame();
14
+ await nextFrame();
15
+
16
+ const folds = document.querySelectorAll<HTMLButtonElement>(
17
+ `.${md.headingFold}[data-level="1"]`,
18
+ );
19
+ expect(folds.length).toBeGreaterThan(0);
20
+
21
+ const h1Fold = folds[0];
22
+ if (!h1Fold) throw new Error("expected an H1 fold control");
23
+
24
+ expect(h1Fold.getAttribute("data-folded")).toBe("false");
25
+ expect(h1Fold.getAttribute("aria-label")).toBe("Collapse section");
26
+
27
+ h1Fold.click();
28
+ await nextFrame();
29
+ await nextFrame();
30
+
31
+ const folded = document.querySelector<HTMLButtonElement>(
32
+ `.${md.headingFold}[data-level="1"]`,
33
+ );
34
+ expect(folded?.getAttribute("data-folded")).toBe("true");
35
+ expect(folded?.getAttribute("aria-label")).toBe("Expand section");
36
+ });
37
+
38
+ it("preview mode does not render heading fold controls in the editor", async () => {
39
+ mountStory(<Stories.Editor />);
40
+ await getEditorView();
41
+ await nextFrame();
42
+
43
+ expect(document.querySelectorAll(`.${md.headingFold}`).length).toBe(0);
44
+ });
44
45
  });