();
+ return {
+ ...actual,
+ useOrganization: () => ({ isLoaded: true, organization, membership: null }),
+ useOrganizationList: () => ({ userMemberships: { revalidate } }),
+ };
+});
+
+beforeEach(() => {
+ destroy = vi.fn();
+ revalidate = vi.fn().mockResolvedValue(undefined);
+ organization = { id: 'org_1', name: ORG_NAME, destroy };
+});
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+function Harness() {
+ const controller = useDeleteOrganizationController();
+ if (controller.status !== 'ready') {
+ return ;
+ }
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function openAndConfirm() {
+ fireEvent.click(screen.getByText('Open'));
+ fireEvent.click(screen.getByText('Type'));
+ fireEvent.click(screen.getByText('Confirm'));
+}
+
+describe('useDeleteOrganizationController', () => {
+ it('drives CONFIRM → deleting → resolve → deleted', async () => {
+ const gate = deferred();
+ destroy.mockReturnValue(gate.promise);
+
+ render();
+ expect(screen.getByTestId('state')).toHaveTextContent('idle');
+
+ openAndConfirm();
+ expect(screen.getByTestId('state')).toHaveTextContent('deleting');
+
+ await act(async () => {
+ gate.resolve();
+ });
+
+ expect(screen.getByTestId('state')).toHaveTextContent('deleted');
+ expect(destroy).toHaveBeenCalledTimes(1);
+ expect(revalidate).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns to confirming with an error message when deleting rejects', async () => {
+ const gate = deferred();
+ destroy.mockReturnValue(gate.promise);
+
+ render();
+ openAndConfirm();
+ expect(screen.getByTestId('state')).toHaveTextContent('deleting');
+
+ await act(async () => {
+ gate.reject(new Error('nope'));
+ });
+
+ expect(screen.getByTestId('state')).toHaveTextContent('confirming');
+ expect(screen.getByTestId('error')).toHaveTextContent('nope');
+ expect(revalidate).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/ui/src/mosaic/sections/__tests__/leave-organization-controller.test.tsx b/packages/ui/src/mosaic/sections/__tests__/leave-organization-controller.test.tsx
new file mode 100644
index 00000000000..316edc44ae7
--- /dev/null
+++ b/packages/ui/src/mosaic/sections/__tests__/leave-organization-controller.test.tsx
@@ -0,0 +1,93 @@
+import { act, fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { deferred } from '../../machines/__tests__/test-utils';
+import { useLeaveOrganizationController } from '../leave-organization-controller';
+
+const ORG_NAME = 'Acme Inc';
+
+let destroy: ReturnType;
+let revalidate: ReturnType;
+let organization: { id: string; name: string } | null;
+let membership: { id: string; destroy: () => Promise } | null;
+
+vi.mock('@clerk/shared/react', async importOriginal => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useOrganization: () => ({ isLoaded: true, organization, membership }),
+ useOrganizationList: () => ({ userMemberships: { revalidate } }),
+ };
+});
+
+beforeEach(() => {
+ destroy = vi.fn();
+ revalidate = vi.fn().mockResolvedValue(undefined);
+ organization = { id: 'org_1', name: ORG_NAME };
+ membership = { id: 'mem_1', destroy };
+});
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+function Harness() {
+ const controller = useLeaveOrganizationController();
+ if (controller.status !== 'ready') {
+ return ;
+ }
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function openAndConfirm() {
+ fireEvent.click(screen.getByText('Open'));
+ fireEvent.click(screen.getByText('Type'));
+ fireEvent.click(screen.getByText('Confirm'));
+}
+
+describe('useLeaveOrganizationController', () => {
+ it('drives CONFIRM → leaving → resolve → left', async () => {
+ const gate = deferred();
+ destroy.mockReturnValue(gate.promise);
+
+ render();
+ expect(screen.getByTestId('state')).toHaveTextContent('idle');
+
+ openAndConfirm();
+ expect(screen.getByTestId('state')).toHaveTextContent('leaving');
+
+ await act(async () => {
+ gate.resolve();
+ });
+
+ expect(screen.getByTestId('state')).toHaveTextContent('left');
+ expect(destroy).toHaveBeenCalledTimes(1);
+ expect(revalidate).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns to confirming with an error message when leaving rejects', async () => {
+ const gate = deferred();
+ destroy.mockReturnValue(gate.promise);
+
+ render();
+ openAndConfirm();
+ expect(screen.getByTestId('state')).toHaveTextContent('leaving');
+
+ await act(async () => {
+ gate.reject(new Error('nope'));
+ });
+
+ expect(screen.getByTestId('state')).toHaveTextContent('confirming');
+ expect(screen.getByTestId('error')).toHaveTextContent('nope');
+ expect(revalidate).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/ui/src/mosaic/sections/delete-organization-controller.tsx b/packages/ui/src/mosaic/sections/delete-organization-controller.tsx
index d46bd67fe89..3beab341616 100644
--- a/packages/ui/src/mosaic/sections/delete-organization-controller.tsx
+++ b/packages/ui/src/mosaic/sections/delete-organization-controller.tsx
@@ -1,13 +1,23 @@
+import { useOrganization, useOrganizationList } from '@clerk/shared/react';
+
import { useMachine } from '../machine/useMachine';
-import { useOrganization } from '../mock/use-organization';
import { deleteOrgMachine } from './delete-organization-machine';
export function useDeleteOrganizationController() {
const { isLoaded, organization } = useOrganization();
+ const { userMemberships } = useOrganizationList({ userMemberships: true });
+
const [snapshot, send, actor] = useMachine(deleteOrgMachine, {
context: {
organizationName: organization?.name ?? '',
- destroyOrganization: () => organization?.destroy() ?? Promise.resolve(),
+ destroyOrganization: async () => {
+ await organization?.destroy();
+ // Refresh org lists elsewhere (e.g. the switcher). Not awaited: a stale
+ // list must not make a successful delete look like it failed.
+ void userMemberships.revalidate?.();
+ // TODO(mosaic): navigate away from the deleted org's profile once the flow
+ // has router context, mirroring legacy navigateAfterLeaveOrganization.
+ },
},
});
diff --git a/packages/ui/src/mosaic/sections/leave-organization-controller.tsx b/packages/ui/src/mosaic/sections/leave-organization-controller.tsx
index 7f10440c7b7..6124337b770 100644
--- a/packages/ui/src/mosaic/sections/leave-organization-controller.tsx
+++ b/packages/ui/src/mosaic/sections/leave-organization-controller.tsx
@@ -1,13 +1,23 @@
+import { useOrganization, useOrganizationList } from '@clerk/shared/react';
+
import { useMachine } from '../machine/useMachine';
-import { useOrganization } from '../mock/use-organization';
import { leaveOrgMachine } from './leave-organization-machine';
export function useLeaveOrganizationController() {
const { isLoaded, organization, membership } = useOrganization();
+ const { userMemberships } = useOrganizationList({ userMemberships: true });
+
const [snapshot, send, actor] = useMachine(leaveOrgMachine, {
context: {
organizationName: organization?.name ?? '',
- leaveOrganization: () => membership?.destroy() ?? Promise.resolve(),
+ leaveOrganization: async () => {
+ await membership?.destroy();
+ // Refresh org lists elsewhere (e.g. the switcher). Not awaited: a stale
+ // list must not make a successful leave look like it failed.
+ void userMemberships.revalidate?.();
+ // TODO(mosaic): navigate away from the left org's profile once the flow
+ // has router context, mirroring legacy navigateAfterLeaveOrganization.
+ },
},
});