@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,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
  });