@assistant-ui/react 0.15.4 → 0.15.5
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/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +383 -368
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/context/providers/MessageProvider.js +15 -34
- package/dist/context/providers/MessageProvider.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts +6 -1
- package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts.map +1 -1
- package/dist/legacy-runtime/AssistantRuntimeProvider.js +10 -8
- package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +8 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +5 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +44 -3
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
- package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentThumb.js +17 -14
- package/dist/primitives/attachment/AttachmentThumb.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -0
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
- package/dist/primitives/queueItem/QueueItemText.js +10 -4
- package/dist/primitives/queueItem/QueueItemText.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -4
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/utils/createActionButton.js +1 -1
- package/dist/utils/createActionButton.js.map +1 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
- package/package.json +6 -6
- package/src/client/ExternalThread.ts +28 -16
- package/src/context/providers/MessageProvider.tsx +8 -6
- package/src/index.ts +5 -1
- package/src/legacy-runtime/AssistantRuntimeProvider.tsx +13 -3
- package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +14 -1
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +38 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +5 -1
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +7 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +290 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +62 -1
- package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
- package/src/primitives/attachment/AttachmentThumb.test.tsx +70 -0
- package/src/primitives/attachment/AttachmentThumb.tsx +8 -4
- package/src/primitives/composer/ComposerInput.test.tsx +53 -2
- package/src/primitives/composer/ComposerInput.tsx +3 -0
- package/src/primitives/queueItem/QueueItemText.tsx +8 -2
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +74 -0
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +3 -3
- package/src/tests/external-thread-parity.test.tsx +9 -4
- package/src/tests/local-runtime-queue.test.tsx +199 -6
- package/src/utils/createActionButton.test.tsx +18 -0
- package/src/utils/createActionButton.tsx +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import type { MouseEvent } from "react";
|
|
3
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type * as GetSelectionMessageIdModule from "../../utils/getSelectionMessageId";
|
|
6
|
+
import { SelectionToolbarPrimitiveRoot } from "./SelectionToolbarRoot";
|
|
7
|
+
|
|
8
|
+
vi.mock("../../utils/getSelectionMessageId", async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal<typeof GetSelectionMessageIdModule>()),
|
|
10
|
+
getSelectionMessageId: () => "m1",
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const fakeSelection = {
|
|
14
|
+
isCollapsed: false,
|
|
15
|
+
toString: () => "selected text",
|
|
16
|
+
getRangeAt: () => ({
|
|
17
|
+
getBoundingClientRect: () => ({ top: 100, left: 50, width: 20 }) as DOMRect,
|
|
18
|
+
}),
|
|
19
|
+
} as unknown as Selection;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
|
|
23
|
+
cb(0);
|
|
24
|
+
return 0;
|
|
25
|
+
});
|
|
26
|
+
vi.spyOn(window, "getSelection").mockReturnValue(fakeSelection);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const setupToolbar = (
|
|
34
|
+
onMouseDown?: (e: MouseEvent<HTMLDivElement>) => void,
|
|
35
|
+
) => {
|
|
36
|
+
render(
|
|
37
|
+
<SelectionToolbarPrimitiveRoot
|
|
38
|
+
data-testid="toolbar"
|
|
39
|
+
onMouseDown={onMouseDown}
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
fireEvent.mouseUp(document);
|
|
43
|
+
const toolbar = document.querySelector('[data-testid="toolbar"]');
|
|
44
|
+
expect(toolbar).not.toBeNull();
|
|
45
|
+
return toolbar as HTMLElement;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("SelectionToolbarPrimitiveRoot onMouseDown composition", () => {
|
|
49
|
+
it("runs the consumer handler on an un-prevented event before preventing default", () => {
|
|
50
|
+
let observedDefaultPrevented: boolean | undefined;
|
|
51
|
+
const onMouseDown = vi.fn((event: MouseEvent<HTMLDivElement>) => {
|
|
52
|
+
observedDefaultPrevented = event.defaultPrevented;
|
|
53
|
+
});
|
|
54
|
+
const toolbar = setupToolbar(onMouseDown);
|
|
55
|
+
|
|
56
|
+
const notPrevented = fireEvent.mouseDown(toolbar);
|
|
57
|
+
|
|
58
|
+
expect(onMouseDown).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(observedDefaultPrevented).toBe(false);
|
|
60
|
+
expect(notPrevented).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("keeps the event prevented when the consumer prevents default", () => {
|
|
64
|
+
const onMouseDown = vi.fn((event: MouseEvent<HTMLDivElement>) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
});
|
|
67
|
+
const toolbar = setupToolbar(onMouseDown);
|
|
68
|
+
|
|
69
|
+
const notPrevented = fireEvent.mouseDown(toolbar);
|
|
70
|
+
|
|
71
|
+
expect(onMouseDown).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(notPrevented).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Primitive } from "../../utils/Primitive";
|
|
4
|
+
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
4
5
|
import {
|
|
5
6
|
type ComponentPropsWithoutRef,
|
|
6
7
|
type ComponentRef,
|
|
@@ -117,11 +118,10 @@ export const SelectionToolbarPrimitiveRoot = forwardRef<
|
|
|
117
118
|
{...props}
|
|
118
119
|
ref={forwardedRef}
|
|
119
120
|
style={positionStyle}
|
|
120
|
-
onMouseDown={(e) => {
|
|
121
|
+
onMouseDown={composeEventHandlers(onMouseDown, (e) => {
|
|
121
122
|
// Prevent mousedown from clearing the text selection
|
|
122
123
|
e.preventDefault();
|
|
123
|
-
|
|
124
|
-
}}
|
|
124
|
+
})}
|
|
125
125
|
/>
|
|
126
126
|
</SelectionToolbarContext.Provider>,
|
|
127
127
|
document.body,
|
|
@@ -246,6 +246,7 @@ describe("ExternalThread composer", () => {
|
|
|
246
246
|
|
|
247
247
|
it("stamps the thread head as parentId on queue-adapter sends", async () => {
|
|
248
248
|
const enqueue = vi.fn();
|
|
249
|
+
const steer = vi.fn();
|
|
249
250
|
const { aui } = renderThread({
|
|
250
251
|
messages: [
|
|
251
252
|
{
|
|
@@ -260,18 +261,22 @@ describe("ExternalThread composer", () => {
|
|
|
260
261
|
isRunning: true,
|
|
261
262
|
queue: {
|
|
262
263
|
items: [],
|
|
264
|
+
steerItems: [],
|
|
263
265
|
enqueue,
|
|
264
|
-
steer
|
|
266
|
+
steer,
|
|
267
|
+
move: vi.fn(),
|
|
268
|
+
edit: vi.fn(),
|
|
265
269
|
remove: vi.fn(),
|
|
266
|
-
clear: vi.fn(),
|
|
267
270
|
},
|
|
268
271
|
});
|
|
269
272
|
|
|
270
273
|
aui().thread.composer().setText("queued");
|
|
271
274
|
aui().thread.composer().send();
|
|
272
275
|
|
|
273
|
-
|
|
274
|
-
expect(
|
|
276
|
+
// mid-run sends default to the steer lane
|
|
277
|
+
await waitFor(() => expect(steer).toHaveBeenCalledTimes(1));
|
|
278
|
+
expect(steer.mock.calls[0]![0].parentId).toBe("u1");
|
|
279
|
+
expect(enqueue).not.toHaveBeenCalled();
|
|
275
280
|
});
|
|
276
281
|
|
|
277
282
|
it("still refuses to send an empty composer synchronously after a send", async () => {
|
|
@@ -34,7 +34,14 @@ const userTexts = (aui: ReturnType<typeof useAui>) =>
|
|
|
34
34
|
m.content.map((p) => (p.type === "text" ? p.text : "")).join(""),
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
-
const renderWithRuntime = (
|
|
37
|
+
const renderWithRuntime = (
|
|
38
|
+
adapter: ChatModelAdapter,
|
|
39
|
+
enableQueue: boolean,
|
|
40
|
+
options?: {
|
|
41
|
+
unstable_queueClearOnRewind?: boolean;
|
|
42
|
+
unstable_queueClearOnCancel?: boolean;
|
|
43
|
+
},
|
|
44
|
+
) => {
|
|
38
45
|
const captured: { aui?: ReturnType<typeof useAui> } = {};
|
|
39
46
|
const Capture: FC = () => {
|
|
40
47
|
captured.aui = useAui();
|
|
@@ -43,6 +50,7 @@ const renderWithRuntime = (adapter: ChatModelAdapter, enableQueue: boolean) => {
|
|
|
43
50
|
const App: FC = () => {
|
|
44
51
|
const runtime = useLocalRuntime(adapter, {
|
|
45
52
|
unstable_enableMessageQueue: enableQueue,
|
|
53
|
+
...options,
|
|
46
54
|
});
|
|
47
55
|
return (
|
|
48
56
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
@@ -114,6 +122,43 @@ describe("local runtime message queue", () => {
|
|
|
114
122
|
).toEqual(["b"]);
|
|
115
123
|
});
|
|
116
124
|
|
|
125
|
+
it("defaults a mid-run send to steer, ahead of an explicitly queued item", async () => {
|
|
126
|
+
const { adapter, releases, getRunCount } = createCountingAdapter();
|
|
127
|
+
const aui = renderWithRuntime(adapter, true);
|
|
128
|
+
|
|
129
|
+
await send(aui, "first");
|
|
130
|
+
expect(getRunCount()).toBe(1);
|
|
131
|
+
|
|
132
|
+
await act(async () => {
|
|
133
|
+
aui.thread.composer().setText("behind");
|
|
134
|
+
aui.thread.composer().send({ steer: false });
|
|
135
|
+
await flush();
|
|
136
|
+
});
|
|
137
|
+
await send(aui, "next");
|
|
138
|
+
expect(
|
|
139
|
+
aui.thread
|
|
140
|
+
.composer()
|
|
141
|
+
.getState()
|
|
142
|
+
.queue.map((q) => q.prompt),
|
|
143
|
+
).toEqual(["next", "behind"]);
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
releases[0]!();
|
|
147
|
+
await flush();
|
|
148
|
+
await flush();
|
|
149
|
+
});
|
|
150
|
+
expect(getRunCount()).toBe(2);
|
|
151
|
+
expect(userTexts(aui)).toEqual(["first", "next"]);
|
|
152
|
+
|
|
153
|
+
await act(async () => {
|
|
154
|
+
releases[1]!();
|
|
155
|
+
await flush();
|
|
156
|
+
await flush();
|
|
157
|
+
});
|
|
158
|
+
expect(getRunCount()).toBe(3);
|
|
159
|
+
expect(userTexts(aui)).toEqual(["first", "next", "behind"]);
|
|
160
|
+
});
|
|
161
|
+
|
|
117
162
|
it("queueItem(index).remove() drops a queued message", async () => {
|
|
118
163
|
const { adapter } = createCountingAdapter();
|
|
119
164
|
const aui = renderWithRuntime(adapter, true);
|
|
@@ -132,7 +177,7 @@ describe("local runtime message queue", () => {
|
|
|
132
177
|
expect(queue[0]!.prompt).toBe("b");
|
|
133
178
|
});
|
|
134
179
|
|
|
135
|
-
it("clears
|
|
180
|
+
it("clears queued items when the user cancels the run", async () => {
|
|
136
181
|
const { adapter, getRunCount } = createCountingAdapter();
|
|
137
182
|
const aui = renderWithRuntime(adapter, true);
|
|
138
183
|
|
|
@@ -147,13 +192,50 @@ describe("local runtime message queue", () => {
|
|
|
147
192
|
await flush();
|
|
148
193
|
});
|
|
149
194
|
|
|
195
|
+
// Stop means stop: nothing pending, nothing dispatched
|
|
196
|
+
expect(getRunCount()).toBe(1);
|
|
150
197
|
expect(aui.thread.composer().getState().queue).toEqual([]);
|
|
151
|
-
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("keeps queued items on cancel when unstable_queueClearOnCancel is false", async () => {
|
|
201
|
+
const { adapter, getRunCount } = createCountingAdapter();
|
|
202
|
+
const aui = renderWithRuntime(adapter, true, {
|
|
203
|
+
unstable_queueClearOnCancel: false,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await send(aui, "first");
|
|
207
|
+
await send(aui, "a");
|
|
208
|
+
await send(aui, "b");
|
|
209
|
+
expect(aui.thread.composer().getState().queue).toHaveLength(2);
|
|
210
|
+
|
|
211
|
+
await act(async () => {
|
|
212
|
+
aui.thread.cancelRun();
|
|
213
|
+
await flush();
|
|
214
|
+
await flush();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// cancel pauses the queue: items survive, nothing auto-dispatches
|
|
152
218
|
expect(getRunCount()).toBe(1);
|
|
219
|
+
expect(
|
|
220
|
+
aui.thread
|
|
221
|
+
.composer()
|
|
222
|
+
.getState()
|
|
223
|
+
.queue.map((q) => q.prompt),
|
|
224
|
+
).toEqual(["a", "b"]);
|
|
225
|
+
|
|
226
|
+
// the next explicit send drains the head
|
|
227
|
+
await send(aui, "c");
|
|
228
|
+
expect(getRunCount()).toBe(2);
|
|
229
|
+
expect(
|
|
230
|
+
aui.thread
|
|
231
|
+
.composer()
|
|
232
|
+
.getState()
|
|
233
|
+
.queue.map((q) => q.prompt),
|
|
234
|
+
).toEqual(["b", "c"]);
|
|
153
235
|
});
|
|
154
236
|
|
|
155
|
-
it("applies an edit instead of queuing it
|
|
156
|
-
const { adapter, releases } = createCountingAdapter();
|
|
237
|
+
it("applies an edit instead of queuing it and clears the pending items", async () => {
|
|
238
|
+
const { adapter, releases, getRunCount } = createCountingAdapter();
|
|
157
239
|
const aui = renderWithRuntime(adapter, true);
|
|
158
240
|
|
|
159
241
|
await send(aui, "first");
|
|
@@ -174,10 +256,52 @@ describe("local runtime message queue", () => {
|
|
|
174
256
|
message.composer().setText("edited");
|
|
175
257
|
message.composer().send();
|
|
176
258
|
await flush();
|
|
259
|
+
await flush();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// the edit is applied (branches the thread); the rewind clears the queue,
|
|
263
|
+
// so nothing pending dispatches against the new branch
|
|
264
|
+
expect(aui.thread.composer().getState().queue).toEqual([]);
|
|
265
|
+
expect(getRunCount()).toBe(3); // first, second, edit — not "queued"
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps the queue across an edit when unstable_queueClearOnRewind is false", async () => {
|
|
269
|
+
const { adapter, releases, getRunCount } = createCountingAdapter();
|
|
270
|
+
const aui = renderWithRuntime(adapter, true, {
|
|
271
|
+
unstable_queueClearOnRewind: false,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await send(aui, "first");
|
|
275
|
+
await act(async () => {
|
|
276
|
+
releases[0]!();
|
|
277
|
+
await flush();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await send(aui, "second");
|
|
281
|
+
await send(aui, "queued");
|
|
282
|
+
expect(aui.thread.composer().getState().queue).toHaveLength(1);
|
|
283
|
+
|
|
284
|
+
await act(async () => {
|
|
285
|
+
const message = aui.thread.message({ index: 0 });
|
|
286
|
+
message.composer().beginEdit();
|
|
287
|
+
message.composer().setText("edited");
|
|
288
|
+
message.composer().send();
|
|
289
|
+
await flush();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// the edit's rerun survives; the queued item waits for it to settle
|
|
293
|
+
expect(getRunCount()).toBe(3);
|
|
294
|
+
expect(aui.thread.composer().getState().queue).toHaveLength(1);
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
releases[2]!();
|
|
298
|
+
await flush();
|
|
177
299
|
});
|
|
178
300
|
|
|
179
|
-
// the
|
|
301
|
+
// the surviving queue drains after the edit's run completes
|
|
302
|
+
expect(getRunCount()).toBe(4);
|
|
180
303
|
expect(aui.thread.composer().getState().queue).toEqual([]);
|
|
304
|
+
expect(userTexts(aui)).toEqual(["edited", "queued"]);
|
|
181
305
|
});
|
|
182
306
|
|
|
183
307
|
it("buffers a send during a regenerate instead of interrupting it", async () => {
|
|
@@ -214,6 +338,75 @@ describe("local runtime message queue", () => {
|
|
|
214
338
|
).toEqual(["Y"]);
|
|
215
339
|
});
|
|
216
340
|
|
|
341
|
+
it("drains the pending head after a regenerate started inside the cancellation window", async () => {
|
|
342
|
+
const { adapter, releases, getRunCount } = createCountingAdapter();
|
|
343
|
+
const aui = renderWithRuntime(adapter, true, {
|
|
344
|
+
unstable_queueClearOnCancel: false,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await send(aui, "first");
|
|
348
|
+
await act(async () => {
|
|
349
|
+
releases[0]!();
|
|
350
|
+
await flush();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// regenerate, buffer a send, then cancel and regenerate again before
|
|
354
|
+
// the cancelled settle lands
|
|
355
|
+
await act(async () => {
|
|
356
|
+
aui.thread.message({ index: 1 }).reload();
|
|
357
|
+
await flush();
|
|
358
|
+
});
|
|
359
|
+
await send(aui, "pending");
|
|
360
|
+
await act(async () => {
|
|
361
|
+
aui.thread.cancelRun();
|
|
362
|
+
aui.thread.message({ index: 1 }).reload();
|
|
363
|
+
await flush();
|
|
364
|
+
});
|
|
365
|
+
expect(getRunCount()).toBe(3);
|
|
366
|
+
expect(aui.thread.composer().getState().queue).toHaveLength(1);
|
|
367
|
+
|
|
368
|
+
// the cancelled settle must not eat the replacement's: once the
|
|
369
|
+
// replacement settles, the pending head drains exactly once
|
|
370
|
+
await act(async () => {
|
|
371
|
+
releases[2]!();
|
|
372
|
+
await flush();
|
|
373
|
+
});
|
|
374
|
+
expect(getRunCount()).toBe(4);
|
|
375
|
+
expect(aui.thread.composer().getState().queue).toEqual([]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("keeps a second regenerate alive when the first one's settle arrives", async () => {
|
|
379
|
+
const { adapter, releases, getRunCount } = createCountingAdapter();
|
|
380
|
+
const aui = renderWithRuntime(adapter, true);
|
|
381
|
+
|
|
382
|
+
await send(aui, "first");
|
|
383
|
+
await act(async () => {
|
|
384
|
+
releases[0]!();
|
|
385
|
+
await flush();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await act(async () => {
|
|
389
|
+
aui.thread.message({ index: 1 }).reload();
|
|
390
|
+
await flush();
|
|
391
|
+
});
|
|
392
|
+
await send(aui, "Y");
|
|
393
|
+
|
|
394
|
+
// the second regenerate aborts the first; its settle must not dispatch
|
|
395
|
+
await act(async () => {
|
|
396
|
+
aui.thread.message({ index: 1 }).reload();
|
|
397
|
+
await flush();
|
|
398
|
+
});
|
|
399
|
+
expect(getRunCount()).toBe(3);
|
|
400
|
+
expect(aui.thread.composer().getState().queue).toHaveLength(1);
|
|
401
|
+
|
|
402
|
+
await act(async () => {
|
|
403
|
+
releases[2]!();
|
|
404
|
+
await flush();
|
|
405
|
+
});
|
|
406
|
+
expect(getRunCount()).toBe(4);
|
|
407
|
+
expect(aui.thread.composer().getState().queue).toEqual([]);
|
|
408
|
+
});
|
|
409
|
+
|
|
217
410
|
it("advances exactly once after a failed run, without deadlocking", async () => {
|
|
218
411
|
const releases: Array<() => void> = [];
|
|
219
412
|
let runCount = 0;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { createActionButton } from "./createActionButton";
|
|
4
|
+
|
|
5
|
+
const TestButton = createActionButton("TestButton", () => () => {});
|
|
6
|
+
|
|
7
|
+
describe("createActionButton", () => {
|
|
8
|
+
it("defaults to type=button", () => {
|
|
9
|
+
const html = renderToStaticMarkup(<TestButton />);
|
|
10
|
+
expect(html).toContain('type="button"');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("allows the caller to override type", () => {
|
|
14
|
+
const html = renderToStaticMarkup(<TestButton type="submit" />);
|
|
15
|
+
expect(html).toContain('type="submit"');
|
|
16
|
+
expect(html).not.toContain('type="button"');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -41,8 +41,8 @@ export const createActionButton = <TProps,>(
|
|
|
41
41
|
const callback = useActionButton(forwardedProps as TProps) ?? undefined;
|
|
42
42
|
return (
|
|
43
43
|
<Primitive.button
|
|
44
|
-
{...primitiveProps}
|
|
45
44
|
type="button"
|
|
45
|
+
{...primitiveProps}
|
|
46
46
|
ref={forwardedRef}
|
|
47
47
|
disabled={primitiveProps.disabled || !callback}
|
|
48
48
|
onClick={composeEventHandlers(primitiveProps.onClick, callback)}
|