@assistant-ui/react-native 0.1.27 → 0.1.29
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/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +8 -0
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/thread.d.ts +3 -3
- package/dist/primitives/thread.js +4 -3
- package/dist/primitives/thread.js.map +1 -1
- package/package.json +11 -7
- package/src/index.ts +32 -0
- package/src/primitives/attachment/AttachmentName.test.tsx +55 -0
- package/src/primitives/attachment/AttachmentRemove.test.tsx +81 -0
- package/src/primitives/attachment/AttachmentThumb.test.tsx +75 -0
- package/src/primitives/branchPicker/BranchPickerCount.test.tsx +55 -0
- package/src/primitives/branchPicker/BranchPickerNext.test.tsx +97 -0
- package/src/primitives/branchPicker/BranchPickerNumber.test.tsx +56 -0
- package/src/primitives/branchPicker/BranchPickerPrevious.test.tsx +100 -0
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.test.tsx +85 -0
- package/src/primitives/composer/ComposerAddAttachment.test.tsx +136 -0
- package/src/primitives/composer/ComposerCancel.test.tsx +116 -0
- package/src/primitives/composer/ComposerInput.test.tsx +253 -0
- package/src/primitives/composer/ComposerInput.tsx +12 -0
- package/src/primitives/composer/ComposerSend.test.tsx +116 -0
- package/src/primitives/error/ErrorMessage.test.tsx +64 -0
- package/src/primitives/error/ErrorRoot.test.tsx +65 -0
- package/src/primitives/message/MessageContent.test.tsx +278 -0
- package/src/primitives/message/MessageIf.test.tsx +156 -0
- package/src/primitives/suggestion/SuggestionDescription.test.tsx +61 -0
- package/src/primitives/suggestion/SuggestionTitle.test.tsx +61 -0
- package/src/primitives/suggestion/SuggestionTrigger.test.tsx +144 -0
- package/src/primitives/thread/ThreadEmpty.test.tsx +56 -0
- package/src/primitives/thread/ThreadIf.test.tsx +121 -0
- package/src/primitives/thread/ThreadMessages.test.tsx +192 -0
- package/src/primitives/thread.ts +1 -0
- package/src/primitives/threadList/ThreadListNew.test.tsx +72 -0
- package/src/primitives/threadListItem/ThreadListItemArchive.test.tsx +72 -0
- package/src/primitives/threadListItem/ThreadListItemDelete.test.tsx +72 -0
- package/src/primitives/threadListItem/ThreadListItemTrigger.test.tsx +72 -0
- package/src/primitives/threadListItem/ThreadListItemUnarchive.test.tsx +72 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { Text } from "react-native";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { ThreadEmpty } from "./ThreadEmpty";
|
|
6
|
+
|
|
7
|
+
const h = vi.hoisted(() => ({
|
|
8
|
+
isEmpty: true,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@assistant-ui/core/react", () => ({
|
|
12
|
+
useThreadIsEmpty: () => h.isEmpty,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
|
+
|
|
17
|
+
describe("ThreadEmpty", () => {
|
|
18
|
+
let container: HTMLDivElement;
|
|
19
|
+
let root: Root;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
h.isEmpty = true;
|
|
23
|
+
|
|
24
|
+
container = document.createElement("div");
|
|
25
|
+
document.body.appendChild(container);
|
|
26
|
+
root = createRoot(container);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await act(async () => {
|
|
31
|
+
root.unmount();
|
|
32
|
+
});
|
|
33
|
+
container.remove();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mount = async () => {
|
|
37
|
+
await act(async () => {
|
|
38
|
+
root.render(
|
|
39
|
+
<ThreadEmpty>
|
|
40
|
+
<Text testID="child">visible</Text>
|
|
41
|
+
</ThreadEmpty>,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
return container.querySelector('[data-testid="child"]');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
it("renders children when the thread is empty", async () => {
|
|
48
|
+
h.isEmpty = true;
|
|
49
|
+
expect(await mount()).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("hides children when the thread is not empty", async () => {
|
|
53
|
+
h.isEmpty = false;
|
|
54
|
+
expect(await mount()).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { Text } from "react-native";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { ThreadIf } from "./ThreadIf";
|
|
6
|
+
|
|
7
|
+
const h = vi.hoisted(() => ({
|
|
8
|
+
thread: { messages: [] as unknown[], isRunning: false },
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@assistant-ui/store", () => ({
|
|
12
|
+
useAuiState: <T,>(selector: (s: { thread: typeof h.thread }) => T) =>
|
|
13
|
+
selector({ thread: h.thread }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
17
|
+
|
|
18
|
+
describe("ThreadIf", () => {
|
|
19
|
+
let container: HTMLDivElement;
|
|
20
|
+
let root: Root;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
h.thread.messages = [];
|
|
24
|
+
h.thread.isRunning = false;
|
|
25
|
+
|
|
26
|
+
container = document.createElement("div");
|
|
27
|
+
document.body.appendChild(container);
|
|
28
|
+
root = createRoot(container);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await act(async () => {
|
|
33
|
+
root.unmount();
|
|
34
|
+
});
|
|
35
|
+
container.remove();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const mount = async (props: Partial<Parameters<typeof ThreadIf>[0]> = {}) => {
|
|
39
|
+
await act(async () => {
|
|
40
|
+
root.render(
|
|
41
|
+
<ThreadIf {...props}>
|
|
42
|
+
<Text testID="child">visible</Text>
|
|
43
|
+
</ThreadIf>,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
return container.querySelector('[data-testid="child"]');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const setMessages = (count: number) => {
|
|
50
|
+
h.thread.messages = Array.from({ length: count }, (_, i) => i);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("renders children when no guard is set", async () => {
|
|
54
|
+
setMessages(2);
|
|
55
|
+
expect(await mount()).not.toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("empty guard", () => {
|
|
59
|
+
it("renders children when empty:true matches an empty thread", async () => {
|
|
60
|
+
setMessages(0);
|
|
61
|
+
expect(await mount({ empty: true })).not.toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("hides children when empty:true but the thread has messages", async () => {
|
|
65
|
+
setMessages(3);
|
|
66
|
+
expect(await mount({ empty: true })).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders children when empty:false matches a non-empty thread", async () => {
|
|
70
|
+
setMessages(3);
|
|
71
|
+
expect(await mount({ empty: false })).not.toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("hides children when empty:false but the thread is empty", async () => {
|
|
75
|
+
setMessages(0);
|
|
76
|
+
expect(await mount({ empty: false })).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("running guard", () => {
|
|
81
|
+
it("renders children when running:true matches a running thread", async () => {
|
|
82
|
+
h.thread.isRunning = true;
|
|
83
|
+
expect(await mount({ running: true })).not.toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("hides children when running:true but the thread is idle", async () => {
|
|
87
|
+
h.thread.isRunning = false;
|
|
88
|
+
expect(await mount({ running: true })).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("renders children when running:false matches an idle thread", async () => {
|
|
92
|
+
h.thread.isRunning = false;
|
|
93
|
+
expect(await mount({ running: false })).not.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("hides children when running:false but the thread is running", async () => {
|
|
97
|
+
h.thread.isRunning = true;
|
|
98
|
+
expect(await mount({ running: false })).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("combined guards", () => {
|
|
103
|
+
it("renders children only when both empty and running match", async () => {
|
|
104
|
+
setMessages(0);
|
|
105
|
+
h.thread.isRunning = true;
|
|
106
|
+
expect(await mount({ empty: true, running: true })).not.toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("hides children when empty matches but running does not", async () => {
|
|
110
|
+
setMessages(0);
|
|
111
|
+
h.thread.isRunning = false;
|
|
112
|
+
expect(await mount({ empty: true, running: true })).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("hides children when running matches but empty does not", async () => {
|
|
116
|
+
setMessages(2);
|
|
117
|
+
h.thread.isRunning = true;
|
|
118
|
+
expect(await mount({ empty: true, running: true })).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ThreadMessages } from "./ThreadMessages";
|
|
5
|
+
|
|
6
|
+
type Msg = { id: string; role: string };
|
|
7
|
+
|
|
8
|
+
const h = vi.hoisted(() => ({
|
|
9
|
+
state: {
|
|
10
|
+
thread: { messages: [] as Msg[] },
|
|
11
|
+
message: { role: "user" as string, composer: { isEditing: false } },
|
|
12
|
+
},
|
|
13
|
+
itemState: { role: "user" } as { role: string },
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@assistant-ui/store", () => ({
|
|
17
|
+
useAuiState: <T,>(selector: (s: typeof h.state) => T) => selector(h.state),
|
|
18
|
+
RenderChildrenWithAccessor: ({
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
children: (getItem: () => unknown) => unknown;
|
|
22
|
+
}) => children(() => h.itemState),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@assistant-ui/core/react", () => ({
|
|
26
|
+
MessageByIndexProvider: ({ children }: { children: unknown }) => children,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
30
|
+
|
|
31
|
+
describe("ThreadMessages", () => {
|
|
32
|
+
let container: HTMLDivElement;
|
|
33
|
+
let root: Root;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
h.state.thread.messages = [];
|
|
37
|
+
h.state.message.role = "user";
|
|
38
|
+
h.state.message.composer.isEditing = false;
|
|
39
|
+
h.itemState = { role: "user" };
|
|
40
|
+
|
|
41
|
+
container = document.createElement("div");
|
|
42
|
+
document.body.appendChild(container);
|
|
43
|
+
root = createRoot(container);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
await act(async () => {
|
|
48
|
+
root.unmount();
|
|
49
|
+
});
|
|
50
|
+
container.remove();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const mount = async (props: Parameters<typeof ThreadMessages>[0]) => {
|
|
54
|
+
await act(async () => {
|
|
55
|
+
root.render(<ThreadMessages {...props} />);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe("components mode dispatch", () => {
|
|
60
|
+
const makeComponents = () => ({
|
|
61
|
+
Message: vi.fn(() => <span data-testid="c-message">message</span>),
|
|
62
|
+
EditComposer: vi.fn(() => <span data-testid="c-edit">edit</span>),
|
|
63
|
+
UserEditComposer: vi.fn(() => (
|
|
64
|
+
<span data-testid="c-user-edit">user-edit</span>
|
|
65
|
+
)),
|
|
66
|
+
AssistantEditComposer: vi.fn(() => (
|
|
67
|
+
<span data-testid="c-assistant-edit">assistant-edit</span>
|
|
68
|
+
)),
|
|
69
|
+
SystemEditComposer: vi.fn(() => (
|
|
70
|
+
<span data-testid="c-system-edit">system-edit</span>
|
|
71
|
+
)),
|
|
72
|
+
UserMessage: vi.fn(() => <span data-testid="c-user">user</span>),
|
|
73
|
+
AssistantMessage: vi.fn(() => (
|
|
74
|
+
<span data-testid="c-assistant">assistant</span>
|
|
75
|
+
)),
|
|
76
|
+
SystemMessage: vi.fn(() => <span data-testid="c-system">system</span>),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders the user message component for a user role", async () => {
|
|
80
|
+
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
81
|
+
h.state.message.role = "user";
|
|
82
|
+
await mount({ components: makeComponents() });
|
|
83
|
+
expect(container.querySelector('[data-testid="c-user"]')).not.toBeNull();
|
|
84
|
+
expect(container.querySelector('[data-testid="c-message"]')).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("renders the assistant message component for an assistant role", async () => {
|
|
88
|
+
h.state.thread.messages = [{ id: "1", role: "assistant" }];
|
|
89
|
+
h.state.message.role = "assistant";
|
|
90
|
+
await mount({ components: makeComponents() });
|
|
91
|
+
expect(
|
|
92
|
+
container.querySelector('[data-testid="c-assistant"]'),
|
|
93
|
+
).not.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("renders the system message component for a system role", async () => {
|
|
97
|
+
h.state.thread.messages = [{ id: "1", role: "system" }];
|
|
98
|
+
h.state.message.role = "system";
|
|
99
|
+
await mount({ components: makeComponents() });
|
|
100
|
+
expect(
|
|
101
|
+
container.querySelector('[data-testid="c-system"]'),
|
|
102
|
+
).not.toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("prefers the role-specific edit composer while editing", async () => {
|
|
106
|
+
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
107
|
+
h.state.message.role = "user";
|
|
108
|
+
h.state.message.composer.isEditing = true;
|
|
109
|
+
await mount({ components: makeComponents() });
|
|
110
|
+
expect(
|
|
111
|
+
container.querySelector('[data-testid="c-user-edit"]'),
|
|
112
|
+
).not.toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("falls back to the shared EditComposer when no role edit composer exists", async () => {
|
|
116
|
+
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
117
|
+
h.state.message.role = "user";
|
|
118
|
+
h.state.message.composer.isEditing = true;
|
|
119
|
+
const components = makeComponents() as Record<string, unknown>;
|
|
120
|
+
delete components.UserEditComposer;
|
|
121
|
+
await mount({ components: components as never });
|
|
122
|
+
expect(container.querySelector('[data-testid="c-edit"]')).not.toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("falls back to the role message then Message when no edit composer exists", async () => {
|
|
126
|
+
h.state.thread.messages = [{ id: "1", role: "assistant" }];
|
|
127
|
+
h.state.message.role = "assistant";
|
|
128
|
+
h.state.message.composer.isEditing = true;
|
|
129
|
+
const components = makeComponents() as Record<string, unknown>;
|
|
130
|
+
delete components.AssistantEditComposer;
|
|
131
|
+
delete components.EditComposer;
|
|
132
|
+
await mount({ components: components as never });
|
|
133
|
+
expect(
|
|
134
|
+
container.querySelector('[data-testid="c-assistant"]'),
|
|
135
|
+
).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("falls back to Message when no role-specific component exists", async () => {
|
|
139
|
+
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
140
|
+
h.state.message.role = "user";
|
|
141
|
+
const components = makeComponents() as Record<string, unknown>;
|
|
142
|
+
delete components.UserMessage;
|
|
143
|
+
await mount({ components: components as never });
|
|
144
|
+
expect(
|
|
145
|
+
container.querySelector('[data-testid="c-message"]'),
|
|
146
|
+
).not.toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("renders nothing for a system role with no system or Message component", async () => {
|
|
150
|
+
h.state.thread.messages = [{ id: "1", role: "system" }];
|
|
151
|
+
h.state.message.role = "system";
|
|
152
|
+
const components = makeComponents() as Record<string, unknown>;
|
|
153
|
+
delete components.SystemMessage;
|
|
154
|
+
delete components.Message;
|
|
155
|
+
await mount({ components: components as never });
|
|
156
|
+
expect(container.querySelector('[data-testid="c-system"]')).toBeNull();
|
|
157
|
+
expect(container.querySelector('[data-testid="c-message"]')).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("throws for an unknown role", async () => {
|
|
161
|
+
h.state.thread.messages = [{ id: "1", role: "ghost" }];
|
|
162
|
+
h.state.message.role = "ghost";
|
|
163
|
+
await expect(mount({ components: makeComponents() })).rejects.toThrow(
|
|
164
|
+
/Unknown message role/,
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("children mode", () => {
|
|
170
|
+
it("renders via the children render prop", async () => {
|
|
171
|
+
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
172
|
+
h.itemState = { role: "user" };
|
|
173
|
+
const children = vi.fn(({ message }: { message: { role: string } }) => (
|
|
174
|
+
<span data-testid="child">child:{message.role}</span>
|
|
175
|
+
));
|
|
176
|
+
await mount({ children });
|
|
177
|
+
const el = container.querySelector('[data-testid="child"]');
|
|
178
|
+
expect(el?.textContent).toBe("child:user");
|
|
179
|
+
expect(children).toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("renders no items for an empty thread", async () => {
|
|
184
|
+
h.state.thread.messages = [];
|
|
185
|
+
await mount({
|
|
186
|
+
components: {
|
|
187
|
+
Message: () => <span data-testid="c-message">message</span>,
|
|
188
|
+
} as never,
|
|
189
|
+
});
|
|
190
|
+
expect(container.querySelector('[data-testid="c-message"]')).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
package/src/primitives/thread.ts
CHANGED
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
type ThreadMessagesProps as MessagesProps,
|
|
8
8
|
} from "./thread/ThreadMessages";
|
|
9
9
|
export { ThreadPrimitiveMessageByIndex as MessageByIndex } from "@assistant-ui/core/react";
|
|
10
|
+
export { ThreadPrimitiveUnstable_MessageById as Unstable_MessageById } from "@assistant-ui/core/react";
|
|
10
11
|
export {
|
|
11
12
|
ThreadEmpty as Empty,
|
|
12
13
|
type ThreadEmptyProps as EmptyProps,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ThreadListNew } from "./ThreadListNew";
|
|
5
|
+
|
|
6
|
+
const h = vi.hoisted(() => ({
|
|
7
|
+
switchToNewThread: vi.fn<() => void>(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@assistant-ui/core/react", () => ({
|
|
11
|
+
useThreadListNew: () => ({ switchToNewThread: h.switchToNewThread }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
const click = (el: Element) =>
|
|
17
|
+
el.dispatchEvent(
|
|
18
|
+
new MouseEvent("click", { bubbles: true, cancelable: true }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("ThreadListNew", () => {
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
h.switchToNewThread.mockReset();
|
|
27
|
+
|
|
28
|
+
container = document.createElement("div");
|
|
29
|
+
document.body.appendChild(container);
|
|
30
|
+
root = createRoot(container);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await act(async () => {
|
|
35
|
+
root.unmount();
|
|
36
|
+
});
|
|
37
|
+
container.remove();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mount = async (
|
|
41
|
+
props: Partial<Parameters<typeof ThreadListNew>[0]> = {},
|
|
42
|
+
) => {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
root.render(
|
|
45
|
+
<ThreadListNew testID="t" {...props}>
|
|
46
|
+
new
|
|
47
|
+
</ThreadListNew>,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return container.querySelector('[data-testid="t"]') as HTMLElement;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("fires switchToNewThread when pressed", async () => {
|
|
54
|
+
const el = await mount();
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
click(el);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(h.switchToNewThread).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not fire when a disabled prop is passed through", async () => {
|
|
64
|
+
const el = await mount({ disabled: true });
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
click(el);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(h.switchToNewThread).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ThreadListItemArchive } from "./ThreadListItemArchive";
|
|
5
|
+
|
|
6
|
+
const h = vi.hoisted(() => ({
|
|
7
|
+
archive: vi.fn<() => void>(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@assistant-ui/core/react", () => ({
|
|
11
|
+
useThreadListItemArchive: () => ({ archive: h.archive }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
const click = (el: Element) =>
|
|
17
|
+
el.dispatchEvent(
|
|
18
|
+
new MouseEvent("click", { bubbles: true, cancelable: true }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("ThreadListItemArchive", () => {
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
h.archive.mockReset();
|
|
27
|
+
|
|
28
|
+
container = document.createElement("div");
|
|
29
|
+
document.body.appendChild(container);
|
|
30
|
+
root = createRoot(container);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await act(async () => {
|
|
35
|
+
root.unmount();
|
|
36
|
+
});
|
|
37
|
+
container.remove();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mount = async (
|
|
41
|
+
props: Partial<Parameters<typeof ThreadListItemArchive>[0]> = {},
|
|
42
|
+
) => {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
root.render(
|
|
45
|
+
<ThreadListItemArchive testID="t" {...props}>
|
|
46
|
+
archive
|
|
47
|
+
</ThreadListItemArchive>,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return container.querySelector('[data-testid="t"]') as HTMLElement;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("fires archive when pressed", async () => {
|
|
54
|
+
const el = await mount();
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
click(el);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(h.archive).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not fire when a disabled prop is passed through", async () => {
|
|
64
|
+
const el = await mount({ disabled: true });
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
click(el);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(h.archive).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ThreadListItemDelete } from "./ThreadListItemDelete";
|
|
5
|
+
|
|
6
|
+
const h = vi.hoisted(() => ({
|
|
7
|
+
deleteThread: vi.fn<() => void>(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@assistant-ui/core/react", () => ({
|
|
11
|
+
useThreadListItemDelete: () => ({ delete: h.deleteThread }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
const click = (el: Element) =>
|
|
17
|
+
el.dispatchEvent(
|
|
18
|
+
new MouseEvent("click", { bubbles: true, cancelable: true }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("ThreadListItemDelete", () => {
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
h.deleteThread.mockReset();
|
|
27
|
+
|
|
28
|
+
container = document.createElement("div");
|
|
29
|
+
document.body.appendChild(container);
|
|
30
|
+
root = createRoot(container);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await act(async () => {
|
|
35
|
+
root.unmount();
|
|
36
|
+
});
|
|
37
|
+
container.remove();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mount = async (
|
|
41
|
+
props: Partial<Parameters<typeof ThreadListItemDelete>[0]> = {},
|
|
42
|
+
) => {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
root.render(
|
|
45
|
+
<ThreadListItemDelete testID="t" {...props}>
|
|
46
|
+
delete
|
|
47
|
+
</ThreadListItemDelete>,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return container.querySelector('[data-testid="t"]') as HTMLElement;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("fires the bound delete when pressed", async () => {
|
|
54
|
+
const el = await mount();
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
click(el);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(h.deleteThread).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not fire when a disabled prop is passed through", async () => {
|
|
64
|
+
const el = await mount({ disabled: true });
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
click(el);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(h.deleteThread).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ThreadListItemTrigger } from "./ThreadListItemTrigger";
|
|
5
|
+
|
|
6
|
+
const h = vi.hoisted(() => ({
|
|
7
|
+
switchTo: vi.fn<() => void>(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@assistant-ui/core/react", () => ({
|
|
11
|
+
useThreadListItemTrigger: () => ({ switchTo: h.switchTo }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
const click = (el: Element) =>
|
|
17
|
+
el.dispatchEvent(
|
|
18
|
+
new MouseEvent("click", { bubbles: true, cancelable: true }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("ThreadListItemTrigger", () => {
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
h.switchTo.mockReset();
|
|
27
|
+
|
|
28
|
+
container = document.createElement("div");
|
|
29
|
+
document.body.appendChild(container);
|
|
30
|
+
root = createRoot(container);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await act(async () => {
|
|
35
|
+
root.unmount();
|
|
36
|
+
});
|
|
37
|
+
container.remove();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const mount = async (
|
|
41
|
+
props: Partial<Parameters<typeof ThreadListItemTrigger>[0]> = {},
|
|
42
|
+
) => {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
root.render(
|
|
45
|
+
<ThreadListItemTrigger testID="t" {...props}>
|
|
46
|
+
open
|
|
47
|
+
</ThreadListItemTrigger>,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return container.querySelector('[data-testid="t"]') as HTMLElement;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("fires switchTo when pressed", async () => {
|
|
54
|
+
const el = await mount();
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
click(el);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(h.switchTo).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not fire when a disabled prop is passed through", async () => {
|
|
64
|
+
const el = await mount({ disabled: true });
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
click(el);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(h.switchTo).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
});
|