@tangle-network/ui 11.51.7 → 11.51.8
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/src/editor/source-markdown-editor.test.tsx +7 -7
- package/src/editor/document-editor-pane.test.tsx +0 -60
- package/src/editor/editor-lazy.test.tsx +0 -102
- package/src/editor/editor-peers.test.ts +0 -250
- package/src/editor/editor-provider.test.tsx +0 -226
- package/src/editor/markdown-document-editor.test.tsx +0 -126
- package/src/editor/tiptap-editor.test.tsx +0 -228
- package/src/files/file-artifact-pane.test.tsx +0 -301
- package/src/files/file-card.test.tsx +0 -62
- package/src/files/file-list.test.tsx +0 -81
- package/src/files/pdf-loader.test.ts +0 -130
- package/src/files/pdf-viewer.test.tsx +0 -245
- package/src/files/rich-file-tree.test.tsx +0 -90
- package/src/markdown/api-presentation.test.tsx +0 -63
- package/src/markdown/code-block.test.tsx +0 -201
- package/src/markdown/markdown-table.test.tsx +0 -76
- package/src/markdown/markdown.test.tsx +0 -166
- package/src/openui/schema.test.tsx +0 -158
- package/src/primitives/button.test.tsx +0 -230
- package/src/primitives/card.test.tsx +0 -71
- package/src/primitives/chip.test.tsx +0 -200
- package/src/primitives/date-picker.test.tsx +0 -146
- package/src/primitives/dialog.test.tsx +0 -75
- package/src/primitives/input.test.tsx +0 -161
- package/src/primitives/interaction-states.test.tsx +0 -287
- package/src/primitives/pagination-alert.test.tsx +0 -180
- package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +0 -165
- package/src/primitives/radio-group.test.tsx +0 -26
- package/src/primitives/segmented-control.test.tsx +0 -451
- package/src/primitives/table.test.tsx +0 -72
- package/src/primitives/theme-toggle.test.tsx +0 -168
- package/src/primitives/toast.test.tsx +0 -130
- package/src/primitives/wizard.test.tsx +0 -106
- package/src/run/expandable-content.test.tsx +0 -52
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { Doc } from "yjs";
|
|
4
|
-
import { createEditorProvider, useEditorContext } from "./editor-provider";
|
|
5
|
-
import type * as EditorPeers from "./editor-peers";
|
|
6
|
-
import type { EditorProviderPeers } from "./editor-peers";
|
|
7
|
-
import { useEditorConnection } from "./use-editor";
|
|
8
|
-
|
|
9
|
-
interface ProviderOptions {
|
|
10
|
-
url: string;
|
|
11
|
-
name: string;
|
|
12
|
-
document: Doc;
|
|
13
|
-
onConnect: () => void;
|
|
14
|
-
onDisconnect: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class StubHocuspocusProvider {
|
|
18
|
-
static instances: StubHocuspocusProvider[] = [];
|
|
19
|
-
|
|
20
|
-
awareness = {
|
|
21
|
-
clientID: 1,
|
|
22
|
-
getStates: () => new Map(),
|
|
23
|
-
getLocalState: () => ({}),
|
|
24
|
-
setLocalStateField: vi.fn(),
|
|
25
|
-
on: vi.fn(),
|
|
26
|
-
off: vi.fn(),
|
|
27
|
-
};
|
|
28
|
-
connect = vi.fn();
|
|
29
|
-
disconnect = vi.fn();
|
|
30
|
-
destroy = vi.fn();
|
|
31
|
-
|
|
32
|
-
constructor(readonly options: ProviderOptions) {
|
|
33
|
-
StubHocuspocusProvider.instances.push(this);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Only the members the provider reads; yjs is the real package. */
|
|
38
|
-
function stubPeers() {
|
|
39
|
-
return {
|
|
40
|
-
yjs: { Doc },
|
|
41
|
-
hocuspocus: { HocuspocusProvider: StubHocuspocusProvider },
|
|
42
|
-
} as unknown as EditorProviderPeers;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function ConnectionProbe() {
|
|
46
|
-
// Reads the context through the public hook, not through the provider
|
|
47
|
-
// module, so a context created per factory call would leave it unresolved.
|
|
48
|
-
const { state, isConnected } = useEditorConnection();
|
|
49
|
-
const { doc } = useEditorContext();
|
|
50
|
-
return (
|
|
51
|
-
<span data-testid="probe" data-fragment={doc.getXmlFragment("prosemirror").length}>
|
|
52
|
-
{state}
|
|
53
|
-
{isConnected ? " (live)" : ""}
|
|
54
|
-
</span>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
afterEach(() => {
|
|
59
|
-
StubHocuspocusProvider.instances = [];
|
|
60
|
-
vi.doUnmock("./editor-peers");
|
|
61
|
-
vi.resetModules();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("createEditorProvider", () => {
|
|
65
|
-
it("builds the document and the transport from the loaded peers", () => {
|
|
66
|
-
const EditorProvider = createEditorProvider(stubPeers());
|
|
67
|
-
|
|
68
|
-
render(
|
|
69
|
-
<EditorProvider
|
|
70
|
-
websocketUrl="wss://collab.example/ws"
|
|
71
|
-
documentName="doc:readme"
|
|
72
|
-
token="jwt"
|
|
73
|
-
user={{ name: "Ada" }}
|
|
74
|
-
>
|
|
75
|
-
<ConnectionProbe />
|
|
76
|
-
</EditorProvider>,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
expect(StubHocuspocusProvider.instances).toHaveLength(1);
|
|
80
|
-
const [provider] = StubHocuspocusProvider.instances;
|
|
81
|
-
expect(provider.options.url).toBe("wss://collab.example/ws");
|
|
82
|
-
expect(provider.options.name).toBe("doc:readme");
|
|
83
|
-
// The Y.Doc came from `peers.yjs.Doc`, and the editor asks it for this
|
|
84
|
-
// fragment, so the transport and the editor must share the one instance.
|
|
85
|
-
expect(provider.options.document).toBeInstanceOf(Doc);
|
|
86
|
-
expect(provider.awareness.setLocalStateField).toHaveBeenCalledWith(
|
|
87
|
-
"user",
|
|
88
|
-
expect.objectContaining({ name: "Ada" }),
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("serves the connection state to hooks that read the shared context", async () => {
|
|
93
|
-
const EditorProvider = createEditorProvider(stubPeers());
|
|
94
|
-
const onConnectionChange = vi.fn();
|
|
95
|
-
|
|
96
|
-
render(
|
|
97
|
-
<EditorProvider
|
|
98
|
-
websocketUrl="wss://collab.example/ws"
|
|
99
|
-
documentName="doc:readme"
|
|
100
|
-
token="jwt"
|
|
101
|
-
user={{ name: "Ada" }}
|
|
102
|
-
onConnectionChange={onConnectionChange}
|
|
103
|
-
>
|
|
104
|
-
<ConnectionProbe />
|
|
105
|
-
</EditorProvider>,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
expect(screen.getByTestId("probe")).toHaveTextContent("connecting");
|
|
109
|
-
|
|
110
|
-
const [provider] = StubHocuspocusProvider.instances;
|
|
111
|
-
act(() => {
|
|
112
|
-
provider.options.onConnect();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await waitFor(() => {
|
|
116
|
-
expect(screen.getByTestId("probe")).toHaveTextContent("connected (live)");
|
|
117
|
-
});
|
|
118
|
-
expect(onConnectionChange).toHaveBeenCalledWith("connected");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("destroys the transport when it unmounts", () => {
|
|
122
|
-
const EditorProvider = createEditorProvider(stubPeers());
|
|
123
|
-
|
|
124
|
-
const { unmount } = render(
|
|
125
|
-
<EditorProvider
|
|
126
|
-
websocketUrl="wss://collab.example/ws"
|
|
127
|
-
documentName="doc:readme"
|
|
128
|
-
token="jwt"
|
|
129
|
-
user={{ name: "Ada" }}
|
|
130
|
-
>
|
|
131
|
-
<ConnectionProbe />
|
|
132
|
-
</EditorProvider>,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const [provider] = StubHocuspocusProvider.instances;
|
|
136
|
-
unmount();
|
|
137
|
-
|
|
138
|
-
expect(provider.destroy).toHaveBeenCalledTimes(1);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("EditorProvider", () => {
|
|
143
|
-
it("renders with no tiptap package installed", async () => {
|
|
144
|
-
// The provider builds the document and the socket from yjs and Hocuspocus
|
|
145
|
-
// alone. A consumer that drives its own editor from this context installs
|
|
146
|
-
// those two and nothing else, so reaching for a tiptap namespace here
|
|
147
|
-
// would break that consumer's build-clean install.
|
|
148
|
-
const tiptapSpecifiers = [
|
|
149
|
-
"@tiptap/react",
|
|
150
|
-
"@tiptap/starter-kit",
|
|
151
|
-
"@tiptap/extension-collaboration",
|
|
152
|
-
"@tiptap/extension-collaboration-caret",
|
|
153
|
-
];
|
|
154
|
-
vi.resetModules();
|
|
155
|
-
for (const specifier of tiptapSpecifiers) {
|
|
156
|
-
vi.doMock(specifier, () => {
|
|
157
|
-
throw new Error(`Could not resolve "${specifier}"`);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
vi.doMock("yjs", () => ({ Doc }));
|
|
161
|
-
vi.doMock("@hocuspocus/provider", () => ({
|
|
162
|
-
HocuspocusProvider: StubHocuspocusProvider,
|
|
163
|
-
}));
|
|
164
|
-
const { EditorProvider } = await import("./editor-provider");
|
|
165
|
-
|
|
166
|
-
render(
|
|
167
|
-
<EditorProvider
|
|
168
|
-
websocketUrl="wss://collab.example/ws"
|
|
169
|
-
documentName="doc:readme"
|
|
170
|
-
token="jwt"
|
|
171
|
-
user={{ name: "Ada" }}
|
|
172
|
-
>
|
|
173
|
-
<span data-testid="child">custom editor surface</span>
|
|
174
|
-
</EditorProvider>,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
await waitFor(() => {
|
|
178
|
-
expect(screen.getByTestId("child")).toBeInTheDocument();
|
|
179
|
-
});
|
|
180
|
-
expect(StubHocuspocusProvider.instances).toHaveLength(1);
|
|
181
|
-
|
|
182
|
-
for (const specifier of [...tiptapSpecifiers, "yjs", "@hocuspocus/provider"]) {
|
|
183
|
-
vi.doUnmock(specifier);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("holds children back until the peers load, then renders them", async () => {
|
|
188
|
-
let releasePeers: (() => void) | null = null;
|
|
189
|
-
const gate = new Promise<void>((resolve) => {
|
|
190
|
-
releasePeers = resolve;
|
|
191
|
-
});
|
|
192
|
-
vi.resetModules();
|
|
193
|
-
vi.doMock("./editor-peers", async () => ({
|
|
194
|
-
...(await vi.importActual<typeof EditorPeers>("./editor-peers")),
|
|
195
|
-
loadEditorProviderPeers: async () => {
|
|
196
|
-
await gate;
|
|
197
|
-
return stubPeers();
|
|
198
|
-
},
|
|
199
|
-
}));
|
|
200
|
-
const { EditorProvider } = await import("./editor-provider");
|
|
201
|
-
|
|
202
|
-
render(
|
|
203
|
-
<EditorProvider
|
|
204
|
-
websocketUrl="wss://collab.example/ws"
|
|
205
|
-
documentName="doc:readme"
|
|
206
|
-
token="jwt"
|
|
207
|
-
user={{ name: "Ada" }}
|
|
208
|
-
>
|
|
209
|
-
<span data-testid="child">connected surface</span>
|
|
210
|
-
</EditorProvider>,
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// A child that rendered before the provider would throw out of
|
|
214
|
-
// `useEditorContext`, so the wrapper must render nothing while the gate
|
|
215
|
-
// holds the loader.
|
|
216
|
-
expect(screen.queryByTestId("child")).toBeNull();
|
|
217
|
-
expect(StubHocuspocusProvider.instances).toHaveLength(0);
|
|
218
|
-
|
|
219
|
-
releasePeers?.();
|
|
220
|
-
|
|
221
|
-
await waitFor(() => {
|
|
222
|
-
expect(screen.getByTestId("child")).toBeInTheDocument();
|
|
223
|
-
});
|
|
224
|
-
expect(StubHocuspocusProvider.instances).toHaveLength(1);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import { Component, type ReactNode } from "react";
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { MarkdownDocumentEditor } from "./markdown-document-editor";
|
|
5
|
-
import type * as EditorPeers from "./editor-peers";
|
|
6
|
-
|
|
7
|
-
class ErrorTrap extends Component<{ children: ReactNode }, { message: string | null }> {
|
|
8
|
-
state = { message: null as string | null };
|
|
9
|
-
|
|
10
|
-
static getDerivedStateFromError(error: Error) {
|
|
11
|
-
return { message: error.message };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
render() {
|
|
15
|
-
return this.state.message ?? this.props.children;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Imports the editor with a fresh module graph, so its module-scope `lazy`
|
|
21
|
-
* has not resolved yet. `loadDocumentEditorPeers` is replaced by `load`.
|
|
22
|
-
*/
|
|
23
|
-
async function freshEditor(load: () => Promise<EditorPeers.DocumentEditorPeers>) {
|
|
24
|
-
vi.resetModules();
|
|
25
|
-
vi.doMock("./editor-peers", async () => ({
|
|
26
|
-
...(await vi.importActual<typeof EditorPeers>("./editor-peers")),
|
|
27
|
-
loadDocumentEditorPeers: load,
|
|
28
|
-
}));
|
|
29
|
-
return (await import("./markdown-document-editor")).MarkdownDocumentEditor;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// The first render imports tiptap for real. A cold import on a busy runner
|
|
33
|
-
// takes several seconds, well past waitFor's 1s and a test's 5s defaults.
|
|
34
|
-
const PEERS_LOADED = { timeout: 15_000 };
|
|
35
|
-
const LOADS_PEERS = 30_000;
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
vi.doUnmock("./editor-peers");
|
|
39
|
-
vi.resetModules();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("MarkdownDocumentEditor", () => {
|
|
43
|
-
it("renders the real tiptap surface once the peers load", async () => {
|
|
44
|
-
const { container } = render(
|
|
45
|
-
<MarkdownDocumentEditor value={"# Title\n\nBody text."} />,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
await waitFor(() => {
|
|
49
|
-
expect(container.querySelector(".ProseMirror")).not.toBeNull();
|
|
50
|
-
}, PEERS_LOADED);
|
|
51
|
-
|
|
52
|
-
const surface = container.querySelector(".ProseMirror");
|
|
53
|
-
expect(surface?.querySelector("h1")?.textContent).toBe("Title");
|
|
54
|
-
expect(surface?.textContent).toContain("Body text.");
|
|
55
|
-
// StarterKit arrives through `peers.starterKit.default`. A broken hand-off
|
|
56
|
-
// leaves the document without the heading node that StarterKit configures.
|
|
57
|
-
expect(screen.getByTitle(/^Bold/)).toBeInTheDocument();
|
|
58
|
-
}, LOADS_PEERS);
|
|
59
|
-
|
|
60
|
-
it("holds a placeholder while the peers are still loading", async () => {
|
|
61
|
-
let releasePeers: (() => void) | null = null;
|
|
62
|
-
const gate = new Promise<void>((resolve) => {
|
|
63
|
-
releasePeers = resolve;
|
|
64
|
-
});
|
|
65
|
-
const HeldEditor = await freshEditor(async () => {
|
|
66
|
-
await gate;
|
|
67
|
-
const actual = await vi.importActual<typeof EditorPeers>("./editor-peers");
|
|
68
|
-
return actual.loadDocumentEditorPeers();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const { container } = render(<HeldEditor value="# Title" />);
|
|
72
|
-
|
|
73
|
-
// A live region, so a screen reader hears that the editor is still coming.
|
|
74
|
-
expect(screen.getByRole("status")).toHaveTextContent("Loading editor…");
|
|
75
|
-
expect(container.querySelector(".ProseMirror")).toBeNull();
|
|
76
|
-
|
|
77
|
-
releasePeers?.();
|
|
78
|
-
|
|
79
|
-
await waitFor(() => {
|
|
80
|
-
expect(container.querySelector(".ProseMirror h1")?.textContent).toBe("Title");
|
|
81
|
-
});
|
|
82
|
-
expect(screen.queryByText("Loading editor…")).toBeNull();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("applies a new value prop to the loaded editor without reporting a change", async () => {
|
|
86
|
-
const onChange = vi.fn();
|
|
87
|
-
const { container, rerender } = render(
|
|
88
|
-
<MarkdownDocumentEditor value="start" onChange={onChange} />,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
await waitFor(() => {
|
|
92
|
-
expect(container.querySelector(".ProseMirror")).not.toBeNull();
|
|
93
|
-
}, PEERS_LOADED);
|
|
94
|
-
|
|
95
|
-
// The editor reports once when it parses the initial content. What must
|
|
96
|
-
// not grow is the count across a value-prop write: pushing that back
|
|
97
|
-
// through onChange makes a controlled parent echo its own write.
|
|
98
|
-
const callsAfterMount = onChange.mock.calls.length;
|
|
99
|
-
|
|
100
|
-
rerender(<MarkdownDocumentEditor value="# Changed" onChange={onChange} />);
|
|
101
|
-
|
|
102
|
-
await waitFor(() => {
|
|
103
|
-
expect(container.querySelector(".ProseMirror h1")?.textContent).toBe("Changed");
|
|
104
|
-
});
|
|
105
|
-
expect(onChange).toHaveBeenCalledTimes(callsAfterMount);
|
|
106
|
-
}, LOADS_PEERS);
|
|
107
|
-
|
|
108
|
-
it("fails with the install list when the peers cannot be loaded", async () => {
|
|
109
|
-
const missing = "install @tiptap/react and @tiptap/starter-kit";
|
|
110
|
-
const LoadFailingEditor = await freshEditor(async () => {
|
|
111
|
-
throw new Error(missing);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
render(
|
|
115
|
-
<ErrorTrap>
|
|
116
|
-
<LoadFailingEditor value="start" />
|
|
117
|
-
</ErrorTrap>,
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// Loud, and only here: the consumer's build already succeeded without the
|
|
121
|
-
// peers, so this render is the first place the gap can surface.
|
|
122
|
-
await waitFor(() => {
|
|
123
|
-
expect(screen.getByText(missing)).toBeInTheDocument();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import { Component, type ReactNode } from "react";
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { Doc } from "yjs";
|
|
5
|
-
import type * as EditorPeers from "./editor-peers";
|
|
6
|
-
import { createEditorProvider } from "./editor-provider";
|
|
7
|
-
import { createTiptapEditor } from "./tiptap-editor";
|
|
8
|
-
|
|
9
|
-
class ErrorTrap extends Component<{ children: ReactNode }, { message: string | null }> {
|
|
10
|
-
state = { message: null as string | null };
|
|
11
|
-
|
|
12
|
-
static getDerivedStateFromError(error: Error) {
|
|
13
|
-
return { message: error.message };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
render() {
|
|
17
|
-
return this.state.message ?? this.props.children;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
class StubHocuspocusProvider {
|
|
22
|
-
static instances: StubHocuspocusProvider[] = [];
|
|
23
|
-
|
|
24
|
-
awareness = {
|
|
25
|
-
clientID: 1,
|
|
26
|
-
getStates: () => new Map(),
|
|
27
|
-
getLocalState: () => ({}),
|
|
28
|
-
setLocalStateField: vi.fn(),
|
|
29
|
-
on: vi.fn(),
|
|
30
|
-
off: vi.fn(),
|
|
31
|
-
};
|
|
32
|
-
connect = vi.fn();
|
|
33
|
-
disconnect = vi.fn();
|
|
34
|
-
destroy = vi.fn();
|
|
35
|
-
|
|
36
|
-
constructor(readonly options: { document: Doc }) {
|
|
37
|
-
StubHocuspocusProvider.instances.push(this);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** A tiptap extension records what the editor configured it with. */
|
|
42
|
-
function stubExtension(name: string) {
|
|
43
|
-
const calls: Record<string, unknown>[] = [];
|
|
44
|
-
return {
|
|
45
|
-
calls,
|
|
46
|
-
default: {
|
|
47
|
-
configure(options: Record<string, unknown>) {
|
|
48
|
-
calls.push(options);
|
|
49
|
-
return { name, options };
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Only the members the collaborative editor calls. yjs is the real package,
|
|
57
|
-
* because the provider and the editor must agree on one document.
|
|
58
|
-
*/
|
|
59
|
-
function collaborationStubs() {
|
|
60
|
-
const starterKit = stubExtension("starterKit");
|
|
61
|
-
const collaboration = stubExtension("collaboration");
|
|
62
|
-
const collaborationCaret = stubExtension("collaborationCaret");
|
|
63
|
-
const editorOptions: Record<string, unknown>[] = [];
|
|
64
|
-
const editor = {
|
|
65
|
-
setEditable: vi.fn(),
|
|
66
|
-
isEmpty: true,
|
|
67
|
-
commands: { setContent: vi.fn() },
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const peers = {
|
|
71
|
-
react: {
|
|
72
|
-
useEditor: (options: Record<string, unknown>) => {
|
|
73
|
-
editorOptions.push(options);
|
|
74
|
-
return editor;
|
|
75
|
-
},
|
|
76
|
-
EditorContent: () => <div data-testid="editor-content" />,
|
|
77
|
-
},
|
|
78
|
-
starterKit,
|
|
79
|
-
collaboration,
|
|
80
|
-
collaborationCaret,
|
|
81
|
-
hocuspocus: { HocuspocusProvider: StubHocuspocusProvider },
|
|
82
|
-
yjs: { Doc },
|
|
83
|
-
} as unknown as EditorPeers.CollaborationPeers;
|
|
84
|
-
|
|
85
|
-
return { peers, starterKit, collaboration, collaborationCaret, editorOptions, editor };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderCollaborativeEditor(stubs: ReturnType<typeof collaborationStubs>) {
|
|
89
|
-
const EditorProvider = createEditorProvider(stubs.peers);
|
|
90
|
-
const TiptapEditor = createTiptapEditor(stubs.peers);
|
|
91
|
-
return render(
|
|
92
|
-
<EditorProvider
|
|
93
|
-
websocketUrl="wss://collab.example/ws"
|
|
94
|
-
documentName="doc:readme"
|
|
95
|
-
token="jwt"
|
|
96
|
-
user={{ name: "Ada" }}
|
|
97
|
-
>
|
|
98
|
-
<TiptapEditor />
|
|
99
|
-
</EditorProvider>,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
afterEach(() => {
|
|
104
|
-
StubHocuspocusProvider.instances = [];
|
|
105
|
-
vi.doUnmock("./editor-peers");
|
|
106
|
-
vi.resetModules();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("createTiptapEditor", () => {
|
|
110
|
-
it("renders the editor surface from the loaded namespaces", () => {
|
|
111
|
-
renderCollaborativeEditor(collaborationStubs());
|
|
112
|
-
|
|
113
|
-
expect(screen.getByTestId("editor-content")).toBeInTheDocument();
|
|
114
|
-
expect(screen.getByText("Connecting...")).toBeInTheDocument();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("gives Collaboration the document fragment the provider shares", () => {
|
|
118
|
-
const stubs = collaborationStubs();
|
|
119
|
-
renderCollaborativeEditor(stubs);
|
|
120
|
-
|
|
121
|
-
const [provider] = StubHocuspocusProvider.instances;
|
|
122
|
-
// The extensions rebuild once the transport arrives, so read the last
|
|
123
|
-
// configuration: the transport and the editor must bind to one Y.Doc, or
|
|
124
|
-
// remote edits never reach the surface.
|
|
125
|
-
expect(stubs.collaboration.calls.at(-1)?.fragment).toBe(
|
|
126
|
-
provider.options.document.getXmlFragment("prosemirror"),
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("gives CollaborationCaret the provider and a named user", () => {
|
|
131
|
-
const stubs = collaborationStubs();
|
|
132
|
-
renderCollaborativeEditor(stubs);
|
|
133
|
-
|
|
134
|
-
const [provider] = StubHocuspocusProvider.instances;
|
|
135
|
-
// The caret joins only once the provider carries awareness.
|
|
136
|
-
expect(stubs.collaborationCaret.calls).toHaveLength(1);
|
|
137
|
-
const [caretOptions] = stubs.collaborationCaret.calls;
|
|
138
|
-
expect(caretOptions.provider).toBe(provider);
|
|
139
|
-
// The stub awareness carries no local user yet, so the editor names the
|
|
140
|
-
// fallback rather than passing undefined into the caret renderer.
|
|
141
|
-
expect(caretOptions.user).toEqual({ name: "Anonymous", color: "#808080" });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("passes every configured extension to the editor", () => {
|
|
145
|
-
const stubs = collaborationStubs();
|
|
146
|
-
renderCollaborativeEditor(stubs);
|
|
147
|
-
|
|
148
|
-
expect(stubs.starterKit.calls.at(-1)).toMatchObject({ history: false });
|
|
149
|
-
const options = stubs.editorOptions.at(-1);
|
|
150
|
-
expect(
|
|
151
|
-
(options?.extensions as { name: string }[]).map((one) => one.name),
|
|
152
|
-
).toEqual(["starterKit", "collaboration", "collaborationCaret"]);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe("TiptapEditor", () => {
|
|
157
|
-
it("holds a placeholder while the peers are still loading", async () => {
|
|
158
|
-
let releasePeers: (() => void) | null = null;
|
|
159
|
-
const gate = new Promise<void>((resolve) => {
|
|
160
|
-
releasePeers = resolve;
|
|
161
|
-
});
|
|
162
|
-
const stubs = collaborationStubs();
|
|
163
|
-
vi.resetModules();
|
|
164
|
-
vi.doMock("./editor-peers", async () => ({
|
|
165
|
-
...(await vi.importActual<typeof EditorPeers>("./editor-peers")),
|
|
166
|
-
loadCollaborationPeers: async () => {
|
|
167
|
-
await gate;
|
|
168
|
-
return stubs.peers;
|
|
169
|
-
},
|
|
170
|
-
}));
|
|
171
|
-
const { TiptapEditor } = await import("./tiptap-editor");
|
|
172
|
-
// Both modules come from the reset graph, so the provider and the editor
|
|
173
|
-
// share one EditorContext. A provider built from the loaded peers
|
|
174
|
-
// directly, because the exported EditorProvider waits on the same gate and
|
|
175
|
-
// would render nothing at all.
|
|
176
|
-
const freshProvider = await import("./editor-provider");
|
|
177
|
-
const ResolvedProvider = freshProvider.createEditorProvider(stubs.peers);
|
|
178
|
-
|
|
179
|
-
render(
|
|
180
|
-
<ResolvedProvider
|
|
181
|
-
websocketUrl="wss://collab.example/ws"
|
|
182
|
-
documentName="doc:readme"
|
|
183
|
-
token="jwt"
|
|
184
|
-
user={{ name: "Ada" }}
|
|
185
|
-
>
|
|
186
|
-
<TiptapEditor />
|
|
187
|
-
</ResolvedProvider>,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
expect(screen.getByText("Loading editor…")).toBeInTheDocument();
|
|
191
|
-
expect(screen.queryByTestId("editor-content")).toBeNull();
|
|
192
|
-
|
|
193
|
-
releasePeers?.();
|
|
194
|
-
|
|
195
|
-
await waitFor(() => {
|
|
196
|
-
expect(screen.getByTestId("editor-content")).toBeInTheDocument();
|
|
197
|
-
});
|
|
198
|
-
expect(screen.queryByText("Loading editor…")).toBeNull();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("fails with the install list when the collaboration peers cannot be loaded", async () => {
|
|
202
|
-
const missing =
|
|
203
|
-
"@tangle-network/ui/editor needs its optional collaboration peers. Install yjs.";
|
|
204
|
-
vi.resetModules();
|
|
205
|
-
vi.doMock("./editor-peers", async () => {
|
|
206
|
-
const actual = await vi.importActual<typeof EditorPeers>("./editor-peers");
|
|
207
|
-
return {
|
|
208
|
-
...actual,
|
|
209
|
-
loadCollaborationPeers: async () => {
|
|
210
|
-
throw new actual.MissingEditorPeersError(missing);
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
});
|
|
214
|
-
const { TiptapEditor } = await import("./tiptap-editor");
|
|
215
|
-
|
|
216
|
-
// The wrapper reaches its loader before it reads the editor context, so
|
|
217
|
-
// the install list surfaces without a provider around it.
|
|
218
|
-
render(
|
|
219
|
-
<ErrorTrap>
|
|
220
|
-
<TiptapEditor />
|
|
221
|
-
</ErrorTrap>,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
await waitFor(() => {
|
|
225
|
-
expect(screen.getByText(missing)).toBeInTheDocument();
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
});
|