@surrealdb/ui 1.2.12 → 1.2.14

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 (99) 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 +25 -19
  6. package/CLAUDE.md +111 -0
  7. package/README.md +15 -10
  8. package/dist/icons.d.ts +4 -0
  9. package/dist/icons.js +91 -89
  10. package/dist/icons.js.map +1 -1
  11. package/dist/ui.css +1 -1
  12. package/dist/ui.d.ts +259 -1
  13. package/dist/ui.js +9807 -12265
  14. package/dist/ui.js.map +1 -1
  15. package/doc/code-style.md +264 -0
  16. package/doc/release.md +102 -0
  17. package/doc/stacker-plan.md +219 -0
  18. package/package.json +120 -116
  19. package/release-notes.md +0 -0
  20. package/res/_mixins.scss +15 -11
  21. package/tests/_setup/e2e-helpers.tsx +85 -85
  22. package/tests/_setup/portable-stories.ts +11 -10
  23. package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +140 -139
  24. package/tests/e2e/MarkdownEditor/edits.test.tsx +101 -100
  25. package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +35 -34
  26. package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +178 -177
  27. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +50 -49
  28. package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +195 -194
  29. package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +47 -46
  30. package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +35 -34
  31. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +39 -38
  32. package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +28 -27
  33. package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +49 -48
  34. package/tests/e2e/MarkdownEditor/media-align.test.tsx +40 -39
  35. package/tests/e2e/MarkdownEditor/media-edit.test.tsx +49 -48
  36. package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +108 -107
  37. package/tests/e2e/MarkdownEditor/modes.test.tsx +79 -78
  38. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +29 -28
  39. package/tests/e2e/MarkdownEditor/regressions.test.tsx +194 -193
  40. package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +81 -80
  41. package/tests/e2e/MarkdownEditor/table-click.test.tsx +36 -35
  42. package/tests/e2e/MarkdownEditor/table-controls.test.tsx +44 -43
  43. package/tests/e2e/MarkdownEditor/table-format.test.tsx +28 -27
  44. package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +22 -21
  45. package/tests/e2e/MarkdownViewer/parity.test.tsx +144 -143
  46. package/tests/e2e/MarkdownViewer/render.test.tsx +20 -19
  47. package/tests/e2e/Stacker/stacker.test.tsx +304 -0
  48. package/tests/unit/Editor/helpers.test.ts +26 -26
  49. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +26 -25
  50. package/tests/unit/MarkdownConformance/indentation.test.tsx +20 -19
  51. package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +255 -0
  52. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +39 -38
  53. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +122 -122
  54. package/tests/unit/MarkdownConformance/links.test.tsx +78 -54
  55. package/tests/unit/MarkdownConformance/render-helpers.tsx +60 -60
  56. package/tests/unit/MarkdownEditor/code-info.test.ts +58 -58
  57. package/tests/unit/MarkdownEditor/decorations.test.ts +423 -421
  58. package/tests/unit/MarkdownEditor/editor-ready.test.ts +22 -22
  59. package/tests/unit/MarkdownEditor/html-descriptors.test.ts +70 -70
  60. package/tests/unit/MarkdownEditor/indented-fence.test.ts +80 -80
  61. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +87 -87
  62. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +51 -51
  63. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +63 -63
  64. package/tests/unit/MarkdownEditor/list-indent.test.ts +67 -67
  65. package/tests/unit/MarkdownEditor/slash-commands.test.ts +150 -150
  66. package/tests/unit/MarkdownEditor/table-format.test.ts +67 -67
  67. package/tests/unit/MarkdownEditor/table.test.ts +96 -96
  68. package/tests/unit/MarkdownEditor/triggers.test.ts +167 -167
  69. package/tests/unit/MarkdownEditor/widget-store.test.ts +90 -90
  70. package/tests/unit/MarkdownViewer/callout.test.tsx +42 -42
  71. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +41 -41
  72. package/tests/unit/MarkdownViewer/code-title.test.tsx +45 -45
  73. package/tests/unit/MarkdownViewer/features.test.tsx +129 -129
  74. package/tests/unit/MarkdownViewer/headings.test.tsx +28 -28
  75. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +53 -53
  76. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +137 -137
  77. package/tests/unit/MarkdownViewer/jsx.test.tsx +188 -188
  78. package/tests/unit/MarkdownViewer/list-bullets.test.tsx +31 -31
  79. package/tests/unit/MarkdownViewer/list-code.test.tsx +45 -45
  80. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +109 -109
  81. package/tests/unit/MarkdownViewer/read-time.test.ts +13 -13
  82. package/tests/unit/MarkdownViewer/renderers.test.tsx +75 -75
  83. package/tests/unit/MarkdownViewer/runnable.test.tsx +51 -51
  84. package/tests/unit/MarkdownViewer/ssr.test.tsx +56 -55
  85. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +43 -42
  86. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +120 -120
  87. package/tests/unit/MarkdownViewer/tabs.test.tsx +83 -82
  88. package/tests/unit/Stacker/drag.test.ts +21 -0
  89. package/tests/unit/Stacker/helpers.test.ts +105 -0
  90. package/tests/unit/Stacker/reducer.test.ts +107 -0
  91. package/tools/duplicate-vector-mappings.json +141 -141
  92. package/tools/glass-export-overrides.json +6 -6
  93. package/tools/icon-parser.ts +41 -40
  94. package/tools/integration-gradient-exports.json +55 -55
  95. package/tools/picto-compare.ts +32 -32
  96. package/tools/picto-merge-urls.ts +19 -19
  97. package/tools/picto-sync.ts +543 -543
  98. package/tools/picto-verify-gradients.ts +50 -50
  99. package/tools/vector-export-nodes.json +975 -975
