@ai-chans/sdk-react 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # chans-sdk-react
1
+ # @ai-chans/sdk-react
2
2
 
3
3
  React components and hooks for [chans.ai](https://chans.ai) voice AI.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install chans-sdk-react
8
+ npm install @ai-chans/sdk-react
9
9
  ```
10
10
 
11
11
  Requires React 18 or 19.
@@ -17,7 +17,7 @@ Requires React 18 or 19.
17
17
  Drop in a ready-to-use voice button:
18
18
 
19
19
  ```tsx
20
- import { ChansVoice } from "chans-sdk-react"
20
+ import { ChansVoice } from "@ai-chans/sdk-react"
21
21
 
22
22
  function App() {
23
23
  return (
@@ -35,7 +35,7 @@ function App() {
35
35
  Build your own interface with render props:
36
36
 
37
37
  ```tsx
38
- import { ChansVoice } from "chans-sdk-react"
38
+ import { ChansVoice } from "@ai-chans/sdk-react"
39
39
 
40
40
  function App() {
41
41
  return (
@@ -64,7 +64,7 @@ function App() {
64
64
  Access voice state from nested components:
65
65
 
66
66
  ```tsx
67
- import { ChansVoice, useChans } from "chans-sdk-react"
67
+ import { ChansVoice, useChans } from "@ai-chans/sdk-react"
68
68
 
69
69
  function VoiceButton() {
70
70
  const { state, connect, disconnect, isConnected } = useChans()
@@ -145,7 +145,7 @@ type ChansState =
145
145
 
146
146
  ```tsx
147
147
  import { useState } from "react"
148
- import { ChansVoice } from "chans-sdk-react"
148
+ import { ChansVoice } from "@ai-chans/sdk-react"
149
149
 
150
150
  function Chat() {
151
151
  const [messages, setMessages] = useState<Array<{role: string, text: string}>>([])
@@ -175,7 +175,7 @@ function Chat() {
175
175
  ### Manual Connect/Disconnect
176
176
 
177
177
  ```tsx
178
- import { ChansVoice } from "chans-sdk-react"
178
+ import { ChansVoice } from "@ai-chans/sdk-react"
179
179
 
180
180
  function App() {
181
181
  return (
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ChansState } from "@chozzz/chans-sdk-js";
2
- export type { ChansState } from "@chozzz/chans-sdk-js";
1
+ import { ChansState } from "@ai-chans/sdk-js";
2
+ export type { ChansState } from "@ai-chans/sdk-js";
3
3
  export interface ChansVoiceProps {
4
4
  /**
5
5
  * Agent token from chans.ai dashboard
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAe,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAG9D,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAErC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAEnC;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IAE3C;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAEhC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IAExB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,KAAK,CAAC,SAAS,CAAA;IAE5D;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,UAAU,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAGD,KAAK,iBAAiB,GAAG,qBAAqB,CAAA;AAI9C;;GAEG;AACH,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,MAAM,EACN,MAAM,EACN,WAAkB,EAClB,YAAY,EACZ,UAAU,EACV,aAAa,EACb,OAAO,EACP,WAAW,EACX,cAAc,EACd,QAAQ,EACR,SAAS,GACV,EAAE,eAAe,2CA4GjB;AA8ID,eAAe,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAe,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAG1D,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAErC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAEnC;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IAE3C;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAEhC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IAExB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,KAAK,CAAC,SAAS,CAAA;IAE5D;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,UAAU,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAGD,KAAK,iBAAiB,GAAG,qBAAqB,CAAA;AAI9C;;GAEG;AACH,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,MAAM,EACN,MAAM,EACN,WAAkB,EAClB,YAAY,EACZ,UAAU,EACV,aAAa,EACb,OAAO,EACP,WAAW,EACX,cAAc,EACd,QAAQ,EACR,SAAS,GACV,EAAE,eAAe,2CA4GjB;AA8ID,eAAe,UAAU,CAAA"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useCallback, useRef, useMemo, createContext, useContext, } from "react";
4
- import { ChansClient } from "@chozzz/chans-sdk-js";
4
+ import { ChansClient } from "@ai-chans/sdk-js";
5
5
  const ChansContext = createContext(null);
6
6
  /**
7
7
  * Hook to access ChansVoice state from child components
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ai-chans/sdk-react",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
+ "license": "Apache-2.0",
4
5
  "repository": {
5
6
  "type": "git",
6
7
  "url": "https://github.com/ai-chans/sdk.git",
7
- "directory": "react"
8
+ "directory": "sdk-react"
8
9
  },
9
10
  "description": "React component for chans.ai voice AI",
10
11
  "type": "module",
@@ -24,13 +25,19 @@
24
25
  "scripts": {
25
26
  "build": "tsc",
26
27
  "dev": "tsc --watch",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage",
27
31
  "lint": "echo 'lint not configured yet'"
28
32
  },
29
33
  "dependencies": {
30
34
  "@ai-chans/sdk-js": "workspace:*"
31
35
  },
32
36
  "devDependencies": {
37
+ "@testing-library/jest-dom": "^6.9.1",
38
+ "@testing-library/react": "^16.3.1",
33
39
  "@types/react": "^19",
40
+ "happy-dom": "^20.0.11",
34
41
  "typescript": "^5.9.3"
35
42
  },
36
43
  "peerDependencies": {
@@ -0,0 +1,529 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2
+ import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"
3
+ import { ChansVoice, useChans } from "./index.js"
4
+
5
+ // Helper to create a mock ChansClient instance
6
+ function createMockClient() {
7
+ const listeners = new Map<string, Set<(...args: unknown[]) => void>>()
8
+
9
+ const mockClient = {
10
+ on: vi.fn((event: string, callback: (...args: unknown[]) => void) => {
11
+ if (!listeners.has(event)) {
12
+ listeners.set(event, new Set())
13
+ }
14
+ listeners.get(event)!.add(callback)
15
+ return () => listeners.get(event)?.delete(callback)
16
+ }),
17
+ off: vi.fn(),
18
+ connect: vi.fn().mockResolvedValue(undefined),
19
+ disconnect: vi.fn().mockResolvedValue(undefined),
20
+ getState: vi.fn().mockReturnValue("idle"),
21
+ isConnected: vi.fn().mockReturnValue(false),
22
+ // Helper to emit events in tests
23
+ _emit: (event: string, ...args: unknown[]) => {
24
+ listeners.get(event)?.forEach((cb) => cb(...args))
25
+ },
26
+ _listeners: listeners,
27
+ }
28
+
29
+ return mockClient
30
+ }
31
+
32
+ // Store the mock client for test access
33
+ let currentMockClient: ReturnType<typeof createMockClient>
34
+ // Track constructor calls for verification
35
+ let lastConstructorArgs: unknown[] = []
36
+
37
+ // Mock the sdk-js module with a proper class constructor
38
+ vi.mock("@ai-chans/sdk-js", async () => {
39
+ const actual = await vi.importActual("@ai-chans/sdk-js")
40
+ return {
41
+ ...actual,
42
+ ChansClient: class MockChansClient {
43
+ constructor(...args: unknown[]) {
44
+ lastConstructorArgs = args
45
+ // Copy all methods from currentMockClient to this instance
46
+ Object.assign(this, currentMockClient)
47
+ }
48
+ },
49
+ }
50
+ })
51
+
52
+ describe("ChansVoice", () => {
53
+ let mockClient: ReturnType<typeof createMockClient>
54
+
55
+ beforeEach(() => {
56
+ mockClient = createMockClient()
57
+ currentMockClient = mockClient
58
+ })
59
+
60
+ afterEach(() => {
61
+ vi.clearAllMocks()
62
+ })
63
+
64
+ describe("rendering", () => {
65
+ it("should render default UI", () => {
66
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
67
+
68
+ expect(screen.getByRole("button")).toBeInTheDocument()
69
+ expect(screen.getByText("Click to start")).toBeInTheDocument()
70
+ })
71
+
72
+ it("should render custom children", () => {
73
+ render(
74
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
75
+ {({ state }) => <div data-testid="custom">State: {state}</div>}
76
+ </ChansVoice>
77
+ )
78
+
79
+ expect(screen.getByTestId("custom")).toHaveTextContent("State: idle")
80
+ })
81
+
82
+ it("should apply className to wrapper", () => {
83
+ const { container } = render(
84
+ <ChansVoice agentToken="agt_test" autoConnect={false} className="custom-class" />
85
+ )
86
+
87
+ expect(container.querySelector(".custom-class")).toBeInTheDocument()
88
+ })
89
+ })
90
+
91
+ describe("client initialization", () => {
92
+ it("should create ChansClient with agentToken", () => {
93
+ render(<ChansVoice agentToken="agt_test_token" autoConnect={false} />)
94
+
95
+ expect(lastConstructorArgs[0]).toEqual({
96
+ agentToken: "agt_test_token",
97
+ apiUrl: undefined,
98
+ })
99
+ })
100
+
101
+ it("should create ChansClient with custom apiUrl", () => {
102
+ render(
103
+ <ChansVoice
104
+ agentToken="agt_test"
105
+ apiUrl="https://custom.api.com"
106
+ autoConnect={false}
107
+ />
108
+ )
109
+
110
+ expect(lastConstructorArgs[0]).toEqual({
111
+ agentToken: "agt_test",
112
+ apiUrl: "https://custom.api.com",
113
+ })
114
+ })
115
+
116
+ it("should set up event listeners on mount", () => {
117
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
118
+
119
+ expect(mockClient.on).toHaveBeenCalledWith("stateChange", expect.any(Function))
120
+ expect(mockClient.on).toHaveBeenCalledWith("transcript", expect.any(Function))
121
+ expect(mockClient.on).toHaveBeenCalledWith("response", expect.any(Function))
122
+ expect(mockClient.on).toHaveBeenCalledWith("error", expect.any(Function))
123
+ expect(mockClient.on).toHaveBeenCalledWith("connected", expect.any(Function))
124
+ expect(mockClient.on).toHaveBeenCalledWith("disconnected", expect.any(Function))
125
+ })
126
+
127
+ it("should disconnect on unmount", () => {
128
+ const { unmount } = render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
129
+
130
+ unmount()
131
+
132
+ expect(mockClient.disconnect).toHaveBeenCalled()
133
+ })
134
+ })
135
+
136
+ describe("auto-connect", () => {
137
+ it("should auto-connect by default", async () => {
138
+ render(<ChansVoice agentToken="agt_test" />)
139
+
140
+ await waitFor(() => {
141
+ expect(mockClient.connect).toHaveBeenCalledWith({ userId: undefined })
142
+ })
143
+ })
144
+
145
+ it("should not auto-connect when autoConnect=false", async () => {
146
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
147
+
148
+ // Give it a moment to potentially call connect
149
+ await new Promise((r) => setTimeout(r, 50))
150
+
151
+ expect(mockClient.connect).not.toHaveBeenCalled()
152
+ })
153
+
154
+ it("should pass userId to connect", async () => {
155
+ render(<ChansVoice agentToken="agt_test" userId="user-123" />)
156
+
157
+ await waitFor(() => {
158
+ expect(mockClient.connect).toHaveBeenCalledWith({ userId: "user-123" })
159
+ })
160
+ })
161
+ })
162
+
163
+ describe("state management", () => {
164
+ it("should update state on stateChange event", async () => {
165
+ render(
166
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
167
+ {({ state }) => <div data-testid="state">{state}</div>}
168
+ </ChansVoice>
169
+ )
170
+
171
+ expect(screen.getByTestId("state")).toHaveTextContent("idle")
172
+
173
+ act(() => {
174
+ mockClient._emit("stateChange", "connecting")
175
+ })
176
+
177
+ expect(screen.getByTestId("state")).toHaveTextContent("connecting")
178
+
179
+ act(() => {
180
+ mockClient._emit("stateChange", "listening")
181
+ })
182
+
183
+ expect(screen.getByTestId("state")).toHaveTextContent("listening")
184
+ })
185
+
186
+ it("should calculate isConnected correctly", () => {
187
+ render(
188
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
189
+ {({ isConnected }) => (
190
+ <div data-testid="connected">{isConnected ? "yes" : "no"}</div>
191
+ )}
192
+ </ChansVoice>
193
+ )
194
+
195
+ expect(screen.getByTestId("connected")).toHaveTextContent("no")
196
+
197
+ act(() => {
198
+ mockClient._emit("stateChange", "listening")
199
+ })
200
+
201
+ expect(screen.getByTestId("connected")).toHaveTextContent("yes")
202
+
203
+ act(() => {
204
+ mockClient._emit("stateChange", "error")
205
+ })
206
+
207
+ expect(screen.getByTestId("connected")).toHaveTextContent("no")
208
+ })
209
+
210
+ it("should handle errors", async () => {
211
+ const onError = vi.fn()
212
+ render(
213
+ <ChansVoice agentToken="agt_test" autoConnect={false} onError={onError}>
214
+ {({ error }) => (
215
+ <div data-testid="error">{error?.message || "no error"}</div>
216
+ )}
217
+ </ChansVoice>
218
+ )
219
+
220
+ expect(screen.getByTestId("error")).toHaveTextContent("no error")
221
+
222
+ act(() => {
223
+ mockClient._emit("error", new Error("Connection failed"))
224
+ })
225
+
226
+ expect(screen.getByTestId("error")).toHaveTextContent("Connection failed")
227
+ expect(onError).toHaveBeenCalledWith(expect.any(Error))
228
+ })
229
+
230
+ it("should clear error on connected event", async () => {
231
+ render(
232
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
233
+ {({ error }) => (
234
+ <div data-testid="error">{error?.message || "no error"}</div>
235
+ )}
236
+ </ChansVoice>
237
+ )
238
+
239
+ // Set an error
240
+ act(() => {
241
+ mockClient._emit("error", new Error("Some error"))
242
+ })
243
+ expect(screen.getByTestId("error")).toHaveTextContent("Some error")
244
+
245
+ // Connect clears error
246
+ act(() => {
247
+ mockClient._emit("connected")
248
+ })
249
+ expect(screen.getByTestId("error")).toHaveTextContent("no error")
250
+ })
251
+ })
252
+
253
+ describe("callbacks", () => {
254
+ it("should call onStateChange", () => {
255
+ const onStateChange = vi.fn()
256
+ render(
257
+ <ChansVoice
258
+ agentToken="agt_test"
259
+ autoConnect={false}
260
+ onStateChange={onStateChange}
261
+ />
262
+ )
263
+
264
+ act(() => {
265
+ mockClient._emit("stateChange", "connecting")
266
+ })
267
+
268
+ expect(onStateChange).toHaveBeenCalledWith("connecting")
269
+ })
270
+
271
+ it("should call onTranscript", () => {
272
+ const onTranscript = vi.fn()
273
+ render(
274
+ <ChansVoice
275
+ agentToken="agt_test"
276
+ autoConnect={false}
277
+ onTranscript={onTranscript}
278
+ />
279
+ )
280
+
281
+ act(() => {
282
+ mockClient._emit("transcript", "Hello world")
283
+ })
284
+
285
+ expect(onTranscript).toHaveBeenCalledWith("Hello world")
286
+ })
287
+
288
+ it("should call onResponse", () => {
289
+ const onResponse = vi.fn()
290
+ render(
291
+ <ChansVoice
292
+ agentToken="agt_test"
293
+ autoConnect={false}
294
+ onResponse={onResponse}
295
+ />
296
+ )
297
+
298
+ act(() => {
299
+ mockClient._emit("response", "Hi there!")
300
+ })
301
+
302
+ expect(onResponse).toHaveBeenCalledWith("Hi there!")
303
+ })
304
+
305
+ it("should call onConnected", () => {
306
+ const onConnected = vi.fn()
307
+ render(
308
+ <ChansVoice
309
+ agentToken="agt_test"
310
+ autoConnect={false}
311
+ onConnected={onConnected}
312
+ />
313
+ )
314
+
315
+ act(() => {
316
+ mockClient._emit("connected")
317
+ })
318
+
319
+ expect(onConnected).toHaveBeenCalled()
320
+ })
321
+
322
+ it("should call onDisconnected", () => {
323
+ const onDisconnected = vi.fn()
324
+ render(
325
+ <ChansVoice
326
+ agentToken="agt_test"
327
+ autoConnect={false}
328
+ onDisconnected={onDisconnected}
329
+ />
330
+ )
331
+
332
+ act(() => {
333
+ mockClient._emit("disconnected")
334
+ })
335
+
336
+ expect(onDisconnected).toHaveBeenCalled()
337
+ })
338
+ })
339
+
340
+ describe("connect/disconnect controls", () => {
341
+ it("should call connect when clicking button in idle state", async () => {
342
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
343
+
344
+ fireEvent.click(screen.getByRole("button"))
345
+
346
+ await waitFor(() => {
347
+ expect(mockClient.connect).toHaveBeenCalled()
348
+ })
349
+ })
350
+
351
+ it("should call disconnect when clicking button in connected state", async () => {
352
+ render(
353
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
354
+ {({ isConnected, connect, disconnect }) => (
355
+ <button onClick={isConnected ? disconnect : connect}>
356
+ {isConnected ? "Stop" : "Start"}
357
+ </button>
358
+ )}
359
+ </ChansVoice>
360
+ )
361
+
362
+ // Simulate connected state
363
+ act(() => {
364
+ mockClient._emit("stateChange", "listening")
365
+ })
366
+
367
+ expect(screen.getByRole("button")).toHaveTextContent("Stop")
368
+
369
+ fireEvent.click(screen.getByRole("button"))
370
+
371
+ await waitFor(() => {
372
+ expect(mockClient.disconnect).toHaveBeenCalled()
373
+ })
374
+ })
375
+
376
+ it("should expose connect/disconnect in render props", () => {
377
+ const connectFn = vi.fn()
378
+ const disconnectFn = vi.fn()
379
+
380
+ render(
381
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
382
+ {({ connect, disconnect }) => (
383
+ <>
384
+ <button data-testid="connect" onClick={connect}>
385
+ Connect
386
+ </button>
387
+ <button data-testid="disconnect" onClick={disconnect}>
388
+ Disconnect
389
+ </button>
390
+ </>
391
+ )}
392
+ </ChansVoice>
393
+ )
394
+
395
+ fireEvent.click(screen.getByTestId("connect"))
396
+ expect(mockClient.connect).toHaveBeenCalled()
397
+
398
+ fireEvent.click(screen.getByTestId("disconnect"))
399
+ expect(mockClient.disconnect).toHaveBeenCalled()
400
+ })
401
+ })
402
+
403
+ describe("default UI states", () => {
404
+ it("should show 'Click to start' in idle state", () => {
405
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
406
+ expect(screen.getByText("Click to start")).toBeInTheDocument()
407
+ })
408
+
409
+ it("should show 'Connecting...' in connecting state", () => {
410
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
411
+
412
+ act(() => {
413
+ mockClient._emit("stateChange", "connecting")
414
+ })
415
+
416
+ expect(screen.getByText("Connecting...")).toBeInTheDocument()
417
+ })
418
+
419
+ it("should show 'Listening...' in listening state", () => {
420
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
421
+
422
+ act(() => {
423
+ mockClient._emit("stateChange", "listening")
424
+ })
425
+
426
+ expect(screen.getByText("Listening...")).toBeInTheDocument()
427
+ })
428
+
429
+ it("should show 'Agent speaking' in speaking state", () => {
430
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
431
+
432
+ act(() => {
433
+ mockClient._emit("stateChange", "speaking")
434
+ })
435
+
436
+ expect(screen.getByText("Agent speaking")).toBeInTheDocument()
437
+ })
438
+
439
+ it("should show error message when error occurs", () => {
440
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
441
+
442
+ act(() => {
443
+ mockClient._emit("error", new Error("Connection failed"))
444
+ mockClient._emit("stateChange", "error")
445
+ })
446
+
447
+ expect(screen.getByText("Connection failed")).toBeInTheDocument()
448
+ })
449
+
450
+ it("should disable button in connecting state", () => {
451
+ render(<ChansVoice agentToken="agt_test" autoConnect={false} />)
452
+
453
+ act(() => {
454
+ mockClient._emit("stateChange", "connecting")
455
+ })
456
+
457
+ expect(screen.getByRole("button")).toBeDisabled()
458
+ })
459
+ })
460
+ })
461
+
462
+ describe("useChans", () => {
463
+ let mockClient: ReturnType<typeof createMockClient>
464
+
465
+ beforeEach(() => {
466
+ mockClient = createMockClient()
467
+ currentMockClient = mockClient
468
+ })
469
+
470
+ afterEach(() => {
471
+ vi.clearAllMocks()
472
+ })
473
+
474
+ it("should throw error when used outside ChansVoice", () => {
475
+ // Suppress console.error for this test
476
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
477
+
478
+ function TestComponent() {
479
+ useChans()
480
+ return null
481
+ }
482
+
483
+ expect(() => render(<TestComponent />)).toThrow(
484
+ "useChans must be used within a ChansVoice component"
485
+ )
486
+
487
+ consoleSpy.mockRestore()
488
+ })
489
+
490
+ it("should provide context values when used inside ChansVoice", () => {
491
+ function ChildComponent() {
492
+ const { state, isConnected } = useChans()
493
+ return (
494
+ <div>
495
+ <span data-testid="state">{state}</span>
496
+ <span data-testid="connected">{isConnected ? "yes" : "no"}</span>
497
+ </div>
498
+ )
499
+ }
500
+
501
+ render(
502
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
503
+ {() => <ChildComponent />}
504
+ </ChansVoice>
505
+ )
506
+
507
+ expect(screen.getByTestId("state")).toHaveTextContent("idle")
508
+ expect(screen.getByTestId("connected")).toHaveTextContent("no")
509
+ })
510
+
511
+ it("should update when state changes", () => {
512
+ function ChildComponent() {
513
+ const { state } = useChans()
514
+ return <span data-testid="state">{state}</span>
515
+ }
516
+
517
+ render(
518
+ <ChansVoice agentToken="agt_test" autoConnect={false}>
519
+ {() => <ChildComponent />}
520
+ </ChansVoice>
521
+ )
522
+
523
+ act(() => {
524
+ mockClient._emit("stateChange", "listening")
525
+ })
526
+
527
+ expect(screen.getByTestId("state")).toHaveTextContent("listening")
528
+ })
529
+ })
package/src/index.tsx CHANGED
@@ -9,10 +9,10 @@ import {
9
9
  createContext,
10
10
  useContext,
11
11
  } from "react"
12
- import { ChansClient, ChansState } from "@chozzz/chans-sdk-js"
12
+ import { ChansClient, ChansState } from "@ai-chans/sdk-js"
13
13
 
14
14
  // Re-export client types
15
- export type { ChansState } from "@chozzz/chans-sdk-js"
15
+ export type { ChansState } from "@ai-chans/sdk-js"
16
16
 
17
17
  export interface ChansVoiceProps {
18
18
  /**