Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 9 additions & 35 deletions src/app/(protected)/active-call/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default function ActiveCallPage() {
setIsSpeakerOn,
isInCall,
takeCall,
hangUpCall,
isAudioConnected,
} = useActiveCall();

Expand Down Expand Up @@ -425,9 +426,7 @@ export default function ActiveCallPage() {
</div>
<div className="flex items-center gap-2">
<TranslateIcon className="h-4 w-4 text-muted-foreground" weight="duotone" />
<span className="text-xs text-muted-foreground">
Auto-translating from Spanish
</span>
<span className="text-xs text-muted-foreground">Auto-translating</span>
</div>
</div>
</div>
Expand Down Expand Up @@ -465,36 +464,6 @@ export default function ActiveCallPage() {

{/* Insights List */}
<div className="flex-1 overflow-auto p-4 space-y-4">
{/* WebSocket Connection Status */}
<div className="rounded-lg border border-border bg-card p-3 mb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className={cn(
"h-2 w-2 rounded-full",
wsConnectionState.isConnected
? "bg-green-500 animate-pulse"
: "bg-red-500"
)}
/>
<span className="text-xs text-muted-foreground">
{wsConnectionState.isConnected
? "Real-time Updates Active"
: "Disconnected"}
</span>
</div>
{wsConnectionState.sessionId && (
<span className="text-xs text-muted-foreground font-mono">
{wsConnectionState.sessionId.slice(0, 8)}
</span>
)}
</div>
</div>

{aiInsights.map((insight, index) => (
<InsightCard key={index} insight={insight} />
))}