@@ -3,121 +3,122 @@ 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
- mountStory,
8
- nextFrame,
9
- preparePreviewEditor,
10
- scrollEditorTo,
8
+ mountStory,
9
+ nextFrame,
10
+ preparePreviewEditor,
11
+ scrollEditorTo,
11
12
  } from "../../_setup/e2e-helpers";
12
13
  import { md, mdViewer } from "../../_setup/markdown-classes";
13
14
 
14
15
  const Stories = composeStories(MarkdownStories);
15
16
 
16
17
  function findLineStart(doc: string, prefix: string): number {
17
- const idx = doc.indexOf(prefix);
18
- if (idx < 0) throw new Error(`prefix not found in doc: ${prefix}`);
19
- return idx;
18
+ const idx = doc.indexOf(prefix);
19
+ if (idx < 0) throw new Error(`prefix not found in doc: ${prefix}`);
20
+ return idx;
20
21
  }
21
22
 
22
23
  describe("MarkdownEditor / media sizing", () => {
23
- it("images and videos do not exceed the editor content width", async () => {
24
- mountStory(<Stories.Editor />);
25
- const view = await preparePreviewEditor();
26
-
27
- const docStr = view.state.doc.toString();
28
- const imageLine = findLineStart(docStr, "![SurrealDB logo]");
29
- const videoLine = findLineStart(docStr, "![Product demo]");
30
-
31
- // Caret away from media lines so replace widgets render.
32
- view.dispatch({ selection: EditorSelection.cursor(0) });
33
- await scrollEditorTo(view, imageLine);
34
- await nextFrame();
35
-
36
- const scroller = view.scrollDOM;
37
- const scrollerWidth = scroller.clientWidth;
38
- expect(scrollerWidth).toBeGreaterThan(0);
39
-
40
- let imgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(
41
- `.cm-editor [data-md-widget="ImageWidget"] img, .cm-editor .${md.image}`,
42
- );
43
- for (let i = 0; i < 40 && imgs.length === 0; i++) {
44
- await nextFrame();
45
- imgs = document.querySelectorAll(
46
- `.cm-editor [data-md-widget="ImageWidget"] img, .cm-editor .${md.image}`,
47
- );
48
- }
49
-
50
- expect(imgs.length).toBeGreaterThan(0);
51
- for (const img of Array.from(imgs)) {
52
- const rect = img.getBoundingClientRect();
53
- expect(
54
- rect.width,
55
- `image ${img.src} width ${rect.width} vs scroller ${scrollerWidth}`,
56
- ).toBeLessThanOrEqual(scrollerWidth + 2);
57
- }
58
-
59
- await scrollEditorTo(view, videoLine);
60
- await nextFrame();
61
-
62
- let videos: NodeListOf<HTMLElement> = document.querySelectorAll(
63
- `.cm-editor [data-md-widget="VideoWidget"] video, .cm-editor [data-md-widget="VideoWidget"] .${md.image}`,
64
- );
65
- for (let i = 0; i < 40 && videos.length === 0; i++) {
66
- await nextFrame();
67
- videos = document.querySelectorAll(
68
- `.cm-editor [data-md-widget="VideoWidget"] video, .cm-editor [data-md-widget="VideoWidget"] .${md.image}`,
69
- );
70
- }
71
-
72
- expect(videos.length).toBeGreaterThan(0);
73
- for (const video of Array.from(videos)) {
74
- const host =
75
- video.closest("[data-md-widget='VideoWidget']") ?? video.closest(`.${md.image}`);
76
- const rect = (host ?? video).getBoundingClientRect();
77
- expect(
78
- rect.width,
79
- `video width ${rect.width} vs scroller ${scrollerWidth}`,
80
- ).toBeLessThanOrEqual(scrollerWidth + 2);
81
- }
82
- });
83
-
84
- it("matches MarkdownViewer image width in the split playground", async () => {
85
- mountStory(<Stories.SideBySide />);
86
- const view = await preparePreviewEditor();
87
-
88
- const docStr = view.state.doc.toString();
89
- const imagePos = findLineStart(docStr, "![SurrealDB logo]");
90
- view.dispatch({
91
- selection: EditorSelection.cursor(0),
92
- effects: EditorView.scrollIntoView(imagePos, { y: "center" }),
93
- });
94
- await nextFrame();
95
-
96
- let editorImg: HTMLImageElement | null = null;
97
- let viewerImg: HTMLImageElement | null = null;
98
- let editorW = 0;
99
- let viewerW = 0;
100
-
101
- for (let i = 0; i < 40; i++) {
102
- editorImg = document.querySelector<HTMLImageElement>(
103
- `.cm-editor [data-md-widget="ImageWidget"] img`,
104
- );
105
- viewerImg = document.querySelector<HTMLImageElement>(
106
- `.${mdViewer.viewerRoot} img.${md.image}`,
107
- );
108
- if (editorImg && viewerImg) {
109
- editorW = editorImg.getBoundingClientRect().width;
110
- viewerW = viewerImg.getBoundingClientRect().width;
111
- if (editorW > 0 && viewerW > 0 && Math.abs(editorW - viewerW) <= 12) {
112
- break;
113
- }
114
- }
115
- await nextFrame();
116
- }
117
-
118
- expect(editorImg).not.toBeNull();
119
- expect(viewerImg).not.toBeNull();
120
- // Editor `.cm-line` uses 6px inline padding mirrored in viewer.module.scss.
121
- expect(Math.abs(editorW - viewerW)).toBeLessThanOrEqual(12);
122
- });
24
+ it("images and videos do not exceed the editor content width", async () => {
25
+ mountStory(<Stories.Editor />);
26
+ const view = await preparePreviewEditor();
27
+
28
+ const docStr = view.state.doc.toString();
29
+ const imageLine = findLineStart(docStr, "![SurrealDB logo]");
30
+ const videoLine = findLineStart(docStr, "![Product demo]");
31
+
32
+ // Caret away from media lines so replace widgets render.
33
+ view.dispatch({ selection: EditorSelection.cursor(0) });
34
+ await scrollEditorTo(view, imageLine);
35
+ await nextFrame();
36
+
37
+ const scroller = view.scrollDOM;
38
+ const scrollerWidth = scroller.clientWidth;
39
+ expect(scrollerWidth).toBeGreaterThan(0);
40
+
41
+ let imgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(
42
+ `.cm-editor [data-md-widget="ImageWidget"] img, .cm-editor .${md.image}`,
43
+ );
44
+ for (let i = 0; i < 40 && imgs.length === 0; i++) {
45
+ await nextFrame();
46
+ imgs = document.querySelectorAll(
47
+ `.cm-editor [data-md-widget="ImageWidget"] img, .cm-editor .${md.image}`,
48
+ );
49
+ }
50
+
51
+ expect(imgs.length).toBeGreaterThan(0);
52
+ for (const img of Array.from(imgs)) {
53
+ const rect = img.getBoundingClientRect();
54
+ expect(
55
+ rect.width,
56
+ `image ${img.src} width ${rect.width} vs scroller ${scrollerWidth}`,
57
+ ).toBeLessThanOrEqual(scrollerWidth + 2);
58
+ }
59
+
60
+ await scrollEditorTo(view, videoLine);
61
+ await nextFrame();
62
+
63
+ let videos: NodeListOf<HTMLElement> = document.querySelectorAll(
64
+ `.cm-editor [data-md-widget="VideoWidget"] video, .cm-editor [data-md-widget="VideoWidget"] .${md.image}`,
65
+ );
66
+ for (let i = 0; i < 40 && videos.length === 0; i++) {
67
+ await nextFrame();
68
+ videos = document.querySelectorAll(
69
+ `.cm-editor [data-md-widget="VideoWidget"] video, .cm-editor [data-md-widget="VideoWidget"] .${md.image}`,
70
+ );
71
+ }
72
+
73
+ expect(videos.length).toBeGreaterThan(0);
74
+ for (const video of Array.from(videos)) {
75
+ const host =
76
+ video.closest("[data-md-widget='VideoWidget']") ?? video.closest(`.${md.image}`);
77
+ const rect = (host ?? video).getBoundingClientRect();
78
+ expect(
79
+ rect.width,
80
+ `video width ${rect.width} vs scroller ${scrollerWidth}`,
81
+ ).toBeLessThanOrEqual(scrollerWidth + 2);
82
+ }
83
+ });
84
+
85
+ it("matches MarkdownViewer image width in the split playground", async () => {
86
+ mountStory(<Stories.SideBySide />);
87
+ const view = await preparePreviewEditor();
88
+
89
+ const docStr = view.state.doc.toString();
90
+ const imagePos = findLineStart(docStr, "![SurrealDB logo]");
91
+ view.dispatch({
92
+ selection: EditorSelection.cursor(0),
93
+ effects: EditorView.scrollIntoView(imagePos, { y: "center" }),
94
+ });
95
+ await nextFrame();
96
+
97
+ let editorImg: HTMLImageElement | null = null;
98
+ let viewerImg: HTMLImageElement | null = null;
99
+ let editorW = 0;
100
+ let viewerW = 0;
101
+
102
+ for (let i = 0; i < 40; i++) {
103
+ editorImg = document.querySelector<HTMLImageElement>(
104
+ `.cm-editor [data-md-widget="ImageWidget"] img`,
105
+ );
106
+ viewerImg = document.querySelector<HTMLImageElement>(
107
+ `.${mdViewer.viewerRoot} img.${md.image}`,
108
+ );
109
+ if (editorImg && viewerImg) {
110
+ editorW = editorImg.getBoundingClientRect().width;
111
+ viewerW = viewerImg.getBoundingClientRect().width;
112
+ if (editorW > 0 && viewerW > 0 && Math.abs(editorW - viewerW) <= 12) {
113
+ break;
114
+ }
115
+ }
116
+ await nextFrame();
117
+ }
118
+
119
+ expect(editorImg).not.toBeNull();
120
+ expect(viewerImg).not.toBeNull();
121
+ // Editor `.cm-line` uses 6px inline padding mirrored in viewer.module.scss.
122
+ expect(Math.abs(editorW - viewerW)).toBeLessThanOrEqual(12);
123
+ });
123
124
  });
