@tangle-network/ui 11.4.0 → 11.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +27 -2
- package/dist/chat.js +3 -3
- package/dist/{chunk-4ORVXSMU.js → chunk-J2G7KCYH.js} +13 -14
- package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
- package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
- package/dist/{chunk-SRAKCVPO.js → chunk-R5GVQDL3.js} +1 -1
- package/dist/{chunk-IH2VRPGZ.js → chunk-RKC3GXHE.js} +2 -2
- package/dist/{chunk-J3YZIQNI.js → chunk-XPYPZMYT.js} +1 -1
- package/dist/{chunk-HW4ROISY.js → chunk-XROQW5J7.js} +1 -1
- package/dist/{document-editor-pane-MX3FIZSE.js → document-editor-pane-A3JURPW5.js} +1 -1
- package/dist/{document-editor-pane-C3cYEDWZ.d.ts → document-editor-pane-IAEQn_Dl.d.ts} +8 -4
- package/dist/editor.d.ts +5 -4
- package/dist/editor.js +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -8
- package/dist/run.d.ts +48 -1
- package/dist/run.js +9 -5
- package/dist/sdk-hooks.js +3 -3
- package/package.json +1 -1
- package/src/editor/document-editor-pane.test.tsx +58 -0
- package/src/editor/editor-lazy.test.tsx +102 -0
- package/src/editor/editor-lazy.ts +40 -0
- package/src/editor/editor-loading.tsx +25 -0
- package/src/editor/editor-peers.test.ts +250 -0
- package/src/editor/editor-peers.ts +272 -0
- package/src/editor/editor-provider.test.tsx +226 -0
- package/src/editor/editor-provider.tsx +301 -265
- package/src/editor/editor-toolbar.tsx +1 -1
- package/src/editor/markdown-document-editor.test.tsx +121 -0
- package/src/editor/markdown-document-editor.tsx +114 -82
- package/src/editor/tiptap-editor.test.tsx +228 -0
- package/src/editor/tiptap-editor.tsx +162 -130
- package/src/run/index.ts +6 -0
- /package/dist/{chunk-47XH56SV.js → chunk-LQS34IGP.js} +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
});
|