fix(web): SpaceInviteCard navigates to space on already-member instead of showing error
This commit is contained in:
@@ -3,11 +3,12 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { SpaceInviteCard } from './SpaceInviteCard';
|
import { SpaceInviteCard } from './SpaceInviteCard';
|
||||||
|
|
||||||
const { mockJoinByCode, mockGetApiForOrigin } = vi.hoisted(() => ({
|
const { mockJoinByCode, mockGetApiForOrigin, mockNavigate } = vi.hoisted(() => ({
|
||||||
mockJoinByCode: vi.fn(),
|
mockJoinByCode: vi.fn(),
|
||||||
mockGetApiForOrigin: vi.fn(() => ({
|
mockGetApiForOrigin: vi.fn(() => ({
|
||||||
spaces: { invitePreview: vi.fn() },
|
spaces: { invitePreview: vi.fn() },
|
||||||
})),
|
})),
|
||||||
|
mockNavigate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock('../../stores/spaceStore', () => ({
|
vi.mock('../../stores/spaceStore', () => ({
|
||||||
useSpaceStore: (selector: any) => selector({ joinByCode: mockJoinByCode }),
|
useSpaceStore: (selector: any) => selector({ joinByCode: mockJoinByCode }),
|
||||||
@@ -16,6 +17,10 @@ vi.mock('../../stores/spaceStore', () => ({
|
|||||||
vi.mock('../../api/client', () => ({
|
vi.mock('../../api/client', () => ({
|
||||||
createApiClient: vi.fn(),
|
createApiClient: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
vi.mock('react-router-dom', async () => ({
|
||||||
|
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
}));
|
||||||
|
|
||||||
const basePayload = {
|
const basePayload = {
|
||||||
event: 'space_invite' as const,
|
event: 'space_invite' as const,
|
||||||
@@ -36,6 +41,7 @@ describe('SpaceInviteCard', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockJoinByCode.mockReset();
|
mockJoinByCode.mockReset();
|
||||||
|
mockNavigate.mockReset();
|
||||||
mockJoinByCode.mockResolvedValue({ id: 'S1', name: 'Aether' });
|
mockJoinByCode.mockResolvedValue({ id: 'S1', name: 'Aether' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,4 +97,21 @@ describe('SpaceInviteCard', () => {
|
|||||||
expect(mockJoinByCode).not.toHaveBeenCalledWith('abc', '');
|
expect(mockJoinByCode).not.toHaveBeenCalledWith('abc', '');
|
||||||
expect(mockJoinByCode).not.toHaveBeenCalledWith('abc', undefined);
|
expect(mockJoinByCode).not.toHaveBeenCalledWith('abc', undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('navigates to space when join returns "already a member" (no error shown)', async () => {
|
||||||
|
const userEvent = (await import('@testing-library/user-event')).default;
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
mockJoinByCode.mockRejectedValueOnce(new Error('You are already a member of this space'));
|
||||||
|
mockGetApiForOrigin.mockReturnValue({
|
||||||
|
spaces: { invitePreview: vi.fn().mockResolvedValue({ ...basePayload.snapshot, spaceId: 'S1' }) },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<MemoryRouter><SpaceInviteCard payload={basePayload} senderName="Alice" /></MemoryRouter>);
|
||||||
|
const btn = await screen.findByRole('button', { name: /^join$/i });
|
||||||
|
await user.click(btn);
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/spaces/S1'));
|
||||||
|
expect(screen.queryByText(/already a member of this space/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,7 +56,15 @@ export function SpaceInviteCard({ payload, senderName }: Props) {
|
|||||||
const space = await joinByCode(payload.inviteCode, payload.spaceInstanceOrigin || undefined);
|
const space = await joinByCode(payload.inviteCode, payload.spaceInstanceOrigin || undefined);
|
||||||
navigate(`/spaces/${space.id}`);
|
navigate(`/spaces/${space.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setJoinError((err as Error)?.message ?? 'Failed to join');
|
const msg = (err as Error)?.message ?? '';
|
||||||
|
if (msg.toLowerCase().includes('already a member')) {
|
||||||
|
// Already a member is a successful state — just navigate to the space.
|
||||||
|
// Look up the space in the store by id; if not found (rare race), stay
|
||||||
|
// silent rather than block the user with a noisy error.
|
||||||
|
navigate(`/spaces/${payload.spaceId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setJoinError(msg || 'Failed to join');
|
||||||
setJoining(false);
|
setJoining(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user