@@ -2,92 +2,93 @@ 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
- getEditorView,
7
- mountStory,
8
- nextFrame,
9
- page,
10
- readDoc,
11
- userEvent,
7
+ getEditorView,
8
+ mountStory,
9
+ nextFrame,
10
+ page,
11
+ readDoc,
12
+ userEvent,
12
13
  } from "../../_setup/e2e-helpers";
13
14
  import { md, mdViewer } from "../../_setup/markdown-classes";
14
15
 
15
16
  const Stories = composeStories(MarkdownStories);
16
17
 
17
18
  async function getMode(): Promise<string | null> {
18
- const editor = document.querySelector<HTMLElement>(".cm-editor");
19
- return editor?.getAttribute("data-markdown-mode") ?? null;
19
+ const editor = document.querySelector<HTMLElement>(".cm-editor");
20
+ return editor?.getAttribute("data-markdown-mode") ?? null;
20
21
  }
21
22
 
22
23
  describe("MarkdownEditor / modes", () => {
23
- it("toggling the SegmentedControl swaps mode without remounting the editor", async () => {
24
- mountStory(<Stories.Editor />);
25
- await getEditorView();
26
-
27
- const initialEditor = document.querySelector(".cm-editor");
28
- expect(initialEditor).not.toBeNull();
29
- expect(await getMode()).toBe("preview");
30
-
31
- // Mantine's SegmentedControl hides the underlying radio inputs and
32
- // styles a sibling label, so we click the visible label text.
33
- await userEvent.click(page.getByText("Source", { exact: true }));
34
- await nextFrame();
35
- expect(await getMode()).toBe("source");
36
- expect(document.querySelector(".cm-editor")).toBe(initialEditor);
37
-
38
- await userEvent.click(page.getByText("Preview", { exact: true }));
39
- await nextFrame();
40
- expect(await getMode()).toBe("preview");
41
- expect(document.querySelector(".cm-editor")).toBe(initialEditor);
42
- });
43
-
44
- it("standalone MarkdownViewer is not a CodeMirror surface", async () => {
45
- mountStory(<Stories.Viewer />);
46
- await nextFrame();
47
- await nextFrame();
48
-
49
- expect(document.querySelector(`.${mdViewer.viewerRoot}`)).not.toBeNull();
50
- expect(document.querySelector(".cm-editor")).toBeNull();
51
- expect(document.querySelector(".cm-content")).toBeNull();
52
- });
53
-
54
- it("source mode renders raw markdown markers (no hidden marks)", async () => {
55
- mountStory(<Stories.Editor />);
56
- await getEditorView();
57
- await userEvent.click(page.getByText("Source", { exact: true }));
58
- await nextFrame();
59
- await getEditorView();
60
-
61
- await nextFrame();
62
- const hidden = document.querySelectorAll(`.${md.markHidden}`);
63
- expect(hidden.length).toBe(0);
64
-
65
- const text = document.querySelector(".cm-content")?.textContent ?? "";
66
- expect(text).toContain("# Welcome to the live preview editor");
67
- expect(text).toContain("**bold**");
68
- });
69
-
70
- it("keeps edits when cycling Source then Preview after a document change", async () => {
71
- mountStory(<Stories.Editor />);
72
- const view = await getEditorView();
73
-
74
- const token = `\nuniq-e2e-${Date.now()}`;
75
- view.dispatch({
76
- selection: EditorSelection.cursor(view.state.doc.length),
77
- changes: { from: view.state.doc.length, insert: token },
78
- });
79
- await nextFrame();
80
-
81
- await userEvent.click(page.getByText("Source", { exact: true }));
82
- await nextFrame();
83
- expect((await readDoc()).includes(token.trim())).toBe(true);
84
-
85
- await userEvent.click(page.getByText("Preview", { exact: true }));
86
- await nextFrame();
87
-
88
- expect((await readDoc()).includes(token.trim())).toBe(true);
89
- expect(document.querySelector(".cm-content")?.getAttribute("aria-readonly")).not.toBe(
90
- "true",
91
- );
92
- });
24
+ it("toggling the SegmentedControl swaps mode without remounting the editor", async () => {
25
+ mountStory(<Stories.Editor />);
26
+ await getEditorView();
27
+
28
+ const initialEditor = document.querySelector(".cm-editor");
29
+ expect(initialEditor).not.toBeNull();
30
+ expect(await getMode()).toBe("preview");
31
+
32
+ // Mantine's SegmentedControl hides the underlying radio inputs and
33
+ // styles a sibling label, so we click the visible label text.
34
+ await userEvent.click(page.getByText("Source", { exact: true }));
35
+ await nextFrame();
36
+ expect(await getMode()).toBe("source");
37
+ expect(document.querySelector(".cm-editor")).toBe(initialEditor);
38
+
39
+ await userEvent.click(page.getByText("Preview", { exact: true }));
40
+ await nextFrame();
41
+ expect(await getMode()).toBe("preview");
42
+ expect(document.querySelector(".cm-editor")).toBe(initialEditor);
43
+ });
44
+
45
+ it("standalone MarkdownViewer is not a CodeMirror surface", async () => {
46
+ mountStory(<Stories.Viewer />);
47
+ await nextFrame();
48
+ await nextFrame();
49
+
50
+ expect(document.querySelector(`.${mdViewer.viewerRoot}`)).not.toBeNull();
51
+ expect(document.querySelector(".cm-editor")).toBeNull();
52
+ expect(document.querySelector(".cm-content")).toBeNull();
53
+ });
54
+
55
+ it("source mode renders raw markdown markers (no hidden marks)", async () => {
56
+ mountStory(<Stories.Editor />);
57
+ await getEditorView();
58
+ await userEvent.click(page.getByText("Source", { exact: true }));
59
+ await nextFrame();
60
+ await getEditorView();
61
+
62
+ await nextFrame();
63
+ const hidden = document.querySelectorAll(`.${md.markHidden}`);
64
+ expect(hidden.length).toBe(0);
65
+
66
+ const text = document.querySelector(".cm-content")?.textContent ?? "";
67
+ expect(text).toContain("# Welcome to the live preview editor");
68
+ expect(text).toContain("**bold**");
69
+ });
70
+
71
+ it("keeps edits when cycling Source then Preview after a document change", async () => {
72
+ mountStory(<Stories.Editor />);
73
+ const view = await getEditorView();
74
+
75
+ const token = `\nuniq-e2e-${Date.now()}`;
76
+ view.dispatch({
77
+ selection: EditorSelection.cursor(view.state.doc.length),
78
+ changes: { from: view.state.doc.length, insert: token },
79
+ });
80
+ await nextFrame();
81
+
82
+ await userEvent.click(page.getByText("Source", { exact: true }));
83
+ await nextFrame();
84
+ expect((await readDoc()).includes(token.trim())).toBe(true);
85
+
86
+ await userEvent.click(page.getByText("Preview", { exact: true }));
87
+ await nextFrame();
88
+
89
+ expect((await readDoc()).includes(token.trim())).toBe(true);
90
+ expect(document.querySelector(".cm-content")?.getAttribute("aria-readonly")).not.toBe(
91
+ "true",
92
+ );
93
+ });
93
94
  });
