@tangle-network/ui 11.51.6 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -0
  3. package/dist/chat/agent-timeline.d.ts +1 -1
  4. package/dist/chat/speaker.d.ts +2 -2
  5. package/dist/chat.d.ts +2 -2
  6. package/dist/index.d.ts +4 -4
  7. package/package.json +31 -1
  8. package/src/editor/source-markdown-editor.test.tsx +7 -7
  9. package/src/editor/document-editor-pane.test.tsx +0 -60
  10. package/src/editor/editor-lazy.test.tsx +0 -102
  11. package/src/editor/editor-peers.test.ts +0 -250
  12. package/src/editor/editor-provider.test.tsx +0 -226
  13. package/src/editor/markdown-document-editor.test.tsx +0 -126
  14. package/src/editor/tiptap-editor.test.tsx +0 -228
  15. package/src/files/file-artifact-pane.test.tsx +0 -301
  16. package/src/files/file-card.test.tsx +0 -62
  17. package/src/files/file-list.test.tsx +0 -81
  18. package/src/files/pdf-loader.test.ts +0 -130
  19. package/src/files/pdf-viewer.test.tsx +0 -245
  20. package/src/files/rich-file-tree.test.tsx +0 -90
  21. package/src/markdown/api-presentation.test.tsx +0 -63
  22. package/src/markdown/code-block.test.tsx +0 -201
  23. package/src/markdown/markdown-table.test.tsx +0 -76
  24. package/src/markdown/markdown.test.tsx +0 -166
  25. package/src/openui/schema.test.tsx +0 -158
  26. package/src/primitives/button.test.tsx +0 -230
  27. package/src/primitives/card.test.tsx +0 -71
  28. package/src/primitives/chip.test.tsx +0 -200
  29. package/src/primitives/date-picker.test.tsx +0 -146
  30. package/src/primitives/dialog.test.tsx +0 -75
  31. package/src/primitives/input.test.tsx +0 -161
  32. package/src/primitives/interaction-states.test.tsx +0 -287
  33. package/src/primitives/pagination-alert.test.tsx +0 -180
  34. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +0 -165
  35. package/src/primitives/radio-group.test.tsx +0 -26
  36. package/src/primitives/segmented-control.test.tsx +0 -451
  37. package/src/primitives/table.test.tsx +0 -72
  38. package/src/primitives/theme-toggle.test.tsx +0 -168
  39. package/src/primitives/toast.test.tsx +0 -130
  40. package/src/primitives/wizard.test.tsx +0 -106
  41. 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
- });