{/* Caller Info Card - Real Data from WebSocket */}
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex items-center gap-2 mb-3">
Expand Down Expand Up @@ -539,7 +508,9 @@ export default function ActiveCallPage() {
</div>
</div>
</div>

{aiInsights.map((insight, index) => (
<InsightCard key={index} insight={insight} />
))}
{/* Connection Events Log (for debugging) */}
{connectionEvents.length > 0 && (
<div className="rounded-lg border border-border bg-card p-4">
Expand Down Expand Up @@ -600,7 +571,10 @@ export default function ActiveCallPage() {
)}
</button>

<button className="flex h-16 w-16 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600 transition-colors shadow-lg">
<button
onClick={hangUpCall}
className="flex h-16 w-16 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600 transition-colors shadow-lg"
>
<PhoneXIcon className="h-8 w-8" weight="fill" />
</button>

Expand Down
288 changes: 288 additions & 0 deletions src/contexts/__tests__/active-call-context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1342,4 +1342,292 @@ describe("ActiveCallContext", () => {

consoleWarn.mockRestore();
});

describe("hangUpCall", () => {
it("should hang up call successfully and update state", async () => {
mockSearchParams.set("callId", "call-123");

mockHandoffApi.takeControl.mockResolvedValue({
success: true,
handoffId: "handoff-456",
message: "Control taken",
aiTerminated: true,
});

const mockAudioManagerInstance = {
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
requestMicrophonePermission: jest.fn().mockResolvedValue(true),
setMuted: jest.fn(),
setSpeakerOn: jest.fn(),
isConnected: jest.fn().mockReturnValue(true),
hangUp: jest.fn(),
};

(mockAudioManager as unknown as jest.Mock).mockImplementation(() => mockAudioManagerInstance);

function HangUpComponent() {
const { takeCall, hangUpCall, isInCall, callStatus } = useActiveCall();
return (
<div>
<button onClick={takeCall} data-testid="take-call-btn">
Take Call
</button>
<button onClick={hangUpCall} data-testid="hang-up-btn">
Hang Up
</button>
<div data-testid="in-call">{isInCall ? "In Call" : "Not In Call"}</div>
<div data-testid="call-status">{callStatus || "Unknown"}</div>
</div>
);
}

render(
<ActiveCallProvider>
<HangUpComponent />
</ActiveCallProvider>
);

await waitFor(() => {
expect(screen.getByTestId("take-call-btn")).toBeInTheDocument();
});

// Take call first
fireEvent.click(screen.getByTestId("take-call-btn"));

await waitFor(() => {
expect(mockHandoffApi.takeControl).toHaveBeenCalled();
expect(screen.getByTestId("in-call")).toHaveTextContent("In Call");
});

// Now hang up
fireEvent.click(screen.getByTestId("hang-up-btn"));

await waitFor(() => {
expect(mockAudioManagerInstance.hangUp).toHaveBeenCalled();
expect(screen.getByTestId("in-call")).toHaveTextContent("Not In Call");
expect(screen.getByTestId("call-status")).toHaveTextContent("completed");
});
});

it("should show error when hanging up without active call", () => {
mockSearchParams.set("callId", "call-123");

function HangUpComponent() {
const { hangUpCall } = useActiveCall();
return (
<button onClick={hangUpCall} data-testid="hang-up-btn">
Hang Up
</button>
);
}

render(
<ActiveCallProvider>
<HangUpComponent />
</ActiveCallProvider>
);

// Try to hang up without taking call first
fireEvent.click(screen.getByTestId("hang-up-btn"));

// Should not crash, error handling is done via toast
expect(screen.getByTestId("hang-up-btn")).toBeInTheDocument();
});

it("should handle hangUp errors gracefully", async () => {
mockSearchParams.set("callId", "call-123");

mockHandoffApi.takeControl.mockResolvedValue({
success: true,
handoffId: "handoff-456",
message: "Control taken",
aiTerminated: true,
});

const mockAudioManagerInstance = {
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
requestMicrophonePermission: jest.fn().mockResolvedValue(true),
setMuted: jest.fn(),
setSpeakerOn: jest.fn(),
isConnected: jest.fn().mockReturnValue(true),
hangUp: jest.fn().mockImplementation(() => {
throw new Error("Failed to hang up");
}),
};

(mockAudioManager as unknown as jest.Mock).mockImplementation(() => mockAudioManagerInstance);

function HangUpComponent() {
const { takeCall, hangUpCall } = useActiveCall();
return (
<div>
<button onClick={takeCall} data-testid="take-call-btn">
Take Call
</button>
<button onClick={hangUpCall} data-testid="hang-up-btn">
Hang Up
</button>
</div>
);
}

render(
<ActiveCallProvider>
<HangUpComponent />
</ActiveCallProvider>
);

await waitFor(() => {
expect(screen.getByTestId("take-call-btn")).toBeInTheDocument();
});

// Take call first
fireEvent.click(screen.getByTestId("take-call-btn"));

await waitFor(() => {
expect(mockHandoffApi.takeControl).toHaveBeenCalled();
});

// Try to hang up (should handle error)
fireEvent.click(screen.getByTestId("hang-up-btn"));

// Should not crash
expect(screen.getByTestId("hang-up-btn")).toBeInTheDocument();
});

it("should clear audio manager reference after hang up", async () => {
mockSearchParams.set("callId", "call-123");

mockHandoffApi.takeControl.mockResolvedValue({
success: true,
handoffId: "handoff-456",
message: "Control taken",
aiTerminated: true,
});

const mockAudioManagerInstance = {
connect: jest.fn().mockImplementation(async (_handoffId, _onMessage, onConnected) => {
// Simulate successful connection
if (onConnected) {
onConnected();
}
}),
disconnect: jest.fn(),
requestMicrophonePermission: jest.fn().mockResolvedValue(true),
setMuted: jest.fn(),
setSpeakerOn: jest.fn(),
isConnected: jest.fn().mockReturnValue(true),
hangUp: jest.fn(),
};

(mockAudioManager as unknown as jest.Mock).mockImplementation(() => mockAudioManagerInstance);

function HangUpComponent() {
const { takeCall, hangUpCall, isAudioConnected } = useActiveCall();
return (
<div>
<button onClick={takeCall} data-testid="take-call-btn">
Take Call
</button>
<button onClick={hangUpCall} data-testid="hang-up-btn">
Hang Up
</button>
<div data-testid="audio-connected">
{isAudioConnected ? "Audio Connected" : "Audio Disconnected"}
</div>
</div>
);
}

render(
<ActiveCallProvider>
<HangUpComponent />
</ActiveCallProvider>
);

await waitFor(() => {
expect(screen.getByTestId("take-call-btn")).toBeInTheDocument();
});

// Take call first
await act(async () => {
fireEvent.click(screen.getByTestId("take-call-btn"));
});

await waitFor(() => {
expect(screen.getByTestId("audio-connected")).toHaveTextContent("Audio Connected");
});

// Hang up
fireEvent.click(screen.getByTestId("hang-up-btn"));

await waitFor(() => {
expect(screen.getByTestId("audio-connected")).toHaveTextContent("Audio Disconnected");
});
});

it("should update call status to completed after hang up", async () => {
mockSearchParams.set("callId", "call-123");

mockHandoffApi.takeControl.mockResolvedValue({
success: true,
handoffId: "handoff-456",
message: "Control taken",
aiTerminated: true,
});

const mockAudioManagerInstance = {
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
requestMicrophonePermission: jest.fn().mockResolvedValue(true),
setMuted: jest.fn(),
setSpeakerOn: jest.fn(),
isConnected: jest.fn().mockReturnValue(true),
hangUp: jest.fn(),
};

(mockAudioManager as unknown as jest.Mock).mockImplementation(() => mockAudioManagerInstance);

function StatusComponent() {
const { takeCall, hangUpCall, callStatus } = useActiveCall();
return (
<div>
<button onClick={takeCall} data-testid="take-call-btn">
Take Call
</button>
<button onClick={hangUpCall} data-testid="hang-up-btn">
Hang Up
</button>
<div data-testid="status">{callStatus || "No Status"}</div>
</div>
);
}

render(
<ActiveCallProvider>
<StatusComponent />
</ActiveCallProvider>
);

await waitFor(() => {
expect(screen.getByTestId("take-call-btn")).toBeInTheDocument();
});

// Take call
fireEvent.click(screen.getByTestId("take-call-btn"));

await waitFor(() => {
expect(screen.getByTestId("status")).toHaveTextContent("active");
});

// Hang up
fireEvent.click(screen.getByTestId("hang-up-btn"));

await waitFor(() => {
expect(screen.getByTestId("status")).toHaveTextContent("completed");
});
});
});
});
Loading
Loading