Files
OpenHands/frontend/__tests__/hooks/query/use-organizations.test.tsx
sp.wack cd2d0ee9a5 feat(frontend): Organizational support (#9496)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: Abhay Mishra <grabhaymishra@gmail.com>
Co-authored-by: Hyun Han <62870362+smosco@users.noreply.github.com>
Co-authored-by: Nhan Nguyen <nhan13574@gmail.com>
Co-authored-by: Bharath A V <avbharath1221@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: Chloe <chloe@openhands.com>
Co-authored-by: HeyItsChloe <54480367+HeyItsChloe@users.noreply.github.com>
2026-03-13 23:38:54 +07:00

175 lines
5.5 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { organizationService } from "#/api/organization-service/organization-service.api";
import { useOrganizations } from "#/hooks/query/use-organizations";
import type { Organization } from "#/types/org";
vi.mock("#/api/organization-service/organization-service.api", () => ({
organizationService: {
getOrganizations: vi.fn(),
},
}));
// Mock useIsAuthed to return authenticated
vi.mock("#/hooks/query/use-is-authed", () => ({
useIsAuthed: () => ({ data: true }),
}));
// Mock useConfig to return SaaS mode (organizations are a SaaS-only feature)
vi.mock("#/hooks/query/use-config", () => ({
useConfig: () => ({ data: { app_mode: "saas" } }),
}));
const mockGetOrganizations = vi.mocked(organizationService.getOrganizations);
function createMinimalOrg(
id: string,
name: string,
is_personal?: boolean,
): Organization {
return {
id,
name,
is_personal,
contact_name: "",
contact_email: "",
conversation_expiration: 0,
agent: "",
default_max_iterations: 0,
security_analyzer: "",
confirmation_mode: false,
default_llm_model: "",
default_llm_api_key_for_byor: "",
default_llm_base_url: "",
remote_runtime_resource_factor: 0,
enable_default_condenser: false,
billing_margin: 0,
enable_proactive_conversation_starters: false,
sandbox_base_container_image: "",
sandbox_runtime_container_image: "",
org_version: 0,
mcp_config: { tools: [], settings: {} },
search_api_key: null,
sandbox_api_key: null,
max_budget_per_task: 0,
enable_solvability_analysis: false,
v1_enabled: false,
credits: 0,
};
}
describe("useOrganizations", () => {
let queryClient: QueryClient;
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
vi.clearAllMocks();
});
it("sorts personal workspace first, then non-personal alphabetically by name", async () => {
// API returns unsorted: Beta, Personal, Acme, All Hands
mockGetOrganizations.mockResolvedValue({
items: [
createMinimalOrg("3", "Beta LLC", false),
createMinimalOrg("1", "Personal Workspace", true),
createMinimalOrg("2", "Acme Corp", false),
createMinimalOrg("4", "All Hands AI", false),
],
currentOrgId: "1",
});
const { result } = renderHook(() => useOrganizations(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
const { organizations } = result.current.data!;
expect(organizations).toHaveLength(4);
expect(organizations[0].id).toBe("1");
expect(organizations[0].is_personal).toBe(true);
expect(organizations[0].name).toBe("Personal Workspace");
expect(organizations[1].name).toBe("Acme Corp");
expect(organizations[2].name).toBe("All Hands AI");
expect(organizations[3].name).toBe("Beta LLC");
});
it("treats missing is_personal as false and sorts by name", async () => {
mockGetOrganizations.mockResolvedValue({
items: [
createMinimalOrg("1", "Zebra Org"), // no is_personal
createMinimalOrg("2", "Alpha Org", true), // personal first
createMinimalOrg("3", "Mango Org"), // no is_personal
],
currentOrgId: "2",
});
const { result } = renderHook(() => useOrganizations(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
const { organizations } = result.current.data!;
expect(organizations[0].id).toBe("2");
expect(organizations[0].is_personal).toBe(true);
expect(organizations[1].name).toBe("Mango Org");
expect(organizations[2].name).toBe("Zebra Org");
});
it("handles missing name by treating as empty string for sort", async () => {
const orgWithName = createMinimalOrg("2", "Beta", false);
const orgNoName = { ...createMinimalOrg("1", "Alpha", false) };
delete (orgNoName as Record<string, unknown>).name;
mockGetOrganizations.mockResolvedValue({
items: [orgWithName, orgNoName] as Organization[],
currentOrgId: "1",
});
const { result } = renderHook(() => useOrganizations(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
const { organizations } = result.current.data!;
// undefined name is coerced to ""; "" sorts before "Beta"
expect(organizations[0].id).toBe("1");
expect(organizations[1].id).toBe("2");
expect(organizations[1].name).toBe("Beta");
});
it("does not mutate the original array from the API", async () => {
const apiOrgs = [
createMinimalOrg("2", "Acme", false),
createMinimalOrg("1", "Personal", true),
];
mockGetOrganizations.mockResolvedValue({
items: apiOrgs,
currentOrgId: "1",
});
const { result } = renderHook(() => useOrganizations(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
// Hook sorts a copy ([...data]), so API order unchanged
expect(apiOrgs[0].id).toBe("2");
expect(apiOrgs[1].id).toBe("1");
// Returned data is sorted
expect(result.current.data!.organizations[0].id).toBe("1");
expect(result.current.data!.organizations[1].id).toBe("2");
});
});