From 941949cdeb9569b80496a7d471842436069b1584 Mon Sep 17 00:00:00 2001 From: Sehyun Chung Date: Mon, 18 Aug 2025 11:26:07 +0900 Subject: [PATCH] feat: Standardize React imports and enhance UI testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix React import patterns across 14 files to use namespace imports - Add ESLint rule to enforce React namespace imports - Create comprehensive test suites for Button, Card, and Input components - Update TypeScript and package configuration for testing - Exclude test files from TypeScript compilation πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../document_symbols_cache_v23-06-25.pkl | Bin 0 -> 200719 bytes .serena/memories/architecture_patterns.md | 279 ++++++++++++++++++ .serena/memories/code_style_conventions.md | 208 +++++++++++++ .serena/memories/project_overview.md | 62 ++++ .serena/memories/serena_code_review_2025.md | 120 ++++++++ .serena/memories/suggested_commands.md | 167 +++++++++++ .serena/memories/task_completion_checklist.md | 183 ++++++++++++ .serena/memories/tech_stack.md | 77 +++++ .serena/project.yml | 68 +++++ apps/mobile/app/(tabs)/_layout.tsx | 2 +- apps/mobile/app/auth.tsx | 2 +- apps/mobile/components/AppNavigator.tsx | 2 +- .../components/SubmissionCompleteMessage.tsx | 2 +- eslint.config.mjs | 12 + packages/ui/package.json | 5 +- .../action-card/action-card.native.tsx | 2 +- .../ui/src/components/alert/alert.native.tsx | 2 +- .../ui/src/components/badge/badge.native.tsx | 2 +- .../ui/src/components/button/button.test.tsx | 91 ++++++ packages/ui/src/components/card/card.test.tsx | 159 ++++++++++ .../src/components/dropzone/dropzone.web.tsx | 13 +- .../event-card/event-card.native.tsx | 2 +- .../ui/src/components/input/input.test.tsx | 103 +++++++ .../ui/src/components/label/label.native.tsx | 2 +- .../components/list-item/list-item.native.tsx | 2 +- .../loading-indicator.native.tsx | 2 +- .../loading-indicator.web.tsx | 2 +- .../src/components/select/select.native.tsx | 8 +- .../components/textarea/textarea.native.tsx | 2 +- packages/ui/tsconfig.json | 2 +- 30 files changed, 1557 insertions(+), 26 deletions(-) create mode 100644 .serena/cache/typescript/document_symbols_cache_v23-06-25.pkl create mode 100644 .serena/memories/architecture_patterns.md create mode 100644 .serena/memories/code_style_conventions.md create mode 100644 .serena/memories/project_overview.md create mode 100644 .serena/memories/serena_code_review_2025.md create mode 100644 .serena/memories/suggested_commands.md create mode 100644 .serena/memories/task_completion_checklist.md create mode 100644 .serena/memories/tech_stack.md create mode 100644 .serena/project.yml create mode 100644 packages/ui/src/components/button/button.test.tsx create mode 100644 packages/ui/src/components/card/card.test.tsx create mode 100644 packages/ui/src/components/input/input.test.tsx diff --git a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl new file mode 100644 index 0000000000000000000000000000000000000000..8834a451729a5b32504302fb8519a3875b6b2a0d GIT binary patch literal 200719 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 IPIch_0!?4qLI3~& literal 0 HcmV?d00001 diff --git a/.serena/memories/architecture_patterns.md b/.serena/memories/architecture_patterns.md new file mode 100644 index 0000000..8c26f7b --- /dev/null +++ b/.serena/memories/architecture_patterns.md @@ -0,0 +1,279 @@ +# Architecture Patterns and Guidelines + +## Monorepo Architecture + +### Workspace Dependencies + +```json +// Package dependency pattern +{ + "dependencies": { + "@rite/backend": "workspace:*", // Always latest + "@rite/shared-types": "workspace:^", // Compatible version + "@rite/ui": "workspace:*" // Always latest + } +} +``` + +### Build Order (Turborepo) + +1. `@rite/shared-types` (foundational types) +2. `@rite/posthog-config` (analytics config) +3. `@rite/backend` (Convex functions) +4. `@rite/ui` (components) +5. `next-app` & `mobile` (applications) + +## Cross-Platform Component Design + +### Platform-Specific Implementation Pattern + +```tsx +// button/button.web.tsx +import { Slot } from '@radix-ui/react-slot'; +export const Button = React.forwardRef(...); + +// button/button.native.tsx +import { Pressable } from 'react-native'; +export const Button: React.FC = (...); + +// button/index.ts +export { Button } from './button.web'; +// OR +export { Button } from './button.native'; +``` + +### "use dom" Components (Mobile) + +```tsx +// For advanced web features in mobile +// qr-code/qr-code.dom.tsx +'use dom'; +import QRCode from 'qrcode'; +export const QRCodeComponent = (...); + +// Provides direct DOM access for Canvas, File API, etc. +``` + +## Authentication Architecture + +### Modular Auth System (Mobile) + +``` +lib/auth/ +β”œβ”€β”€ types.ts # AuthError class, interfaces +β”œβ”€β”€ oauth-config.ts # Platform-specific OAuth config +β”œβ”€β”€ secure-storage.ts # Cross-platform storage +β”œβ”€β”€ session-utils.ts # Session management +└── index.ts # Centralized exports + +hooks/auth/ +β”œβ”€β”€ useGoogleAuth.ts # Provider-specific logic +β”œβ”€β”€ useSession.ts # Session state +└── useOAuthFlow.ts # Complete flow orchestration + +contexts/ +└── AuthContext.tsx # Clean orchestration (88 lines) +``` + +### OAuth Flow Types + +- **Web**: Implicit flow (`response_type=token`) +- **Mobile**: Authorization code flow with auto-exchange +- **Platform Detection**: Automatic flow selection based on environment + +## Theme System Architecture + +### CSS Variables Pattern + +```css +:root { + --brand-primary: hsl(293deg 100% 66%); + --bg-primary: hsl(254deg 35% 15%); + --text-primary: hsl(0deg 0% 100%); + --button-primary-text: white; +} +``` + +### Theme Switching + +```tsx +// Automatic CSS variable updates +const applyTheme = (theme: Theme) => { + const css = generateThemeCSS(theme); + document.documentElement.style.cssText = css; + localStorage.setItem('theme', theme.name); +}; +``` + +## Data Protection Pattern + +### Basic Obfuscation (Convex V8 Runtime) + +```tsx +// Synchronous XOR + Base64 encoding +const encryptSensitiveData = (data: string): string => { + // XOR with encryption key + Base64 encoding + // NOT cryptographically secure - for basic privacy only +}; + +// Usage in mutations +const obfuscatedData = encryptSensitiveData(sensitiveInput); +await ctx.db.insert('submissions', { data: obfuscatedData }); +``` + +## Testing Patterns + +### TDD Workflow + +```tsx +// 1. RED - Write failing test +test('should export guest list as CSV', () => { + expect(exportToCSV(mockData)).toContain('Name,Email'); +}); + +// 2. GREEN - Minimal implementation +const exportToCSV = (data) => 'Name,Email\n...'; + +// 3. REFACTOR - Improve while tests pass +const exportToCSV = (data: GuestData[]) => { + // Proper implementation +}; +``` + +### Visual Testing + +```tsx +// Playwright visual regression +test('button variants', async ({ page }) => { + await page.goto('/visual-test'); + await expect(page.locator('[data-testid="buttons"]')).toHaveScreenshot(); +}); +``` + +## Effect.ts Integration + +### Functional Programming Pattern + +```tsx +import { Effect, pipe } from 'effect'; + +// Error handling with Effect +const processExportEffect = (data: ExportData) => + pipe( + Effect.tryPromise(() => generateExcel(data)), + Effect.mapError((error) => new ExportProcessingError(error)), + Effect.map((buffer) => ({ buffer, filename: 'export.xlsx' })) + ); +``` + +## File Upload Pattern + +### Cross-Platform File Handling + +```tsx +// Web (react-dropzone) +const { getRootProps, getInputProps } = useDropzone({ + accept: { 'image/*': ['.jpg', '.png'] }, + onDrop: handleFiles, +}); + +// Mobile (expo-image-picker) +const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, +}); +``` + +## Convex Backend Patterns + +### Database Schema + +```tsx +// Schema with proper relationships +export default defineSchema({ + events: defineTable({ + title: v.string(), + organizerId: v.id('users'), + // ... other fields + }).index('by_organizer', ['organizerId']), + + timeslots: defineTable({ + eventId: v.id('events'), + djId: v.optional(v.id('users')), + submissionToken: v.string(), + }).index('by_event', ['eventId']), +}); +``` + +### Mutation Pattern + +```tsx +export const createEvent = mutation({ + args: { title: v.string(), description: v.string() }, + handler: async (ctx, { title, description }) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + return await ctx.db.insert('events', { + title, + description, + organizerId: userId, + createdAt: Date.now(), + }); + }, +}); +``` + +## Internationalization Pattern + +### next-intl Setup + +```tsx +// Locale routing: /[locale]/page +// Messages: /messages/en.json, /messages/ko.json + +const t = useTranslations('dashboard'); +return

