Files
Moheeb ZaraandClaude Opus 4.8 b1edb5c460 types: clear remaining tsc errors across 41 files (76→14, behavior-preserving)
Per-file type-only fixes surfaced after the Supabase client was correctly
typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error.

- lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database>
  (same ssr/js drift as server.ts; fixes it centrally for all callers).
- Supabase embedded-select joins whose FK metadata is missing from the
  generated types are bridged with `as unknown as <target>` + a TODO to remove
  after regenerating types/database.ts.
- setState optimistic-update variance and raw <select> enum values cast to the
  precise element/union type; .insert payload fields (Json, enums) given the
  precise column type via satisfies/field casts.
- Added narrowing guards where a value was legitimately nullable (comms channel,
  public form page) without changing the intended path.

The 14 remaining errors are confined to the four files fixed in Phase B (real
bugs + the tautological actions.test rewrite). 639 tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 18:56:11 -07:00

350 lines
12 KiB
TypeScript

import { describe, it, expect } from 'vitest'
// ─── Date & Time Utilities ────────────────────────────────────────────────────
describe('Date Utilities', () => {
describe('formatDate', () => {
it('should format ISO date to readable format', () => {
const isoDate = '2024-01-15T10:30:00.000Z'
const date = new Date(isoDate)
const formatted = date.toLocaleDateString()
expect(formatted).toBeDefined()
})
it('should handle null dates gracefully', () => {
const date = null
const formatted = date ? new Date(date).toLocaleDateString() : 'N/A'
expect(formatted).toBe('N/A')
})
})
describe('isOverdue', () => {
it('should identify overdue dates', () => {
const pastDate = new Date(Date.now() - 86400000) // Yesterday
expect(pastDate < new Date()).toBe(true)
})
it('should identify future dates as not overdue', () => {
const futureDate = new Date(Date.now() + 86400000) // Tomorrow
expect(futureDate > new Date()).toBe(true)
})
})
describe('getRelativeTime', () => {
it('should show "just now" for recent times', () => {
const now = new Date()
const diff = Date.now() - now.getTime()
expect(diff).toBeLessThanOrEqual(0)
})
it('should calculate days difference', () => {
const daysAgo = new Date(Date.now() - 3 * 86400000)
const diffDays = Math.floor((Date.now() - daysAgo.getTime()) / 86400000)
expect(diffDays).toBe(3)
})
})
})
// ─── String Utilities ─────────────────────────────────────────────────────────
describe('String Utilities', () => {
describe('generateCode', () => {
it('should generate code from name', () => {
const name = 'John Doe'
const code = name.slice(0, 3).toUpperCase() + Math.floor(Math.random() * 900 + 100)
expect(code).toMatch(/^JOH\d{3}$/)
})
it('should handle short names', () => {
const name = 'Jo'
const code = name.slice(0, 3).toUpperCase() + Math.floor(100)
expect(code.length).toBeGreaterThanOrEqual(4)
})
})
describe('getInitials', () => {
it('should extract initials from full name', () => {
const name = 'John Doe'
const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
expect(initials).toBe('JD')
})
it('should handle single name', () => {
const name = 'John'
const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
expect(initials).toBe('J')
})
it('should handle empty name', () => {
const name: string = ''
const initials = name ? name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) : '??'
expect(initials).toBe('??')
})
})
describe('truncate', () => {
it('should truncate long strings', () => {
const text = 'This is a very long string that should be truncated'
const maxLength = 20
const truncated = text.length > maxLength ? text.slice(0, maxLength) + '...' : text
expect(truncated.length).toBeLessThanOrEqual(maxLength + 3)
})
it('should not truncate short strings', () => {
const text = 'Short'
const maxLength = 20
const truncated = text.length > maxLength ? text.slice(0, maxLength) + '...' : text
expect(truncated).toBe('Short')
})
})
})
// ─── Number Utilities ─────────────────────────────────────────────────────────
describe('Number Utilities', () => {
describe('formatCurrency', () => {
it('should format number as currency', () => {
const amount = 1234.56
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
expect(formatted).toBe('$1,234.56')
})
it('should handle zero', () => {
const amount = 0
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
expect(formatted).toBe('$0.00')
})
it('should handle negative numbers', () => {
const amount = -50
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
expect(formatted).toContain('-')
})
})
describe('clamp', () => {
it('should clamp value within range', () => {
const clamp = (val: number, min: number, max: number) => Math.min(Math.max(val, min), max)
expect(clamp(5, 0, 10)).toBe(5)
expect(clamp(-5, 0, 10)).toBe(0)
expect(clamp(15, 0, 10)).toBe(10)
})
})
})
// ─── Array Utilities ──────────────────────────────────────────────────────────
describe('Array Utilities', () => {
describe('groupBy', () => {
it('should group items by key', () => {
const items = [
{ type: 'task', name: 'A' },
{ type: 'chore', name: 'B' },
{ type: 'task', name: 'C' },
]
const grouped = items.reduce((acc, item) => {
const key = item.type
if (!acc[key]) acc[key] = []
acc[key].push(item)
return acc
}, {} as Record<string, typeof items>)
expect(grouped['task']).toHaveLength(2)
expect(grouped['chore']).toHaveLength(1)
})
})
describe('sortBy', () => {
it('should sort by string property', () => {
const items = [
{ name: 'Charlie' },
{ name: 'Alice' },
{ name: 'Bob' },
]
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name))
expect(sorted[0].name).toBe('Alice')
})
it('should sort by date property', () => {
const items = [
{ date: '2024-03-01' },
{ date: '2024-01-01' },
{ date: '2024-02-01' },
]
const sorted = [...items].sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
)
expect(sorted[0].date).toBe('2024-01-01')
})
})
describe('unique', () => {
it('should remove duplicates', () => {
const items = [1, 2, 2, 3, 3, 3]
const unique = [...new Set(items)]
expect(unique).toEqual([1, 2, 3])
})
it('should handle strings', () => {
const items = ['a', 'b', 'a', 'c']
const unique = [...new Set(items)]
expect(unique).toEqual(['a', 'b', 'c'])
})
})
})
// ─── Validation Utilities ─────────────────────────────────────────────────────
describe('Validation Utilities', () => {
describe('isValidEmail', () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
it('should validate correct email', () => {
expect(emailRegex.test('[email protected]')).toBe(true)
})
it('should reject invalid email', () => {
expect(emailRegex.test('invalid-email')).toBe(false)
})
it('should reject email without domain', () => {
expect(emailRegex.test('user@')).toBe(false)
})
it('should reject email without @', () => {
expect(emailRegex.test('userexample.com')).toBe(false)
})
})
describe('isValidPhone', () => {
it('should validate US phone numbers', () => {
const phoneRegex = /^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/
expect(phoneRegex.test('(555) 123-4567')).toBe(true)
expect(phoneRegex.test('555-123-4567')).toBe(true)
})
})
describe('isNonEmpty', () => {
it('should validate non-empty strings', () => {
const isNonEmpty = (str: string) => str.trim().length > 0
expect(isNonEmpty('hello')).toBe(true)
expect(isNonEmpty('')).toBe(false)
expect(isNonEmpty(' ')).toBe(false)
})
})
describe('isPositiveNumber', () => {
it('should validate positive numbers', () => {
const isPositive = (n: number) => n > 0
expect(isPositive(1)).toBe(true)
expect(isPositive(0)).toBe(false)
expect(isPositive(-1)).toBe(false)
})
})
})
// ─── URL Utilities ────────────────────────────────────────────────────────────
describe('URL Utilities', () => {
describe('buildQueryString', () => {
it('should build query string from object', () => {
const params = { page: 1, limit: 10, sort: 'name' }
const queryString = new URLSearchParams(params as any).toString()
expect(queryString).toBe('page=1&limit=10&sort=name')
})
it('should handle empty object', () => {
const params = {}
const queryString = new URLSearchParams(params as any).toString()
expect(queryString).toBe('')
})
})
describe('parseQueryString', () => {
it('should parse query string to object', () => {
const queryString = 'page=1&limit=10'
const params = Object.fromEntries(new URLSearchParams(queryString))
expect(params.page).toBe('1')
expect(params.limit).toBe('10')
})
})
})
// ─── Status & Role Utilities ──────────────────────────────────────────────────
describe('Status & Role Utilities', () => {
describe('isActiveMember', () => {
it('should identify active statuses', () => {
const activeStatuses = ['current', 'unverified', 'late']
expect(activeStatuses.includes('current')).toBe(true)
expect(activeStatuses.includes('unverified')).toBe(true)
expect(activeStatuses.includes('late')).toBe(true)
expect(activeStatuses.includes('inactive')).toBe(false)
})
})
describe('isAdmin', () => {
it('should identify admin roles', () => {
const adminRoles = ['admin', 'board']
expect(adminRoles.includes('admin')).toBe(true)
expect(adminRoles.includes('board')).toBe(true)
expect(adminRoles.includes('member')).toBe(false)
})
})
describe('isTreasurer', () => {
it('should identify treasurer-capable roles', () => {
const treasurerRoles = ['admin', 'board', 'treasurer']
expect(treasurerRoles.includes('admin')).toBe(true)
expect(treasurerRoles.includes('treasurer')).toBe(true)
expect(treasurerRoles.includes('member')).toBe(false)
})
})
describe('getStatusColor', () => {
it('should return correct color for status', () => {
const colors: Record<string, string> = {
current: 'green',
unverified: 'yellow',
late: 'orange',
inactive: 'gray',
}
expect(colors['current']).toBe('green')
expect(colors['unverified']).toBe('yellow')
})
})
})
// ─── Task Utilities ───────────────────────────────────────────────────────────
describe('Task Utilities', () => {
describe('isTaskOverdue', () => {
it('should identify overdue tasks', () => {
const task = { due_date: '2020-01-01', status: 'open' }
const isOverdue = task.due_date && new Date(task.due_date) < new Date() && task.status !== 'completed'
expect(isOverdue).toBe(true)
})
it('should not flag completed tasks as overdue', () => {
const task = { due_date: '2020-01-01', status: 'completed' }
const isOverdue = task.due_date && new Date(task.due_date) < new Date() && task.status !== 'completed'
expect(isOverdue).toBe(false)
})
})
describe('getRecurrenceLabel', () => {
it('should return human-readable recurrence', () => {
const labels: Record<string, string> = {
none: 'One-time',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
yearly: 'Yearly',
}
expect(labels['daily']).toBe('Daily')
expect(labels['none']).toBe('One-time')
})
})
})