Fix task and shift self-assignment features
This commit is contained in:
@@ -1,8 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ShiftCard } from '../shifts/shift-card';
|
||||
import { useSignUpShift, useCancelSignUp } from '@/hooks/useShifts';
|
||||
|
||||
vi.mock('@/hooks/useShifts', () => ({
|
||||
useSignUpShift: vi.fn(),
|
||||
useCancelSignUp: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ShiftCard', () => {
|
||||
const mockSignUp = vi.fn();
|
||||
const mockCancel = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useSignUpShift as ReturnType<typeof vi.fn>).mockReturnValue({ mutate: mockSignUp, isPending: false });
|
||||
(useCancelSignUp as ReturnType<typeof vi.fn>).mockReturnValue({ mutate: mockCancel, isPending: false });
|
||||
});
|
||||
|
||||
it('shows capacity correctly (2/3 spots filled)', () => {
|
||||
render(
|
||||
<ShiftCard
|
||||
@@ -13,6 +28,7 @@ describe('ShiftCard', () => {
|
||||
endTime: new Date(Date.now() + 200000).toISOString(),
|
||||
capacity: 3,
|
||||
currentSignups: 2,
|
||||
isSignedUp: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -29,6 +45,7 @@ describe('ShiftCard', () => {
|
||||
endTime: new Date(Date.now() + 200000).toISOString(),
|
||||
capacity: 3,
|
||||
currentSignups: 3,
|
||||
isSignedUp: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -46,10 +63,28 @@ describe('ShiftCard', () => {
|
||||
endTime: new Date(Date.now() - 100000).toISOString(),
|
||||
capacity: 3,
|
||||
currentSignups: 1,
|
||||
isSignedUp: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Past')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Sign Up' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows cancel sign-up button when signed up', () => {
|
||||
render(
|
||||
<ShiftCard
|
||||
shift={{
|
||||
id: '1',
|
||||
title: 'Signed Up Shift',
|
||||
startTime: new Date(Date.now() + 100000).toISOString(),
|
||||
endTime: new Date(Date.now() + 200000).toISOString(),
|
||||
capacity: 3,
|
||||
currentSignups: 1,
|
||||
isSignedUp: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Cancel Sign-up')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('ShiftDetailPage', () => {
|
||||
endTime: new Date(Date.now() + 200000).toISOString(),
|
||||
capacity: 3,
|
||||
signups: [{ id: 's1', memberId: 'other-user' }],
|
||||
isSignedUp: false,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -77,6 +78,7 @@ describe('ShiftDetailPage', () => {
|
||||
endTime: new Date(Date.now() + 200000).toISOString(),
|
||||
capacity: 3,
|
||||
signups: [{ id: 's1', memberId: 'user-123' }],
|
||||
isSignedUp: true,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -103,6 +105,7 @@ describe('ShiftDetailPage', () => {
|
||||
endTime: new Date(Date.now() + 200000).toISOString(),
|
||||
capacity: 3,
|
||||
signups: [],
|
||||
isSignedUp: false,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import TaskDetailPage from '@/app/(protected)/tasks/[id]/page';
|
||||
import { useTask, useUpdateTask } from '@/hooks/useTasks';
|
||||
import { useTask, useUpdateTask, useAssignTask, useUnassignTask } from '@/hooks/useTasks';
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: vi.fn(() => ({
|
||||
@@ -21,21 +21,34 @@ vi.mock('next-auth/react', () => ({
|
||||
vi.mock('@/hooks/useTasks', () => ({
|
||||
useTask: vi.fn(),
|
||||
useUpdateTask: vi.fn(),
|
||||
useAssignTask: vi.fn(),
|
||||
useUnassignTask: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('TaskDetailPage', () => {
|
||||
const mockMutate = vi.fn();
|
||||
const mockUpdate = vi.fn();
|
||||
const mockAssign = vi.fn();
|
||||
const mockUnassign = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useUpdateTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
mutate: mockUpdate,
|
||||
isPending: false,
|
||||
});
|
||||
(useAssignTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutate: mockAssign,
|
||||
isPending: false,
|
||||
});
|
||||
(useUnassignTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutate: mockUnassign,
|
||||
isPending: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows valid transitions for Open status', async () => {
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { id: '1', title: 'Task 1', status: 'Open', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
data: { id: '1', title: 'Task 1', status: 'Open', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01', isAssignedToMe: false },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -52,7 +65,7 @@ describe('TaskDetailPage', () => {
|
||||
|
||||
it('shows valid transitions for InProgress status', async () => {
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { id: '1', title: 'Task 1', status: 'InProgress', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
data: { id: '1', title: 'Task 1', status: 'InProgress', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01', isAssignedToMe: false },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -68,7 +81,7 @@ describe('TaskDetailPage', () => {
|
||||
|
||||
it('shows valid transitions for Review status (including back transition)', async () => {
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { id: '1', title: 'Task 1', status: 'Review', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
data: { id: '1', title: 'Task 1', status: 'Review', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01', isAssignedToMe: false },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -91,7 +104,8 @@ describe('TaskDetailPage', () => {
|
||||
assigneeId: null,
|
||||
description: 'Desc',
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-01-01'
|
||||
updatedAt: '2024-01-01',
|
||||
isAssignedToMe: false
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -105,8 +119,7 @@ describe('TaskDetailPage', () => {
|
||||
expect(screen.getByText('Assign to Me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls updateTask with assigneeId when Assign to Me clicked', async () => {
|
||||
const mockMutate = vi.fn();
|
||||
it('calls assignTask with task id when Assign to Me clicked', async () => {
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
id: '1',
|
||||
@@ -115,15 +128,12 @@ describe('TaskDetailPage', () => {
|
||||
assigneeId: null,
|
||||
description: 'Desc',
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-01-01'
|
||||
updatedAt: '2024-01-01',
|
||||
isAssignedToMe: false
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
(useUpdateTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: false,
|
||||
});
|
||||
|
||||
const params = Promise.resolve({ id: '1' });
|
||||
await act(async () => {
|
||||
@@ -135,9 +145,37 @@ describe('TaskDetailPage', () => {
|
||||
button.click();
|
||||
});
|
||||
|
||||
expect(mockMutate).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
data: { assigneeId: 'user-123' },
|
||||
expect(mockAssign).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('renders Unassign button and calls unassignTask when clicked', async () => {
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
id: '1',
|
||||
title: 'Task 1',
|
||||
status: 'Assigned',
|
||||
assigneeId: 'some-member-id',
|
||||
description: 'Desc',
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-01-01',
|
||||
isAssignedToMe: true
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const params = Promise.resolve({ id: '1' });
|
||||
await act(async () => {
|
||||
render(<TaskDetailPage params={params} />);
|
||||
});
|
||||
|
||||
const button = screen.getByText('Unassign');
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
button.click();
|
||||
});
|
||||
|
||||
expect(mockUnassign).toHaveBeenCalledWith('1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,13 +3,16 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ShiftListItemDto } from '@/hooks/useShifts';
|
||||
import { ShiftListItemDto, useSignUpShift, useCancelSignUp } from '@/hooks/useShifts';
|
||||
|
||||
interface ShiftCardProps {
|
||||
shift: ShiftListItemDto;
|
||||
}
|
||||
|
||||
export function ShiftCard({ shift }: ShiftCardProps) {
|
||||
const signUpMutation = useSignUpShift();
|
||||
const cancelMutation = useCancelSignUp();
|
||||
|
||||
const capacityPercentage = (shift.currentSignups / shift.capacity) * 100;
|
||||
const isFull = shift.currentSignups >= shift.capacity;
|
||||
const isPast = new Date(shift.startTime) < new Date();
|
||||
@@ -39,8 +42,15 @@ export function ShiftCard({ shift }: ShiftCardProps) {
|
||||
<Link href={`/shifts/${shift.id}`}>
|
||||
<Button variant="outline" size="sm">View Details</Button>
|
||||
</Link>
|
||||
{!isPast && !isFull && (
|
||||
<Button size="sm">Sign Up</Button>
|
||||
{!isPast && !isFull && !shift.isSignedUp && (
|
||||
<Button size="sm" onClick={() => signUpMutation.mutate(shift.id)} disabled={signUpMutation.isPending}>
|
||||
{signUpMutation.isPending ? 'Signing up...' : 'Sign Up'}
|
||||
</Button>
|
||||
)}
|
||||
{!isPast && shift.isSignedUp && (
|
||||
<Button variant="outline" size="sm" onClick={() => cancelMutation.mutate(shift.id)} disabled={cancelMutation.isPending}>
|
||||
{cancelMutation.isPending ? 'Canceling...' : 'Cancel Sign-up'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user