{t('title')}

; + +// Type-safe with generated types +type Messages = typeof import('../messages/en.json'); +``` + +## Design Token System + +### Token Hierarchy + +```tsx +// Base tokens +const colors = { + brand: { primary: '#E946FF' }, + neutral: { 0: '#FFFFFF', 900: '#000000' }, +}; + +// Semantic tokens +const semanticColors = { + bg: { primary: colors.neutral[900] }, + text: { primary: colors.neutral[0] }, +}; + +// Component tokens +const buttonTokens = { + primary: { bg: colors.brand.primary, text: 'white' }, +}; +``` + +## Performance Patterns + +### Bundle Optimization + +- **Dynamic imports** for large components +- **Tree shaking** with proper ES modules +- **Code splitting** at route level +- **Font optimization** with variable fonts + +### Mobile Performance + +- **Image optimization** with expo-image +- **Navigation optimization** with React Navigation +- **State management** with minimal re-renders +- **Platform-specific optimizations** diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md new file mode 100644 index 0000000..ae88c6f --- /dev/null +++ b/.serena/memories/code_style_conventions.md @@ -0,0 +1,208 @@ +# Code Style and Conventions + +## Mandatory Rules (from CLAUDE.md) + +### React Imports + +```tsx +// βœ… ALWAYS use namespace import for React +import * as React from 'react'; + +// ❌ NEVER use default import +import React from 'react'; +``` + +### TypeScript + +```tsx +// ❌ NEVER use any or non-null assertion +const data: any = getValue(); +const value = data!.property; + +// βœ… Use proper typing +const data: ExpectedType = getValue(); +const value = data?.property; +``` + +### Design System + +```tsx +// βœ… Use @rite/ui components exclusively +import { Button, Card } from '@rite/ui'; + +// βœ… Use CSS variables for colors +className = 'bg-[var(--brand-primary)]'; + +// ❌ Never use hardcoded colors +className = 'bg-purple-500'; +``` + +## ESLint Configuration + +### Key Rules + +- **Indentation**: Tabs (2-space width), SwitchCase: 1 +- **TypeScript**: No explicit `any`, no unsafe operations disabled +- **React**: Hooks rules enforced, refresh warnings +- **Unused vars**: Warn for vars/args starting with `_` + +### File Ignores + +- `dist/`, `convex/_generated/`, `instagram-oauth-proxy/` +- Config files: `eslint.config.mjs`, `postcss.config.js`, etc. + +## Prettier Configuration + +### Formatting Rules + +```json +{ + "useTabs": true, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} +``` + +### File Overrides + +- **YAML**: Uses spaces (tabWidth: 2) +- **Markdown**: Uses spaces, preserves prose wrapping + +## Component Structure + +### Platform-Specific Components + +``` +components/button/ +β”œβ”€β”€ button.web.tsx # Web implementation (Radix UI) +β”œβ”€β”€ button.native.tsx # React Native implementation +β”œβ”€β”€ index.ts # Platform-specific exports +└── button.test.tsx # Shared tests +``` + +### Import/Export Pattern + +```tsx +// index.ts - Platform-specific exports +export { Button } from './button.web'; +// or +export { Button } from './button.native'; + +// Usage - same import works everywhere +import { Button } from '@rite/ui'; +``` + +## TypeScript Conventions + +### Type Definitions + +```tsx +// βœ… Use 'type' instead of 'interface' unless absolutely necessary +type UserData = { + id: string; + name: string; + email?: string; +}; + +// βœ… Use proper component prop types +type ButtonProps = { + variant?: 'primary' | 'secondary'; + children: React.ReactNode; + onPress?: () => void; +}; +``` + +### Naming Conventions + +- **Components**: PascalCase (`ExportGuestList`, `ThemeSwitcher`) +- **Hooks**: camelCase with `use` prefix (`useGoogleAuth`, `useSession`) +- **Types**: PascalCase (`ExportData`, `ButtonProps`) +- **Constants**: UPPER_SNAKE_CASE (`API_ENDPOINTS`) +- **Files**: kebab-case or PascalCase for components + +## CSS/Styling Conventions + +### Tailwind Usage + +```tsx +// βœ… Use design tokens and CSS variables +className = 'bg-[var(--bg-primary)] text-[var(--text-primary)]'; + +// βœ… Use flex and gap (React Native compatible) +className = 'flex flex-row gap-2'; + +// ❌ Never use space-x or space-y (not React Native compatible) +className = 'space-x-2'; + +// βœ… Theme-agnostic design +className = 'bg-brand-primary text-button-primary-text'; +``` + +### Component Patterns + +```tsx +// βœ… Use forwardRef for web components +const Button = React.forwardRef( + ({ className, variant, ...props }, ref) => { + return ); + const button = screen.getByRole('button', { name: 'Click me' }); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('inline-flex', 'items-center', 'justify-center'); + }); + + it('applies variant classes correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Delete' }); + expect(button).toHaveClass('bg-error'); + }); + + it('applies size classes correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Small Button' }); + expect(button).toHaveClass('h-10', 'rounded-md', 'px-4', 'py-2', 'text-sm'); + }); + + it('applies large size classes correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Large Button' }); + expect(button).toHaveClass('h-14', 'rounded-xl', 'px-8', 'py-4', 'text-lg'); + }); + + it('handles click events', () => { + const handleClick = vi.fn(); + render(); + const button = screen.getByRole('button', { name: 'Click me' }); + + fireEvent.click(button); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('is disabled when disabled prop is true', () => { + render(); + const button = screen.getByRole('button', { name: 'Disabled Button' }); + expect(button).toBeDisabled(); + expect(button).toHaveClass('disabled:pointer-events-none', 'disabled:opacity-50'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); + + it('applies custom className', () => { + render(); + const button = screen.getByRole('button', { name: 'Custom Button' }); + expect(button).toHaveClass('custom-class'); + }); + + it('renders all variant types without errors', () => { + const variants = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'] as const; + + variants.forEach((variant) => { + render(); + const button = screen.getByRole('button', { name: `${variant} Button` }); + expect(button).toBeInTheDocument(); + }); + }); + + it('renders all size types without errors', () => { + const sizes = ['default', 'sm', 'lg', 'icon'] as const; + + sizes.forEach((size) => { + render(); + const button = screen.getByRole('button', { name: `${size} Button` }); + expect(button).toBeInTheDocument(); + }); + }); + + it('has correct accessibility attributes', () => { + render(); + const button = screen.getByRole('button', { name: 'Accessible button' }); + expect(button).toHaveAttribute('aria-label', 'Accessible button'); + }); + + it('applies focus styles correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Focus Button' }); + expect(button).toHaveClass('focus-visible:ring-2', 'focus-visible:ring-brand-primary'); + }); +}); \ No newline at end of file diff --git a/packages/ui/src/components/card/card.test.tsx b/packages/ui/src/components/card/card.test.tsx new file mode 100644 index 0000000..8fba234 --- /dev/null +++ b/packages/ui/src/components/card/card.test.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card.web'; + +describe('Card Components', () => { + describe('Card', () => { + it('renders correctly with default props', () => { + render( + + Card content + + ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('rounded-lg', 'bg-card', 'text-card-foreground', 'border', 'border-border', 'shadow-sm'); + }); + + it('applies custom className', () => { + render( + + Card content + + ); + const card = screen.getByTestId('card'); + expect(card).toHaveClass('custom-card'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render( + + Card content + + ); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + }); + + describe('CardHeader', () => { + it('renders correctly', () => { + render( + + + Title + + + ); + const header = screen.getByTestId('card-header'); + expect(header).toBeInTheDocument(); + expect(header).toHaveClass('flex', 'flex-col', 'gap-2', 'p-6'); + }); + }); + + describe('CardTitle', () => { + it('renders correctly as div element', () => { + render( + + + Card Title + + + ); + const title = screen.getByText('Card Title'); + expect(title).toBeInTheDocument(); + expect(title).toHaveClass('text-2xl', 'font-bold', 'leading-tight', 'text-text-primary'); + }); + + it('applies custom className', () => { + render( + + + Card Title + + + ); + const title = screen.getByText('Card Title'); + expect(title).toHaveClass('custom-title'); + }); + }); + + describe('CardDescription', () => { + it('renders correctly', () => { + render( + + + Card description text + + + ); + const description = screen.getByText('Card description text'); + expect(description).toBeInTheDocument(); + expect(description).toHaveClass('text-sm', 'text-text-secondary'); + }); + }); + + describe('CardContent', () => { + it('renders correctly', () => { + render( + + + Content goes here + + + ); + const content = screen.getByTestId('card-content'); + expect(content).toBeInTheDocument(); + expect(content).toHaveClass('p-6', 'pt-0'); + expect(content).toHaveTextContent('Content goes here'); + }); + }); + + describe('CardFooter', () => { + it('renders correctly', () => { + render( + + + Footer content + + + ); + const footer = screen.getByTestId('card-footer'); + expect(footer).toBeInTheDocument(); + expect(footer).toHaveClass('flex', 'items-center', 'p-6', 'pt-0'); + expect(footer).toHaveTextContent('Footer content'); + }); + }); + + describe('Complete Card Structure', () => { + it('renders a complete card with all components', () => { + render( + + + Complete Card + This is a complete card example + + +

