From ed6970e67a4165d965463dec5c95816a40c3a9ff Mon Sep 17 00:00:00 2001 From: Sergei Date: Sat, 31 Jan 2026 17:09:21 -0800 Subject: [PATCH] Add WiFi signal strength indicator component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add reusable WiFiSignalIndicator component with visual bars showing signal strength levels (excellent/good/fair/weak) based on RSSI values. Includes helper functions for signal labels and colors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/WiFiSignalIndicator.test.tsx | 145 ++++++++++++++++++ components/WiFiSignalIndicator.tsx | 101 ++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 __tests__/components/WiFiSignalIndicator.test.tsx create mode 100644 components/WiFiSignalIndicator.tsx diff --git a/__tests__/components/WiFiSignalIndicator.test.tsx b/__tests__/components/WiFiSignalIndicator.test.tsx new file mode 100644 index 0000000..f933a56 --- /dev/null +++ b/__tests__/components/WiFiSignalIndicator.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { + WiFiSignalIndicator, + getSignalStrengthLabel, + getSignalStrengthColor, +} from '@/components/WiFiSignalIndicator'; +import { AppColors } from '@/constants/theme'; + +describe('WiFiSignalIndicator', () => { + describe('Component rendering', () => { + it('renders without crashing', () => { + const { root } = render(); + expect(root).toBeDefined(); + }); + + it('renders with small size', () => { + const { root } = render(); + expect(root).toBeDefined(); + }); + + it('renders with medium size', () => { + const { root } = render(); + expect(root).toBeDefined(); + }); + + it('renders with large size', () => { + const { root } = render(); + expect(root).toBeDefined(); + }); + }); + + describe('Signal strength logic', () => { + it('shows excellent signal for RSSI >= -50', () => { + expect(getSignalStrengthLabel(-50)).toBe('Excellent'); + expect(getSignalStrengthLabel(-40)).toBe('Excellent'); + expect(getSignalStrengthLabel(-30)).toBe('Excellent'); + }); + + it('shows good signal for RSSI >= -60 and < -50', () => { + expect(getSignalStrengthLabel(-60)).toBe('Good'); + expect(getSignalStrengthLabel(-55)).toBe('Good'); + expect(getSignalStrengthLabel(-59)).toBe('Good'); + }); + + it('shows fair signal for RSSI >= -70 and < -60', () => { + expect(getSignalStrengthLabel(-70)).toBe('Fair'); + expect(getSignalStrengthLabel(-65)).toBe('Fair'); + expect(getSignalStrengthLabel(-69)).toBe('Fair'); + }); + + it('shows weak signal for RSSI < -70', () => { + expect(getSignalStrengthLabel(-71)).toBe('Weak'); + expect(getSignalStrengthLabel(-80)).toBe('Weak'); + expect(getSignalStrengthLabel(-90)).toBe('Weak'); + }); + }); + + describe('Signal strength colors', () => { + it('returns success color for excellent signal', () => { + expect(getSignalStrengthColor(-50)).toBe(AppColors.success); + expect(getSignalStrengthColor(-40)).toBe(AppColors.success); + }); + + it('returns info color for good signal', () => { + expect(getSignalStrengthColor(-60)).toBe(AppColors.info); + expect(getSignalStrengthColor(-55)).toBe(AppColors.info); + }); + + it('returns warning color for fair signal', () => { + expect(getSignalStrengthColor(-70)).toBe(AppColors.warning); + expect(getSignalStrengthColor(-65)).toBe(AppColors.warning); + }); + + it('returns error color for weak signal', () => { + expect(getSignalStrengthColor(-71)).toBe(AppColors.error); + expect(getSignalStrengthColor(-80)).toBe(AppColors.error); + }); + }); + + describe('Edge cases', () => { + it('handles boundary values correctly', () => { + // Test exact boundary values + expect(getSignalStrengthLabel(-50)).toBe('Excellent'); // Boundary: excellent/good + expect(getSignalStrengthLabel(-51)).toBe('Good'); + + expect(getSignalStrengthLabel(-60)).toBe('Good'); // Boundary: good/fair + expect(getSignalStrengthLabel(-61)).toBe('Fair'); + + expect(getSignalStrengthLabel(-70)).toBe('Fair'); // Boundary: fair/weak + expect(getSignalStrengthLabel(-71)).toBe('Weak'); + }); + + it('handles extreme RSSI values', () => { + expect(getSignalStrengthLabel(-10)).toBe('Excellent'); // Very strong signal + expect(getSignalStrengthLabel(-100)).toBe('Weak'); // Very weak signal + expect(getSignalStrengthLabel(0)).toBe('Excellent'); // Maximum possible + }); + + it('handles negative zero', () => { + expect(getSignalStrengthLabel(-0)).toBe('Excellent'); + }); + }); + + describe('Real-world RSSI values', () => { + it('handles typical home WiFi signals', () => { + expect(getSignalStrengthLabel(-45)).toBe('Excellent'); // Next to router + expect(getSignalStrengthLabel(-55)).toBe('Good'); // Same room + expect(getSignalStrengthLabel(-65)).toBe('Fair'); // Next room + expect(getSignalStrengthLabel(-75)).toBe('Weak'); // Far away + }); + + it('handles typical office WiFi signals', () => { + expect(getSignalStrengthLabel(-50)).toBe('Excellent'); + expect(getSignalStrengthLabel(-60)).toBe('Good'); + expect(getSignalStrengthLabel(-70)).toBe('Fair'); + expect(getSignalStrengthLabel(-80)).toBe('Weak'); + }); + }); + + describe('Component props', () => { + it('accepts valid RSSI values', () => { + const rssiValues = [-40, -50, -60, -70, -80]; + rssiValues.forEach((rssi) => { + const { root } = render(); + expect(root).toBeDefined(); + }); + }); + + it('accepts all size variants', () => { + const sizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; + sizes.forEach((size) => { + const { root } = render(); + expect(root).toBeDefined(); + }); + }); + + it('uses medium size by default', () => { + const withoutSize = render(); + const withMediumSize = render(); + expect(withoutSize.root).toBeDefined(); + expect(withMediumSize.root).toBeDefined(); + }); + }); +}); diff --git a/components/WiFiSignalIndicator.tsx b/components/WiFiSignalIndicator.tsx new file mode 100644 index 0000000..c77f9ed --- /dev/null +++ b/components/WiFiSignalIndicator.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { AppColors } from '@/constants/theme'; + +export type SignalStrength = 'excellent' | 'good' | 'fair' | 'weak'; + +interface WiFiSignalIndicatorProps { + rssi: number; + size?: 'small' | 'medium' | 'large'; +} + +/** + * Visual WiFi signal strength indicator with bars + * + * RSSI ranges: + * - Excellent: >= -50 dBm (4 bars) + * - Good: >= -60 dBm (3 bars) + * - Fair: >= -70 dBm (2 bars) + * - Weak: < -70 dBm (1 bar) + */ +export function WiFiSignalIndicator({ rssi, size = 'medium' }: WiFiSignalIndicatorProps) { + const getSignalColor = (rssi: number): string => { + if (rssi >= -50) return AppColors.success; + if (rssi >= -60) return AppColors.info; + if (rssi >= -70) return AppColors.warning; + return AppColors.error; + }; + + const getBars = (rssi: number): number => { + if (rssi >= -50) return 4; + if (rssi >= -60) return 3; + if (rssi >= -70) return 2; + return 1; + }; + + const color = getSignalColor(rssi); + const activeBars = getBars(rssi); + + const sizeStyles = { + small: { width: 2, maxHeight: 12, gap: 2 }, + medium: { width: 3, maxHeight: 16, gap: 3 }, + large: { width: 4, maxHeight: 20, gap: 4 }, + }; + + const { width: barWidth, maxHeight, gap } = sizeStyles[size]; + + return ( + + {[1, 2, 3, 4].map((barNumber) => { + const isActive = barNumber <= activeBars; + const barHeight = (maxHeight / 4) * barNumber; + + return ( + + ); + })} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + }, + bar: { + borderRadius: 1, + }, +}); + +/** + * Get human-readable signal strength label + */ +export function getSignalStrengthLabel(rssi: number): string { + if (rssi >= -50) return 'Excellent'; + if (rssi >= -60) return 'Good'; + if (rssi >= -70) return 'Fair'; + return 'Weak'; +} + +/** + * Get signal strength color + */ +export function getSignalStrengthColor(rssi: number): string { + if (rssi >= -50) return AppColors.success; + if (rssi >= -60) return AppColors.info; + if (rssi >= -70) return AppColors.warning; + return AppColors.error; +}