// @vitest-environment jsdom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { VoiceCallContext } from "./VoiceCallContext";
import { queryKeys } from "@/lib/queryKeys";
import { TaskVoiceLauncher } from "./TaskVoiceLauncher";
import { spekoEndpoint } from "../../../storybook/support/speko/spekoNative";
const mocks = vi.hoisted(() => ({ list: vi.fn() }));
vi.mock("@/api/chatEndpoints", () => ({ chatEndpointsApi: { list: mocks.list } }));
vi.mock("@/hooks/useChatConnectorsEnabled", () => ({ useChatConnectorsEnabled: () => ({ enabled: true }) }));
vi.mock("./NativeVoiceConversation", () => ({ NativeVoiceConversation: () =>
Active voice controls
}));
describe("task voice connection failures", () => {
let container: HTMLDivElement, root: Root, client: QueryClient;
beforeEach(() => {
vi.resetAllMocks(); container = document.createElement("div"); document.body.append(container);
root = createRoot(container); client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: 30_000 } } });
});
afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); });
async function render(bound = false, active?: boolean) {
await act(async () => root.render());
}
it.each([false, true])("shows a retryable request failure for bound=%s instead of a missing connection", async bound => {
mocks.list.mockRejectedValueOnce(new Error("Offline")).mockResolvedValue([spekoEndpoint]); await render(bound);
await vi.waitFor(() => expect(container.querySelector('[role="alert"]')?.textContent).toContain("Voice connections could not be loaded"));
expect(container.textContent).not.toContain("Voice is unavailable");
container.querySelector('button')!.focus();
await act(async () => container.querySelector('button')!.click());
await vi.waitFor(() => expect(container.textContent).toContain(`Talk to ${spekoEndpoint.assignedAgentName}`));
expect(container.querySelector('[role="alert"]')).toBeNull();
expect(document.activeElement).toBe(container.querySelector('button'));
});
it("restores persona selection and keyboard focus after the hosted panel closes", async () => {
mocks.list.mockResolvedValue([spekoEndpoint, {...spekoEndpoint, id: "second", botLabel: "Second persona"}]);
await render(false, false);
await vi.waitFor(() => expect(container.querySelector('select')).not.toBeNull());
await act(async () => container.querySelector('button')!.click());
expect(container.querySelector('select')).toBeNull();
await render(false, true);
await render(false, false);
await vi.waitFor(() => expect(container.querySelector('select')).not.toBeNull());
expect(document.activeElement).toBe(container.querySelector('button'));
await act(async () => {
const select = container.querySelector('select')!; select.value = "second";
select.dispatchEvent(new Event("change", {bubbles: true}));
});
expect(container.querySelector('select')?.value).toBe("second");
});
it("updates the mounted launcher after shared connection pause and resume invalidation", async () => {
mocks.list.mockResolvedValue([spekoEndpoint]); await render(true);
await vi.waitFor(() => expect(container.textContent).toContain(`Talk to ${spekoEndpoint.assignedAgentName}`));
mocks.list.mockResolvedValue([{...spekoEndpoint, status: "paused"}]);
await act(async () => { await client.invalidateQueries({queryKey: queryKeys.chatEndpoints.list("company")}); });
await vi.waitFor(() => expect(container.textContent).toContain("Voice is unavailable"));
expect(container.textContent).not.toContain(`Talk to ${spekoEndpoint.assignedAgentName}`);
mocks.list.mockResolvedValue([spekoEndpoint]);
await act(async () => { await client.invalidateQueries({queryKey: queryKeys.chatEndpoints.list("company")}); });
await vi.waitFor(() => expect(container.textContent).toContain(`Talk to ${spekoEndpoint.assignedAgentName}`));
});
it("keeps active controls mounted when a background connection refresh fails", async () => {
mocks.list.mockResolvedValue([spekoEndpoint]); await render();
await vi.waitFor(() => expect(container.querySelector('button')).not.toBeNull());
await act(async () => container.querySelector('button')!.click());
expect(container.textContent).toContain("Active voice controls");
mocks.list.mockRejectedValue(new Error("Offline"));
await act(async () => { await client.refetchQueries({queryKey: queryKeys.chatEndpoints.list("company")}); });
await vi.waitFor(() => expect(container.querySelector('[role="alert"]')).not.toBeNull());
expect(container.textContent).toContain("Active voice controls");
});
});