Main card content goes here

+
+ + + +
+ ); + + const card = screen.getByTestId('complete-card'); + const title = screen.getByText('Complete Card'); + const description = screen.getByText('This is a complete card example'); + const content = screen.getByText('Main card content goes here'); + const button = screen.getByRole('button', { name: 'Action Button' }); + + expect(card).toBeInTheDocument(); + expect(title).toBeInTheDocument(); + expect(description).toBeInTheDocument(); + expect(content).toBeInTheDocument(); + expect(button).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/packages/ui/src/components/dropzone/dropzone.web.tsx b/packages/ui/src/components/dropzone/dropzone.web.tsx index 0e3edef..50b4aa4 100644 --- a/packages/ui/src/components/dropzone/dropzone.web.tsx +++ b/packages/ui/src/components/dropzone/dropzone.web.tsx @@ -1,8 +1,7 @@ 'use client'; import { UploadIcon } from 'lucide-react'; -import type { ReactNode } from 'react'; -import { createContext, useContext } from 'react'; +import * as React from 'react'; import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone'; import { useDropzone } from 'react-dropzone'; import { Button } from '../button'; @@ -29,13 +28,13 @@ const renderBytes = (bytes: number) => { return `${size.toFixed(2)}${units[unitIndex]}`; }; -const DropzoneContext = createContext(undefined); +const DropzoneContext = React.createContext(undefined); export type DropzoneProps = Omit & { src?: File[]; className?: string; onDrop?: (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => void; - children?: ReactNode; + children?: React.ReactNode; }; export const Dropzone = ({ @@ -94,7 +93,7 @@ export const Dropzone = ({ }; const useDropzoneContext = () => { - const context = useContext(DropzoneContext); + const context = React.useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); @@ -104,7 +103,7 @@ const useDropzoneContext = () => { }; export type DropzoneContentProps = { - children?: ReactNode; + children?: React.ReactNode; className?: string; }; @@ -141,7 +140,7 @@ export const DropzoneContent = ({ children, className }: DropzoneContentProps) = }; export type DropzoneEmptyStateProps = { - children?: ReactNode; + children?: React.ReactNode; className?: string; }; diff --git a/packages/ui/src/components/event-card/event-card.native.tsx b/packages/ui/src/components/event-card/event-card.native.tsx index e843d56..38119fa 100644 --- a/packages/ui/src/components/event-card/event-card.native.tsx +++ b/packages/ui/src/components/event-card/event-card.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Pressable, Text, View } from 'react-native'; import { Link2 } from 'lucide-react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/input/input.test.tsx b/packages/ui/src/components/input/input.test.tsx new file mode 100644 index 0000000..222c6e5 --- /dev/null +++ b/packages/ui/src/components/input/input.test.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { Input } from './input.web'; + +describe('Input Component', () => { + it('renders correctly with default props', () => { + render(); + const input = screen.getByPlaceholderText('Enter text'); + expect(input).toBeInTheDocument(); + expect(input).toHaveClass('flex', 'h-12', 'w-full', 'rounded-lg', 'border', 'border-border'); + }); + + it('applies custom className', () => { + render(); + const input = screen.getByPlaceholderText('Test input'); + expect(input).toHaveClass('custom-input'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLInputElement); + }); + + it('handles value changes', () => { + const handleChange = vi.fn(); + render(); + const input = screen.getByPlaceholderText('Change test'); + + fireEvent.change(input, { target: { value: 'new value' } }); + expect(handleChange).toHaveBeenCalledTimes(1); + }); + + it('can be disabled', () => { + render(); + const input = screen.getByPlaceholderText('Disabled input'); + expect(input).toBeDisabled(); + expect(input).toHaveClass('disabled:cursor-not-allowed', 'disabled:opacity-50'); + }); + + it('supports different input types', () => { + render(); + const input = screen.getByPlaceholderText('Email input'); + expect(input).toHaveAttribute('type', 'email'); + }); + + it('supports password type', () => { + render(); + const input = screen.getByPlaceholderText('Password input'); + expect(input).toHaveAttribute('type', 'password'); + }); + + it('applies focus styles', () => { + render(); + const input = screen.getByPlaceholderText('Focus test'); + expect(input).toHaveClass('focus:border-brand-primary', 'focus:ring-[3px]', 'focus:ring-brand-primary'); + }); + + it('handles controlled value', () => { + const TestComponent = () => { + const [value, setValue] = React.useState('initial value'); + return ( + setValue(e.target.value)} + placeholder="Controlled input" + /> + ); + }; + + render(); + const input = screen.getByDisplayValue('initial value'); + expect(input).toBeInTheDocument(); + + fireEvent.change(input, { target: { value: 'updated value' } }); + expect(screen.getByDisplayValue('updated value')).toBeInTheDocument(); + }); + + it('supports required attribute', () => { + render(); + const input = screen.getByPlaceholderText('Required input'); + expect(input).toBeRequired(); + }); + + it('supports maxLength attribute', () => { + render(); + const input = screen.getByPlaceholderText('Max length input'); + expect(input).toHaveAttribute('maxLength', '10'); + }); + + it('supports readOnly attribute', () => { + render(); + const input = screen.getByDisplayValue('Read only value'); + expect(input).toHaveAttribute('readOnly'); + }); + + it('renders without type attribute when no type specified', () => { + render(); + const input = screen.getByPlaceholderText('Default type'); + expect(input).not.toHaveAttribute('type'); + }); +}); \ No newline at end of file diff --git a/packages/ui/src/components/label/label.native.tsx b/packages/ui/src/components/label/label.native.tsx index 55c3b1a..18fff9c 100644 --- a/packages/ui/src/components/label/label.native.tsx +++ b/packages/ui/src/components/label/label.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Text, View, Pressable } from 'react-native'; import type { TextProps } from 'react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/list-item/list-item.native.tsx b/packages/ui/src/components/list-item/list-item.native.tsx index bcf7472..773b28e 100644 --- a/packages/ui/src/components/list-item/list-item.native.tsx +++ b/packages/ui/src/components/list-item/list-item.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Pressable, Text, View } from 'react-native'; import { ChevronRight } from 'lucide-react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx b/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx index 3bfc2a2..96ab928 100644 --- a/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx +++ b/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx b/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx index ded173f..0bfe727 100644 --- a/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx +++ b/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; interface LoadingIndicatorProps { className?: string; diff --git a/packages/ui/src/components/select/select.native.tsx b/packages/ui/src/components/select/select.native.tsx index 28f3970..de6dcd3 100644 --- a/packages/ui/src/components/select/select.native.tsx +++ b/packages/ui/src/components/select/select.native.tsx @@ -1,15 +1,15 @@ -import React, { useState } from 'react'; +import * as React from 'react'; import { View, Text, TouchableOpacity, Modal, ScrollView, Pressable } from 'react-native'; import { ChevronDownIcon, CheckIcon } from 'lucide-react-native'; import '@rite/ui/types/nativewind'; // Root Select component - manages state -export interface SelectProps { +export type SelectProps = { value?: string; onValueChange?: (value: string) => void; disabled?: boolean; children?: React.ReactNode; -} +}; interface SelectContextType { value?: string; @@ -22,7 +22,7 @@ interface SelectContextType { const SelectContext = React.createContext(undefined); export function Select({ value, onValueChange, disabled, children }: SelectProps) { - const [open, setOpen] = useState(false); + const [open, setOpen] = React.useState(false); return ( diff --git a/packages/ui/src/components/textarea/textarea.native.tsx b/packages/ui/src/components/textarea/textarea.native.tsx index 7ffd243..b74f194 100644 --- a/packages/ui/src/components/textarea/textarea.native.tsx +++ b/packages/ui/src/components/textarea/textarea.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { TextInput, Platform } from 'react-native'; import type { TextInputProps } from 'react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 7a6b452..0950d3a 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -25,5 +25,5 @@ "typeRoots": ["./node_modules/@types", "./src/types"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/**/*.test.*", "src/**/*.spec.*"] }