← Tasks
[TEST-01] Basic unit tests for utility functions and components
completedcode_genP0
Description
## TEST-01: Basic unit tests for utility functions and components
Section: Testing | Milestone: M1 | Owner: None
### Failing Tests
- **unit_test_files_exist** (grep): Pattern `\.test\.(ts|tsx)$` in `src/**/*.test.ts*` (need 5+ matches)
### Acceptance Criteria
- File matching `src/**/*.test.ts*` must contain pattern `\.test\.(ts|tsx)$`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
23d ago
Completed
19d ago
Quality
Overall
79%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 20,
"estimated_usd": 0.09081,
"output_tokens": 6050
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 62191,
"response_text": "I'll help you create a comprehensive test suite for basic utility functions and components. Let me provide examples for common scenarios:\n\n## Utility Functions Tests\n\n### 1. String Utilities\n\n```javascript\n// utils/string.js\nexport const capitalize = (str) => {\n if (!str || typeof str !== 'string') return '';\n return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();\n};\n\nexport const truncate = (str, length = 50) => {\n if (!str || typeof str !== 'string') return '';\n return str.length > length ? str.slice(0, length) + '...' : str;\n};\n\nexport const slugify = (str) => {\n if (!str || typeof str !== 'string') return '';\n return str\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_-]+/g, '-')\n .replace(/^-+|-+$/g, '');\n};\n```\n\n```javascript\n// utils/__tests__/string.test.js\nimport { capitalize, truncate, slugify } from '../string';\n\ndescribe('String Utilities', () => {\n describe('capitalize', () => {\n it('should capitalize the first letter', () => {\n expect(capitalize('hello')).toBe('Hello');\n expect(capitalize('HELLO')).toBe('Hello');\n expect(capitalize('hELLO')).toBe('Hello');\n });\n\n it('should handle edge cases', () => {\n expect(capitalize('')).toBe('');\n expect(capitalize(null)).toBe('');\n expect(capitalize(undefined)).toBe('');\n expect(capitalize(123)).toBe('');\n });\n\n it('should handle single character', () => {\n expect(capitalize('a')).toBe('A');\n expect(capitalize('A')).toBe('A');\n });\n });\n\n describe('truncate', () => {\n it('should truncate strings longer than specified length', () => {\n const longString = 'This is a very long string that should be truncated';\n expect(truncate(longString, 20)).toBe('This is a very long ...');\n });\n\n it('should not truncate strings shorter than specified length', () => {\n const shortString = 'Short';\n expect(truncate(shortString, 20)).toBe('Short');\n });\n\n it('should use default length when not specified', () => {\n const longString = 'a'.repeat(60);\n expect(truncate(longString)).toBe('a'.repeat(50) + '...');\n });\n\n it('should handle edge cases', () => {\n expect(truncate('')).toBe('');\n expect(truncate(null)).toBe('');\n expect(truncate(undefined)).toBe('');\n });\n });\n\n describe('slugify', () => {\n it('should convert string to URL-friendly slug', () => {\n expect(slugify('Hello World')).toBe('hello-world');\n expect(slugify('Hello, World!')).toBe('hello-world');\n expect(slugify(' Multiple Spaces ')).toBe('multiple-spaces');\n });\n\n it('should handle special characters', () => {\n expect(slugify('Hello@World#123')).toBe('helloworld123');\n expect(slugify('Test-String_With-Dashes')).toBe('test-string-with-dashes');\n });\n\n it('should handle edge cases', () => {\n expect(slugify('')).toBe('');\n expect(slugify(' ')).toBe('');\n expect(slugify('!!!')).toBe('');\n });\n });\n});\n```\n\n### 2. Array Utilities\n\n```javascript\n// utils/array.js\nexport const chunk = (array, size) => {\n if (!Array.isArray(array) || size <= 0) return [];\n const chunks = [];\n for (let i = 0; i < array.length; i += size) {\n chunks.push(array.slice(i, i + size));\n }\n return chunks;\n};\n\nexport const unique = (array) => {\n if (!Array.isArray(array)) return [];\n return [...new Set(array)];\n};\n\nexport const groupBy = (array, key) => {\n if (!Array.isArray(array)) return {};\n return array.reduce((groups, item) => {\n const group = typeof key === 'function' ? key(item) : item[key];\n groups[group] = groups[group] || [];\n groups[group].push(item);\n return groups;\n }, {});\n};\n```\n\n```javascript\n// utils/__tests__/array.test.js\nimport { chunk, unique, groupBy } from '../array';\n\ndescribe('Array Utilities', () => {\n describe('chunk', () => {\n it('should split array into chunks of specified size', () => {\n expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);\n expect(chunk([1, 2, 3, 4, 5, 6], 3)).toEqual([[1, 2, 3], [4, 5, 6]]);\n });\n\n it('should handle edge cases', () => {\n expect(chunk([], 2)).toEqual([]);\n expect(chunk([1, 2, 3], 0)).toEqual([]);\n expect(chunk([1, 2, 3], -1)).toEqual([]);\n expect(chunk('not an array', 2)).toEqual([]);\n });\n\n it('should handle chunk size larger than array', () => {\n expect(chunk([1, 2], 5)).toEqual([[1, 2]]);\n });\n });\n\n describe('unique', () => {\n it('should remove duplicate values', () => {\n expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);\n expect(unique(['a', 'b', 'a', 'c'])).toEqual(['a', 'b', 'c']);\n });\n\n it('should handle empty array', () => {\n expect(unique([])).toEqual([]);\n });\n\n it('should handle non-array input', () => {\n expect(unique('not an array')).toEqual([]);\n expect(unique(null)).toEqual([]);\n });\n\n it('should preserve order of first occurrence', () => {\n expect(unique([3, 1, 2, 1, 3])).toEqual([3, 1, 2]);\n });\n });\n\n describe('groupBy', () => {\n const users = [\n { name: 'John', department: 'IT' },\n { name: 'Jane', department: 'HR' },\n { name: 'Bob', department: 'IT' },\n ];\n\n it('should group array by property key', () => {\n const result = groupBy(users, 'department');\n expect(result).toEqual({\n IT: [\n { name: 'John', department: 'IT' },\n { name: 'Bob', department: 'IT' },\n ],\n HR: [{ name: 'Jane', department: 'HR' }],\n });\n });\n\n it('should group array by function', () => {\n const numbers = [1, 2, 3, 4, 5, 6];\n const result = groupBy(numbers, (n) => n % 2 === 0 ? 'even' : 'odd');\n expect(result).toEqual({\n odd: [1, 3, 5],\n even: [2, 4, 6],\n });\n });\n\n it('should handle edge cases', () => {\n expect(groupBy([], 'key')).toEqual({});\n expect(groupBy('not an array', 'key')).toEqual({});\n });\n });\n});\n```\n\n### 3. Date Utilities\n\n```javascript\n// utils/date.js\nexport const formatDate = (date, format = 'YYYY-MM-DD') => {\n if (!date || isNaN(new Date(date))) return '';\n \n const d = new Date(date);\n const year = d.getFullYear();\n const month = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n \n return format\n .replace('YYYY', year)\n .replace('MM', month)\n .replace('DD', day);\n};\n\nexport const isValidDate = (date) => {\n return date instanceof Date && !isNaN(date);\n};\n\nexport const daysBetween = (date1, date2) => {\n const oneDay = 24 * 60 * 60 * 1000;\n const firstDate = new Date(date1);\n const secondDate = new Date(date2);\n \n if (!isValidDate(firstDate) || !isValidDate(secondDate)) {\n return null;\n }\n \n return Math.round(Math.abs((firstDate - secondDate) / oneDay));\n};\n```\n\n```javascript\n// utils/__tests__/date.test.js\nimport { formatDate, isValidDate, daysBetween } from '../date';\n\ndescribe('Date Utilities', () => {\n describe('formatDate', () => {\n it('should format date with default format', () => {\n const date = new Date('2023-12-25');\n expect(formatDate(date)).toBe('2023-12-25');\n });\n\n it('should format date with custom format', () => {\n const date = new Date('2023-12-25');\n expect(formatDate(date, 'DD/MM/YYYY')).toBe('25/12/2023');\n expect(formatDate(date, 'MM-DD-YYYY')).toBe('12-25-2023');\n });\n\n it('should handle invalid dates', () => {\n expect(formatDate('invalid')).toBe('');\n expect(formatDate(null)).toBe('');\n expect(formatDate(undefined)).toBe('');\n });\n });\n\n describe('isValidDate', () => {\n it('should return true for valid dates', () => {\n expect(isValidDate(new Date())).toBe(true);\n expect(isValidDate(new Date('2023-12-25'))).toBe(true);\n });\n\n it('should return false for invalid dates', () => {\n expect(isValidDate(new Date('invalid'))).toBe(false);\n expect(isValidDate('2023-12-25')).toBe(false);\n expect(isValidDate(null)).toBe(false);\n });\n });\n\n describe('daysBetween', () => {\n it('should calculate days between two dates', () => {\n const date1 = new Date('2023-12-25');\n const date2 = new Date('2023-12-30');\n expect(daysBetween(date1, date2)).toBe(5);\n });\n\n it('should return absolute difference', () => {\n const date1 = new Date('2023-12-30');\n const date2 = new Date('2023-12-25');\n expect(daysBetween(date1, date2)).toBe(5);\n });\n\n it('should handle invalid dates', () => {\n expect(daysBetween('invalid', new Date())).toBeNull();\n expect(daysBetween(new Date(), 'invalid')).toBeNull();\n });\n });\n});\n```\n\n## Component Tests\n\n### 1. Button Component\n\n```jsx\n// components/Button.jsx\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst Button = ({ \n children, \n onClick, \n disabled = false, \n variant = 'primary',\n size = 'medium',\n type = 'button',\n ...props \n}) => {\n const baseClass = 'btn';\n const variantClass = `btn--${variant}`;\n const sizeClass = `btn--${size}`;\n \n const className = [baseClass, variantClass, sizeClass]\n .filter(Boolean)\n .join(' ');\n\n return (\n <button\n className={className}\n onClick={onClick}\n disabled={disabled}\n type={type}\n {...props}\n >\n {children}\n </button>\n );\n};\n\nButton.propTypes = {\n children: PropTypes.node.isRequired,\n onClick: PropTypes.func,\n disabled: PropTypes.bool,\n variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),\n size: PropTypes.oneOf(['small', 'medium', 'large']),\n type: PropTypes.oneOf(['button', 'submit', 'reset']),\n};\n\nexport default Button;\n```\n\n```javascript\n// components/__tests__/Button.test.jsx\nimport React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport '@testing-library/jest-dom';\nimport Button from '../Button';\n\ndescribe('Button Component', () => {\n it('should render with children', () => {\n render(<Button>Click me</Button>);\n expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();\n });\n\n it('should handle click events', () => {\n const handleClick = jest.fn();\n render(<Button onClick={handleClick}>Click me</Button>);\n \n fireEvent.click(screen.getByRole('button'));\n expect(handleClick).toHaveBeenCalledTimes(1);\n });\n\n it('should be disabled when disabled prop is true', () => {\n render(<Button disabled>Click me</Button>);\n expect(screen.getByRole('button')).toBeDisabled();\n });\n\n it('should not call onClick when disabled', () => {\n const handleClick = jest.fn();\n render(<Button onClick={handleClick} disabled>Click me</Button>);\n \n fireEvent.click(screen.getByRole('button'));\n expect(handleClick).not.toHaveBeenCalled();\n });\n\n it('should apply correct CSS classes', () => {\n render(<Button variant=\"secondary\" size=\"large\">Click me</Button>);\n const button = screen.getByRole('button');\n \n expect(button).toHaveClass('btn');\n expect(button).toHaveClass('btn--secondary');\n expect(button).toHaveClass('btn--large');\n });\n\n it('should apply default props', () => {\n render(<Button>Click me</Button>);\n const button = screen.getByRole('button');\n \n expect(button).toHaveClass('btn--primary');\n expect(button).toHaveClass('btn--medium');\n expect(button).toHaveAttribute('type', 'button');\n });\n\n it('should forward additional props', () => {\n render(<Button data-testid=\"custom-button\" aria-label=\"Custom button\">Click me</Button>);\n const button = screen.getByTestId('custom-button');\n \n expect(button).toHaveAttribute('aria-label', 'Custom button');\n });\n});\n```\n\n### 2. Input Component\n\n```jsx\n// components/Input.jsx\nimport React, { forwardRef } from 'react';\nimport PropTypes from 'prop-types';\n\nconst Input = forwardRef(({ \n label,\n error,\n helperText,\n required = false,\n disabled = false,\n type = 'text',\n className = '',\n ...props \n}, ref) => {\n const inputId = props.id || `input-${Math.random().toString(36).substr(2, 9)}`;\n \n return (\n <div className={`input-group ${className}`}>\n {label && (\n <label htmlFor={inputId} className=\"input-label\">\n {label}\n {required && <span className=\"required\">*</span>}\n </label>\n )}\n \n <input\n ref={ref}\n id={inputId}\n type={type}\n disabled={disabled}\n className={`input ${error ? 'input--error' : ''}`}\n aria-invalid={error ? 'true' : 'false'}\n aria-describedby={error ? `${inputId}-error` : undefined}\n {...props}\n />\n \n {error && (\n <span id={`${inputId}-error`} className=\"input-error\" role=\"alert\">\n {error}\n </span>\n )}\n \n {helperText && !error && (\n <span className=\"input-helper\">{helperText}</span>\n )}\n </div>\n );\n});\n\nInput.displayName = 'Input';\n\nInput.propTypes = {\n label: PropTypes.string,\n error: PropTypes.string,\n helperText: PropTypes.string,\n required: PropTypes.bool,\n disabled: PropTypes.bool,\n type: PropTypes.string,\n className: PropTypes.string,\n id: PropTypes.string,\n};\n\nexport default Input;\n```\n\n```javascript\n// components/__tests__/Input.test.jsx\nimport React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport '@testing-library/jest-dom';\nimport Input from '../Input';\n\ndescribe('Input Component', () => {\n it('should render input field', () => {\n render(<Input />);\n expect(screen.getByRole('textbox')).toBeInTheDocument();\n });\n\n it('should render with label', () => {\n render(<Input label=\"Username\" />);\n expect(screen.getByLabelText('Username')).toBeInTheDocument();\n });\n\n it('should show required indicator when required', () => {\n render(<Input label=\"Username\" required />);\n expect(screen.getByText('*')).toBeInTheDocument();\n });\n\n it('should display error message', () => {\n render(<Input label=\"Email\" error=\"Invalid email format\" />);\n const errorMessage = screen.getByRole('alert');\n expect(errorMessage).toHaveTextContent('Invalid email format');\n });\n\n it('should display helper text when no error', () => {\n render(<Input label=\"Password\" helperText=\"Must be 8+ characters\" />);\n expect(screen.getByText('Must be 8+ characters')).toBeInTheDocument();\n });\n\n it('should not display helper text when error is present', () => {\n render(\n <Input \n label=\"Password\" \n helperText=\"Must be 8+ characters\" \n error=\"Password too short\" \n />\n );\n expect(screen.queryByText('Must be 8+ characters')).not.toBeInTheDocument();\n expect(screen.getByText('Password too short')).toBeInTheDocument();\n });\n\n it('should be disabled when disabled prop is true', () => {\n render(<Input disabled />);\n expect(screen.getByRole('textbox')).toBeDisabled();\n });\n\n it('should handle different input types', () => {\n render(<Input type=\"email\" />);\n expect(screen.getByRole('textbox')).toHaveAttribute('type', 'email');\n });\n\n it('should apply error styling', () => {\n render(<Input error=\"Error message\" />);\n const input = screen.getByRole('textbox');\n expect(input).toHaveClass('input--error');\n expect(input).toHaveAttribute('aria-invalid', 'true');\n });\n\n it('should forward ref', () => {\n const ref = React.createRef();\n render(<Input ref={ref} />);\n expect(ref.current).toBeInstanceOf(HTMLInputElement);\n });\n\n it('should handle value changes', () => {\n const handleChange = jest.fn();\n render(<Input onChange={handleChange} />);\n \n const input = screen.getByRole('textbox');\n fireEvent.change(input, { target: { value: 'test value' } });\n \n expect(handleChange).toHaveBeenCalled();\n expect(input).toHaveValue('test value');\n });\n});\n```\n\n### 3. Test Configuration Files\n\n```javascript\n// jest.config.js\nmodule.exports = {\n testEnvironment: 'jsdom',\n setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],\n moduleNameMapping: {\n '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy',\n '^@/(.*)$': '<rootDir>/src/$1',\n },\n collectCoverageFrom: [\n 'src/**/*.{js,jsx}',\n '!src/index.js',\n '!src/reportWebVitals.js',\n '!src/**/*.stories.{js,jsx}',\n ],\n coverageThreshold: {\n global: {\n branches: 80,\n functions: 80,\n lines: 80,\n statements: 80,\n },\n },\n testMatch: [\n '<rootDir>/src/**/__tests__/**/*.{js,jsx}',\n '<rootDir>/src/**/*.{test,spec}.{js,jsx}',\n ],\n};\n```\n\n```javascript\n// src/setupTests.js\nimport '@testing-library/jest-dom';\n\n// Mock console methods in tests\nglobal.console = {\n ...console,\n warn: jest.fn(),\n error: jest.fn(),\n};\n\n// Mock IntersectionObserver\nglobal.IntersectionObserver = class IntersectionObserver {\n constructor() {}\n disconnect() {}\n observe() {}\n unobserve() {}\n};\n\n// Mock window.matchMedia\nObject.defineProperty(window, 'matchMedia', {\n writable: true,\n value: jest.fn().mockImplementation(query => ({\n matches: false,\n media: query,\n onchange: null,\n addListener: jest.fn(),\n removeListener: jest.fn(),\n addEventListener: jest.fn(),\n removeEventListener: jest.fn(),\n dispatchEvent: jest.fn(),\n })),\n});\n```\n\n### 4. Test Helper Utilities\n\n```javascript\n// src/test-utils.jsx\nimport React from 'react';\nimport { render } from '@testing-library/react';\nimport { BrowserRouter } from 'react-router-dom';\n\n// Custom render function that includes providers\nconst customRender = (ui, options = {}) => {\n const Wrapper = ({ children }) => (\n <BrowserRouter>\n {children}\n </BrowserRouter>\n );\n\n return render(ui, { wrapper: Wrapper, ...options });\n};\n\n// Common test data factories\nexport const createUser = (overrides = {}) => ({\n id: 1,\n name: 'John Doe',\n email: 'john@example.com',\n role: 'user',\n ...overrides,\n});\n\nexport const createProduct = (overrides = {}) => ({\n id: 1,\n name: 'Test Product',\n price: 99.99,\n description: 'A test product',\n inStock: true,\n ...overrides,\n});\n\n// Mock API responses\nexport const mockApiResponse = (data, status = 200) => ({\n ok: status >= 200 && status < 300,\n status,\n json: () => Promise.resolve(data),\n});\n\n// Export everything from @testing-library/react\nexport * from '@testing-library/react';\n\n// Override the default render method\nexport { customRender as render };\n```\n\nThis test suite provides comprehensive coverage for:\n\n1. **Utility Functions**: String, array, and date manipulation functions\n2. **React Components**: Button and Input components with various props and states\n3. **Test Configuration**: Jest setup and testing utilities\n4. **Best Practices**: Proper mocking, accessibility testing, and edge case handling\n\nEach test follows the AAA pattern (Arrange, Act, Assert) and includes both happy path and edge case scenarios."
}Task ID: d7c3082e-4918-4963-877f-c103f1f72a89