@@ -1,6 +1,7 @@
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 { mountStory, nextFrame } from "../../_setup/e2e-helpers";
5
6
 
6
7
  const Stories = composeStories(MarkdownStories);
@@ -13,37 +14,37 @@ const WEBP_IMAGE_SRC = "cu72bvkattbs738rle0g.webp";
13
14
  const VIDEO_SRC = "cu72bvkattbs738rle0g.mp4";
14
15
 
15
16
  async function waitFor<T>(get: () => T | null | undefined, tries = 60): Promise<T | null> {
16
- for (let i = 0; i < tries; i++) {
17
- const value = get();
18
- if (value) return value;
19
- await nextFrame();
20
- }
21
- return null;
17
+ for (let i = 0; i < tries; i++) {
18
+ const value = get();
19
+ if (value) return value;
20
+ await nextFrame();
21
+ }
22
+ return null;
22
23
  }
23
24
 
24
25
  describe("MarkdownEditor / onImage parity (side-by-side story)", () => {
25
- it("resolves CDN codes to URLs by extension in both the editor preview and the viewer", async () => {
26
- mountStory(<Stories.SideBySideOnImage />);
26
+ it("resolves CDN codes to URLs by extension in both the editor preview and the viewer", async () => {
27
+ mountStory(<Stories.SideBySideOnImage />);
27
28
 
28
- // A bare code resolves to a `.auto` image; a `.webp` code resolves to a
29
- // webp image. Both the editor widget and the viewer render each — the same
30
- // onImage resolver feeds both surfaces. The editor widget mounts a frame
31
- // after the viewer, so poll until parity.
32
- const autoImages = await waitFor(() => {
33
- const found = document.querySelectorAll(`img[src*="${AUTO_IMAGE_SRC}"]`);
34
- return found.length >= 2 ? found : null;
35
- });
36
- expect(autoImages?.length ?? 0).toBeGreaterThanOrEqual(2);
37
- expect(
38
- document.querySelectorAll(`img[src*="${WEBP_IMAGE_SRC}"]`).length,
39
- ).toBeGreaterThanOrEqual(2);
29
+ // A bare code resolves to a `.auto` image; a `.webp` code resolves to a
30
+ // webp image. Both the editor widget and the viewer render each — the same
31
+ // onImage resolver feeds both surfaces. The editor widget mounts a frame
32
+ // after the viewer, so poll until parity.
33
+ const autoImages = await waitFor(() => {
34
+ const found = document.querySelectorAll(`img[src*="${AUTO_IMAGE_SRC}"]`);
35
+ return found.length >= 2 ? found : null;
36
+ });
37
+ expect(autoImages?.length ?? 0).toBeGreaterThanOrEqual(2);
38
+ expect(
39
+ document.querySelectorAll(`img[src*="${WEBP_IMAGE_SRC}"]`).length,
40
+ ).toBeGreaterThanOrEqual(2);
40
41
 
41
- // A `.mp4` code resolves to a video container, so both surfaces render a player.
42
- const videos = await waitFor(() => {
43
- const found = document.querySelectorAll("video");
44
- return found.length >= 2 ? found : null;
45
- });
46
- expect(videos?.length ?? 0).toBeGreaterThanOrEqual(2);
47
- expect(document.body.innerHTML).toContain(VIDEO_SRC);
48
- });
42
+ // A `.mp4` code resolves to a video container, so both surfaces render a player.
43
+ const videos = await waitFor(() => {
44
+ const found = document.querySelectorAll("video");
45
+ return found.length >= 2 ? found : null;
46
+ });
47
+ expect(videos?.length ?? 0).toBeGreaterThanOrEqual(2);
48
+ expect(document.body.innerHTML).toContain(VIDEO_SRC);
49
+ });
49
50
  });