feat(config): add Babel configuration and improve project structure

This commit is contained in:
zimplexing
2025-08-14 15:29:52 +08:00
parent 1ef5a6b445
commit 9b7833b430
6 changed files with 552 additions and 83 deletions

View File

@@ -11,7 +11,8 @@
"Bash(yarn add:*)",
"Bash(git reset:*)",
"Bash(git add:*)",
"Bash(git commit:*)"
"Bash(git commit:*)",
"Bash(yarn test-ci:*)"
],
"deny": []
}

3
.gitignore vendored
View File

@@ -24,4 +24,5 @@ web/**
.bmad-core
.kilocodemodes
.roomodes
yarn-errors.log
yarn-errors.log
coverage/

172
CLAUDE.md
View File

@@ -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

7
babel.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [],
};
};

View File

@@ -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<typeof Dimensions>;
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);
});
});
});

View File

@@ -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<typeof StyleSheet>;
const mockedDeviceUtils = DeviceUtils as jest.Mocked<typeof DeviceUtils>;
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<any> = (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
});
});
});