From 869d2b55d4fb32b662f45ce58e9d8dc94e5d9b6a Mon Sep 17 00:00:00 2001 From: abbaszaid Date: Fri, 5 Jun 2026 10:48:45 -0700 Subject: [PATCH] test: add ShipmentCard component tests Add Jest setup and React Testing Library tests for the ShipmentCard component, covering rendering, status badges, navigation link, and optional field handling. Closes #908. Co-authored-by: Cursor --- .../shipment/shipment-card.test.tsx | 127 ++++++++++++++++++ frontend/jest.config.ts | 14 ++ frontend/jest.setup.ts | 1 + 3 files changed, 142 insertions(+) create mode 100644 frontend/components/shipment/shipment-card.test.tsx create mode 100644 frontend/jest.config.ts create mode 100644 frontend/jest.setup.ts diff --git a/frontend/components/shipment/shipment-card.test.tsx b/frontend/components/shipment/shipment-card.test.tsx new file mode 100644 index 00000000..855dda95 --- /dev/null +++ b/frontend/components/shipment/shipment-card.test.tsx @@ -0,0 +1,127 @@ +import { render, screen } from '@testing-library/react'; +import { ShipmentCard } from './shipment-card'; +import { Shipment, ShipmentStatus } from '../../types/shipment.types'; + +jest.mock('next/link', () => { + return function MockLink({ + children, + href, + ...props + }: { + children: React.ReactNode; + href: string; + className?: string; + }) { + return ( + + {children} + + ); + }; +}); + +const STATUS_BADGE_CLASSES: Record = { + [ShipmentStatus.PENDING]: 'bg-yellow-100', + [ShipmentStatus.ACCEPTED]: 'bg-blue-100', + [ShipmentStatus.IN_TRANSIT]: 'bg-indigo-100', + [ShipmentStatus.DELIVERED]: 'bg-teal-100', + [ShipmentStatus.COMPLETED]: 'bg-green-100', + [ShipmentStatus.CANCELLED]: 'bg-gray-100', + [ShipmentStatus.DISPUTED]: 'bg-red-100', +}; + +const STATUS_LABELS: Record = { + [ShipmentStatus.PENDING]: 'Pending', + [ShipmentStatus.ACCEPTED]: 'Accepted', + [ShipmentStatus.IN_TRANSIT]: 'In Transit', + [ShipmentStatus.DELIVERED]: 'Delivered', + [ShipmentStatus.COMPLETED]: 'Completed', + [ShipmentStatus.CANCELLED]: 'Cancelled', + [ShipmentStatus.DISPUTED]: 'Disputed', +}; + +function createMockShipment(overrides: Partial = {}): Shipment { + return { + id: 'shipment-123', + trackingNumber: 'FF-ABC123', + shipperId: 'shipper-1', + shipper: { + id: 'shipper-1', + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + }, + carrierId: null, + carrier: null, + origin: 'Lagos', + destination: 'Abuja', + cargoDescription: 'Electronics and spare parts', + weightKg: 500, + volumeCbm: 2.5, + price: 1500, + currency: 'USD', + status: ShipmentStatus.PENDING, + notes: null, + pickupDate: null, + estimatedDeliveryDate: '2026-07-15T00:00:00.000Z', + actualDeliveryDate: null, + createdAt: '2026-06-01T00:00:00.000Z', + updatedAt: '2026-06-01T00:00:00.000Z', + ...overrides, + }; +} + +describe('ShipmentCard', () => { + it('renders tracking number, origin, destination, price, and ETA date from props', () => { + render(); + + expect(screen.getByText('FF-ABC123')).toBeInTheDocument(); + expect(screen.getByText('Lagos → Abuja')).toBeInTheDocument(); + expect(screen.getByText('$1,500.00')).toBeInTheDocument(); + expect(screen.getByText(/ETA:/)).toBeInTheDocument(); + expect(screen.getByText(/Jul 14, 2026|Jul 15, 2026/)).toBeInTheDocument(); + expect(screen.getByText('Electronics and spare parts')).toBeInTheDocument(); + expect(screen.getByText('500 kg')).toBeInTheDocument(); + }); + + it.each([ + ShipmentStatus.PENDING, + ShipmentStatus.ACCEPTED, + ShipmentStatus.IN_TRANSIT, + ShipmentStatus.DELIVERED, + ShipmentStatus.COMPLETED, + ShipmentStatus.CANCELLED, + ])('renders the correct status badge for %s', (status) => { + render(); + + const badge = screen.getByText(STATUS_LABELS[status]); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveClass(STATUS_BADGE_CLASSES[status]); + }); + + it('links to the shipment detail page with the correct shipment ID', () => { + render(); + + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/shipments/abc-456'); + }); + + it('renders without crashing when optional cargo description is undefined', () => { + const shipment = createMockShipment(); + // Simulate missing optional display field at runtime + (shipment as { cargoDescription?: string }).cargoDescription = undefined; + + expect(() => render()).not.toThrow(); + expect(screen.getByRole('link')).toHaveAttribute('href', '/shipments/shipment-123'); + expect(screen.getByText('FF-ABC123')).toBeInTheDocument(); + }); + + it('renders without crashing when price is undefined', () => { + const shipment = createMockShipment(); + (shipment as { price?: number }).price = undefined; + + expect(() => render()).not.toThrow(); + expect(screen.getByRole('link')).toHaveAttribute('href', '/shipments/shipment-123'); + expect(screen.getByText('Lagos → Abuja')).toBeInTheDocument(); + }); +}); diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts new file mode 100644 index 00000000..82d5990b --- /dev/null +++ b/frontend/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from 'jest'; +import nextJest from 'next/jest.js'; + +const createJestConfig = nextJest({ + dir: './', +}); + +const config: Config = { + setupFilesAfterEnv: ['/jest.setup.ts'], + testEnvironment: 'jest-environment-jsdom', + testMatch: ['**/*.test.{ts,tsx}'], +}; + +export default createJestConfig(config); diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/frontend/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom';