From 9b7833b4302705d9088ed609e80552703f089b53 Mon Sep 17 00:00:00 2001 From: zimplexing Date: Thu, 14 Aug 2025 15:29:52 +0800 Subject: [PATCH] feat(config): add Babel configuration and improve project structure --- .claude/settings.local.json | 3 +- .gitignore | 3 +- CLAUDE.md | 172 ++++++++-------- babel.config.js | 7 + utils/__tests__/DeviceUtils.test.ts | 213 ++++++++++++++++++++ utils/__tests__/ResponsiveStyles.test.ts | 237 +++++++++++++++++++++++ 6 files changed, 552 insertions(+), 83 deletions(-) create mode 100644 babel.config.js create mode 100644 utils/__tests__/DeviceUtils.test.ts create mode 100644 utils/__tests__/ResponsiveStyles.test.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 388fa8c..5f63d0a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -11,7 +11,8 @@ "Bash(yarn add:*)", "Bash(git reset:*)", "Bash(git add:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(yarn test-ci:*)" ], "deny": [] } diff --git a/.gitignore b/.gitignore index 704dd47..33c274a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ web/** .bmad-core .kilocodemodes .roomodes -yarn-errors.log \ No newline at end of file +yarn-errors.log +coverage/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3d12a7d..053a3e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,115 +11,125 @@ OrionTV is a React Native TVOS application for streaming video content, built wi ### Development Commands #### TV Development (Apple TV & Android TV) -- `yarn start-tv` - Start Metro bundler in TV mode (EXPO_TV=1) -- `yarn android-tv` - Build and run on Android TV -- `yarn ios-tv` - Build and run on Apple TV -- `yarn prebuild-tv` - Generate native project files for TV (run after dependency changes) -- `yarn build-tv` - Build Android APK for TV release +- `yarn start` - Start Metro bundler in TV mode (EXPO_TV=1) +- `yarn android` - Build and run on Android TV +- `yarn ios` - Build and run on Apple TV +- `yarn prebuild` - Generate native project files for TV (run after dependency changes) +- `yarn build` - Build Android APK for TV release -#### Mobile/Tablet Development (Responsive) -- `yarn start` or `yarn start-mobile` - Start Metro bundler for mobile/tablet -- `yarn android` or `yarn android-mobile` - Build and run on Android mobile/tablet -- `yarn ios` or `yarn ios-mobile` - Build and run on iOS mobile/tablet -- `yarn prebuild` or `yarn prebuild-mobile` - Generate native project files for mobile -- `yarn build` or `yarn build-mobile` - Build Android APK for mobile release - -#### General Commands -- `yarn copy-config` - Copy TV-specific Android configurations -- `yarn build-debug` - Build Android APK for debugging -- `yarn lint` - Run linting checks -- `yarn typecheck` - Run TypeScript type checking +#### Testing Commands - `yarn test` - Run Jest tests with watch mode - `yarn test-ci` - Run Jest tests for CI with coverage +- `yarn test utils` - Run tests for specific directory/file pattern +- `yarn lint` - Run ESLint checks +- `yarn typecheck` - Run TypeScript type checking + +#### Build and Deployment +- `yarn copy-config` - Copy TV-specific Android configurations +- `yarn build-debug` - Build Android APK for debugging - `yarn clean` - Clean cache and build artifacts - `yarn clean-modules` - Reinstall all node modules ## Architecture Overview -### Frontend Structure +### Multi-Platform Responsive Design -- **Expo Router**: File-based routing with screens in `/app` directory -- **State Management**: Zustand stores for global state (`/stores`) -- **TV-Specific Components**: Components optimized for TV remote control interaction -- **Services**: API layer, storage management, remote control server, and update service +OrionTV implements a sophisticated responsive architecture supporting multiple device types: +- **Device Detection**: Width-based breakpoints (mobile <768px, tablet 768-1023px, TV ≥1024px) +- **Component Variants**: Platform-specific files with `.tv.tsx`, `.mobile.tsx`, `.tablet.tsx` extensions +- **Responsive Utilities**: `DeviceUtils` and `ResponsiveStyles` for adaptive layouts and scaling +- **Adaptive Navigation**: Different interaction patterns per device type (touch vs remote control) -### Key Technologies +### State Management Architecture (Zustand) -- React Native TVOS (0.74.x) - TV-optimized React Native with TV-specific events -- Expo SDK 51 - Development platform and tooling -- TypeScript - Type safety throughout with `@/*` path mapping -- Zustand - Lightweight state management -- Expo AV - Video playback functionality +Domain-specific stores with consistent patterns: +- **homeStore.ts** - Home screen content, categories, Douban API data, and play records +- **playerStore.ts** - Video player state, controls, and episode management +- **settingsStore.ts** - App settings, API configuration, and user preferences +- **remoteControlStore.ts** - Remote control server functionality and HTTP bridge +- **authStore.ts** - User authentication state +- **updateStore.ts** - Automatic update checking and version management +- **favoritesStore.ts** - User favorites management -### State Management (Zustand Stores) +### Service Layer Pattern -- `homeStore.ts` - Home screen content, categories, Douban API data, and play records -- `playerStore.ts` - Video player state, controls, and episode management -- `settingsStore.ts` - App settings, API configuration, and user preferences -- `remoteControlStore.ts` - Remote control server functionality and HTTP bridge -- `authStore.ts` - User authentication state -- `updateStore.ts` - Automatic update checking and version management -- `favoritesStore.ts` - User favorites management +Clean separation of concerns across service modules: +- **api.ts** - External API integration with error handling and caching +- **storage.ts** - AsyncStorage wrapper with typed interfaces +- **remoteControlService.ts** - TCP-based HTTP server for external device control +- **updateService.ts** - Automatic version checking and APK download management +- **tcpHttpServer.ts** - Low-level TCP server implementation -### TV-Specific Features +### TV Remote Control System -- Remote control navigation (`useTVRemoteHandler` hook with HWEvent handling) -- TV-optimized UI components with focus management and `.tv.tsx` extensions -- Remote control server for external control via HTTP bridge (`remoteControlService.ts`) -- Gesture handling for TV remote interactions (select, left/right seeking, long press) -- TV-specific assets and icons for Apple TV and Android TV +Sophisticated TV interaction handling: +- **useTVRemoteHandler** - Centralized hook for TV remote event processing +- **Hardware Events** - HWEvent handling for TV-specific controls (play/pause, seek, menu) +- **Focus Management** - TV-specific focus states and navigation flows +- **Gesture Support** - Long press, directional seeking, auto-hide controls -### Service Layer Architecture +## Key Technologies -- `api.ts` - External API integration (search, video details, Douban data) -- `storage.ts` - AsyncStorage wrapper for local data persistence -- `remoteControlService.ts` - HTTP server for external device control -- `updateService.ts` - Automatic version checking and APK download -- `tcpHttpServer.ts` - TCP-based HTTP server implementation +- **React Native TVOS (0.74.x)** - TV-optimized React Native with TV-specific event handling +- **Expo SDK 51** - Development platform providing native capabilities and build tooling +- **TypeScript** - Complete type safety with `@/*` path mapping configuration +- **Zustand** - Lightweight state management for global application state +- **Expo Router** - File-based routing system with typed routes +- **Expo AV** - Video playback with TV-optimized controls ## Development Workflow -### Responsive Development Notes +### TV-First Development Pattern -- Use TV commands (`*-tv` variants) with EXPO_TV=1 for TV development -- Use mobile/tablet commands (without EXPO_TV=1) for responsive mobile/tablet development -- Run `yarn prebuild-tv` after adding new dependencies for TV builds -- Run `yarn prebuild-mobile` after adding new dependencies for mobile builds -- Use `yarn copy-config` to apply TV-specific Android configurations (TV builds only) -- Test on both TV devices (Apple TV/Android TV) and mobile devices (phones/tablets) +This project uses a TV-first approach with responsive adaptations: +- **Primary Target**: Apple TV and Android TV with remote control interaction +- **Secondary Targets**: Mobile and tablet with touch-optimized responsive design +- **Build Environment**: `EXPO_TV=1` environment variable enables TV-specific features +- **Component Strategy**: Shared components with platform-specific variants using file extensions + +### Testing Strategy + +- **Unit Tests**: Comprehensive test coverage for utilities (`utils/__tests__/`) +- **Jest Configuration**: Expo preset with Babel transpilation +- **Test Patterns**: Mock-based testing for React Native modules and external dependencies +- **Coverage Reporting**: CI-compatible coverage reports with detailed metrics + +### Important Development Notes + +- Run `yarn prebuild` after adding new dependencies for native builds +- Use `yarn copy-config` to apply TV-specific Android configurations - TV components require focus management and remote control support -- Mobile/tablet components use touch-optimized responsive design -- The same codebase supports all platforms through responsive architecture +- Test on both TV devices (Apple TV/Android TV) and responsive mobile/tablet layouts +- All API calls are centralized in `/services` directory with error handling +- Storage operations use AsyncStorage wrapper in `storage.ts` with typed interfaces -### State Management Patterns +### Component Development Patterns -- Use Zustand stores for global state -- Stores follow a consistent pattern with actions and state -- API calls are centralized in the `/services` directory -- Storage operations use AsyncStorage wrapper in `storage.ts` +- **Platform Variants**: Use `.tv.tsx`, `.mobile.tsx`, `.tablet.tsx` for platform-specific implementations +- **Responsive Utilities**: Leverage `DeviceUtils.getDeviceType()` for responsive logic +- **TV Remote Handling**: Use `useTVRemoteHandler` hook for TV-specific interactions +- **Focus Management**: TV components must handle focus states for remote navigation +- **Shared Logic**: Place common logic in `/hooks` directory for reusability -### Component Structure +## Common Development Tasks -- TV-specific components have `.tv.tsx` extensions -- Common components in `/components` directory -- Custom hooks in `/hooks` directory for reusable logic -- TV remote handling is centralized in `useTVRemoteHandler` +### Adding New Components +1. Create base component in `/components` directory +2. Add platform-specific variants (`.tv.tsx`) if needed +3. Import and use responsive utilities from `@/utils/DeviceUtils` +4. Test across device types for proper responsive behavior -## Common Issues +### Working with State +1. Identify appropriate Zustand store in `/stores` directory +2. Follow existing patterns for actions and state structure +3. Use TypeScript interfaces for type safety +4. Consider cross-store dependencies and data flow -### TV Platform Specifics - -- TV apps require special focus management -- Remote control events need careful handling -- TV-specific assets and icons required -- Platform-specific build configurations - -### Development Environment - -- Ensure Xcode is installed for Apple TV development -- Android Studio required for Android TV development -- Metro bundler must run in TV mode (`EXPO_TV=1`) -- External API servers configured in settings for video content +### API Integration +1. Add new endpoints to `/services/api.ts` +2. Implement proper error handling and loading states +3. Use caching strategies for frequently accessed data +4. Update relevant Zustand stores with API responses ## File Structure Notes diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..df63172 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,7 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [], + }; +}; \ No newline at end of file diff --git a/utils/__tests__/DeviceUtils.test.ts b/utils/__tests__/DeviceUtils.test.ts new file mode 100644 index 0000000..f9bbb32 --- /dev/null +++ b/utils/__tests__/DeviceUtils.test.ts @@ -0,0 +1,213 @@ +import { Dimensions } from "react-native"; +import { DeviceUtils } from "../DeviceUtils"; + +jest.mock("react-native", () => ({ + Dimensions: { + get: jest.fn(), + }, +})); + +const mockedDimensions = Dimensions as jest.Mocked; + +describe("DeviceUtils", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("getDeviceType", () => { + it("应该在宽度 >= 1024 时返回 tv", () => { + mockedDimensions.get.mockReturnValue({ width: 1024, height: 768 }); + expect(DeviceUtils.getDeviceType()).toBe("tv"); + }); + + it("应该在宽度 >= 768 且 < 1024 时返回 tablet", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + expect(DeviceUtils.getDeviceType()).toBe("tablet"); + }); + + it("应该在宽度 < 768 时返回 mobile", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.getDeviceType()).toBe("mobile"); + }); + }); + + describe("isTV", () => { + it("应该在 TV 设备上返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.isTV()).toBe(true); + }); + + it("应该在非 TV 设备上返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.isTV()).toBe(false); + }); + }); + + describe("isMobile", () => { + it("应该在移动设备上返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.isMobile()).toBe(true); + }); + + it("应该在非移动设备上返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 1024, height: 768 }); + expect(DeviceUtils.isMobile()).toBe(false); + }); + }); + + describe("isTablet", () => { + it("应该在平板设备上返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + expect(DeviceUtils.isTablet()).toBe(true); + }); + + it("应该在非平板设备上返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.isTablet()).toBe(false); + }); + }); + + describe("supportsTouchInteraction", () => { + it("应该在非 TV 设备上返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.supportsTouchInteraction()).toBe(true); + }); + + it("应该在 TV 设备上返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.supportsTouchInteraction()).toBe(false); + }); + }); + + describe("supportsRemoteControlInteraction", () => { + it("应该在 TV 设备上返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.supportsRemoteControlInteraction()).toBe(true); + }); + + it("应该在非 TV 设备上返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.supportsRemoteControlInteraction()).toBe(false); + }); + }); + + describe("getMinTouchTargetSize", () => { + it("应该为 mobile 设备返回 44", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.getMinTouchTargetSize()).toBe(44); + }); + + it("应该为 tablet 设备返回 48", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + expect(DeviceUtils.getMinTouchTargetSize()).toBe(48); + }); + + it("应该为 tv 设备返回 60", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.getMinTouchTargetSize()).toBe(60); + }); + }); + + describe("getOptimalFontSize", () => { + it("应该为 mobile 设备返回基础大小 * 1.0", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.getOptimalFontSize(16)).toBe(16); + }); + + it("应该为 tablet 设备返回基础大小 * 1.1", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + expect(DeviceUtils.getOptimalFontSize(16)).toBe(18); + }); + + it("应该为 tv 设备返回基础大小 * 1.25", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.getOptimalFontSize(16)).toBe(20); + }); + }); + + describe("getOptimalSpacing", () => { + it("应该为 mobile 设备返回基础间距 * 0.8", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.getOptimalSpacing(20)).toBe(16); + }); + + it("应该为 tablet 设备返回基础间距 * 1.0", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + expect(DeviceUtils.getOptimalSpacing(20)).toBe(20); + }); + + it("应该为 tv 设备返回基础间距 * 1.5", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.getOptimalSpacing(20)).toBe(30); + }); + }); + + describe("isLandscape", () => { + it("应该在横屏模式下返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 812, height: 375 }); + expect(DeviceUtils.isLandscape()).toBe(true); + }); + + it("应该在竖屏模式下返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.isLandscape()).toBe(false); + }); + + it("应该在宽高相等时返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 500, height: 500 }); + expect(DeviceUtils.isLandscape()).toBe(false); + }); + }); + + describe("isPortrait", () => { + it("应该在竖屏模式下返回 true", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.isPortrait()).toBe(true); + }); + + it("应该在横屏模式下返回 false", () => { + mockedDimensions.get.mockReturnValue({ width: 812, height: 375 }); + expect(DeviceUtils.isPortrait()).toBe(false); + }); + }); + + describe("getSafeColumnCount", () => { + it("应该在 mobile 设备上返回安全列数", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + // minCardWidth = 120, maxColumns = 375 / 120 = 3.125 = 3 + expect(DeviceUtils.getSafeColumnCount(5)).toBe(3); + expect(DeviceUtils.getSafeColumnCount(2)).toBe(2); + }); + + it("应该在 tablet 设备上返回安全列数", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + // minCardWidth = 140, maxColumns = 768 / 140 = 5.485 = 5 + expect(DeviceUtils.getSafeColumnCount(6)).toBe(5); + expect(DeviceUtils.getSafeColumnCount(3)).toBe(3); + }); + + it("应该在 tv 设备上返回安全列数", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + // minCardWidth = 160, maxColumns = 1920 / 160 = 12 + expect(DeviceUtils.getSafeColumnCount(15)).toBe(12); + expect(DeviceUtils.getSafeColumnCount(8)).toBe(8); + }); + }); + + describe("getAnimationDuration", () => { + it("应该为 mobile 设备返回基础持续时间 * 1.0", () => { + mockedDimensions.get.mockReturnValue({ width: 375, height: 812 }); + expect(DeviceUtils.getAnimationDuration(300)).toBe(300); + }); + + it("应该为 tablet 设备返回基础持续时间 * 1.0", () => { + mockedDimensions.get.mockReturnValue({ width: 768, height: 1024 }); + expect(DeviceUtils.getAnimationDuration(300)).toBe(300); + }); + + it("应该为 tv 设备返回基础持续时间 * 1.2", () => { + mockedDimensions.get.mockReturnValue({ width: 1920, height: 1080 }); + expect(DeviceUtils.getAnimationDuration(300)).toBe(360); + }); + }); +}); \ No newline at end of file diff --git a/utils/__tests__/ResponsiveStyles.test.ts b/utils/__tests__/ResponsiveStyles.test.ts new file mode 100644 index 0000000..d9e9bf6 --- /dev/null +++ b/utils/__tests__/ResponsiveStyles.test.ts @@ -0,0 +1,237 @@ +import { StyleSheet } from "react-native"; +import { + createResponsiveStyles, + useResponsiveStyles, + getCommonResponsiveStyles, + getResponsiveTextSize, + getResponsiveSpacing, + ResponsiveStyleCreator, +} from "../ResponsiveStyles"; +import { ResponsiveConfig } from "@/hooks/useResponsiveLayout"; +import { DeviceUtils } from "../DeviceUtils"; + +jest.mock("react-native", () => ({ + StyleSheet: { + create: jest.fn((styles) => styles), + }, +})); + +jest.mock("@/hooks/useResponsiveLayout", () => ({ + useResponsiveLayout: jest.fn(), +})); + +jest.mock("@/utils/DeviceUtils", () => ({ + DeviceUtils: { + getMinTouchTargetSize: jest.fn(), + getOptimalFontSize: jest.fn(), + }, +})); + +const mockedStyleSheet = StyleSheet as jest.Mocked; +const mockedDeviceUtils = DeviceUtils as jest.Mocked; + +describe("ResponsiveStyles", () => { + const mockConfig: ResponsiveConfig = { + deviceType: "mobile", + spacing: 16, + safeAreaInsets: { top: 0, bottom: 0, left: 0, right: 0 }, + windowWidth: 375, + windowHeight: 812, + isLandscape: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockedDeviceUtils.getMinTouchTargetSize.mockReturnValue(44); + mockedDeviceUtils.getOptimalFontSize.mockImplementation((size) => size); + }); + + describe("createResponsiveStyles", () => { + it("应该创建响应式样式函数", () => { + const styleCreator: ResponsiveStyleCreator = (config) => ({ + container: { + padding: config.spacing, + }, + }); + + const responsiveStylesFunc = createResponsiveStyles(styleCreator); + const styles = responsiveStylesFunc(mockConfig); + + expect(mockedStyleSheet.create).toHaveBeenCalledWith({ + container: { + padding: 16, + }, + }); + expect(styles).toEqual({ + container: { + padding: 16, + }, + }); + }); + }); + + describe("getCommonResponsiveStyles", () => { + beforeEach(() => { + mockedDeviceUtils.getOptimalFontSize.mockImplementation((size) => { + const deviceType = "mobile"; + const scaleFactor = { + mobile: 1.0, + tablet: 1.1, + tv: 1.25, + }[deviceType]; + return Math.round(size * scaleFactor); + }); + }); + + it("应该为 mobile 设备返回正确的样式", () => { + const mobileConfig: ResponsiveConfig = { + ...mockConfig, + deviceType: "mobile", + spacing: 16, + }; + + mockedDeviceUtils.getMinTouchTargetSize.mockReturnValue(44); + + const styles = getCommonResponsiveStyles(mobileConfig); + + expect(styles.container).toEqual({ + flex: 1, + paddingHorizontal: 16, + }); + + expect(styles.safeContainer).toEqual({ + flex: 1, + paddingHorizontal: 16, + paddingTop: 20, + }); + + expect(styles.primaryButton).toEqual({ + minHeight: 44, + paddingHorizontal: 24, + paddingVertical: 16, + borderRadius: 8, + justifyContent: "center", + alignItems: "center", + }); + }); + + it("应该为 tablet 设备返回正确的样式", () => { + const tabletConfig: ResponsiveConfig = { + ...mockConfig, + deviceType: "tablet", + spacing: 20, + }; + + mockedDeviceUtils.getMinTouchTargetSize.mockReturnValue(48); + + const styles = getCommonResponsiveStyles(tabletConfig); + + expect(styles.safeContainer.paddingTop).toBe(30); + expect(styles.primaryButton.borderRadius).toBe(10); + expect(styles.primaryButton.minHeight).toBe(48); + }); + + it("应该为 tv 设备返回正确的样式", () => { + const tvConfig: ResponsiveConfig = { + ...mockConfig, + deviceType: "tv", + spacing: 24, + }; + + mockedDeviceUtils.getMinTouchTargetSize.mockReturnValue(60); + + const styles = getCommonResponsiveStyles(tvConfig); + + expect(styles.safeContainer.paddingTop).toBe(40); + expect(styles.primaryButton.borderRadius).toBe(12); + expect(styles.primaryButton.minHeight).toBe(60); + }); + + it("应该为 tv 设备不包含阴影样式", () => { + const tvConfig: ResponsiveConfig = { + ...mockConfig, + deviceType: "tv", + spacing: 24, + }; + + const styles = getCommonResponsiveStyles(tvConfig); + + expect(styles.shadow).toEqual({}); + }); + + it("应该为非 tv 设备包含阴影样式", () => { + const mobileConfig: ResponsiveConfig = { + ...mockConfig, + deviceType: "mobile", + spacing: 16, + }; + + const styles = getCommonResponsiveStyles(mobileConfig); + + expect(styles.shadow).toEqual({ + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }); + }); + }); + + describe("getResponsiveTextSize", () => { + it("应该为 mobile 设备返回基础大小", () => { + const result = getResponsiveTextSize(16, "mobile"); + expect(result).toBe(16); + }); + + it("应该为 tablet 设备返回缩放后的大小", () => { + const result = getResponsiveTextSize(16, "tablet"); + expect(result).toBe(18); // 16 * 1.1 = 17.6, rounded to 18 + }); + + it("应该为 tv 设备返回缩放后的大小", () => { + const result = getResponsiveTextSize(16, "tv"); + expect(result).toBe(20); // 16 * 1.25 = 20 + }); + + it("应该为未知设备类型返回基础大小", () => { + const result = getResponsiveTextSize(16, "unknown"); + expect(result).toBe(16); + }); + + it("应该正确处理小数点", () => { + const result = getResponsiveTextSize(15, "tablet"); + expect(result).toBe(17); // 15 * 1.1 = 16.5, rounded to 17 + }); + }); + + describe("getResponsiveSpacing", () => { + it("应该为 mobile 设备返回缩放后的间距", () => { + const result = getResponsiveSpacing(20, "mobile"); + expect(result).toBe(16); // 20 * 0.8 = 16 + }); + + it("应该为 tablet 设备返回基础间距", () => { + const result = getResponsiveSpacing(20, "tablet"); + expect(result).toBe(20); + }); + + it("应该为 tv 设备返回缩放后的间距", () => { + const result = getResponsiveSpacing(20, "tv"); + expect(result).toBe(30); // 20 * 1.5 = 30 + }); + + it("应该为未知设备类型返回基础间距", () => { + const result = getResponsiveSpacing(20, "unknown"); + expect(result).toBe(20); + }); + + it("应该正确处理小数点", () => { + const result = getResponsiveSpacing(15, "mobile"); + expect(result).toBe(12); // 15 * 0.8 = 12 + }); + }); +}); \ No newline at end of file