|
| 1 | +import { useEffect, useState } from 'react'; |
| 2 | +import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; |
| 3 | +import { useLocalSearchParams } from 'expo-router'; |
| 4 | + |
| 5 | +import { useAppColors, type AppColors } from '../theme'; |
| 6 | + |
| 7 | +/** |
| 8 | + * A list whose every row repaints on a timer: `rows` rows of counters that advance every |
| 9 | + * `intervalMs`. The screen never goes idle, so it reproduces the accessibility capture cost of a |
| 10 | + * live dashboard (tester-army/e2e#841) without a third-party app. The Stop button freezes it for an |
| 11 | + * A/B against the same tree at rest. |
| 12 | + */ |
| 13 | +export function TickerScreen() { |
| 14 | + const colors = useAppColors(); |
| 15 | + const styles = createStyles(colors); |
| 16 | + const params = useLocalSearchParams<{ rows?: string; intervalMs?: string }>(); |
| 17 | + const rows = clampInteger(params.rows, 30, 1, 200); |
| 18 | + const intervalMs = clampInteger(params.intervalMs, 1000, 50, 10_000); |
| 19 | + const [tick, setTick] = useState(0); |
| 20 | + const [running, setRunning] = useState(true); |
| 21 | + |
| 22 | + useEffect(() => { |
| 23 | + if (!running) return; |
| 24 | + const timer = setInterval(() => setTick((value) => value + 1), intervalMs); |
| 25 | + return () => clearInterval(timer); |
| 26 | + }, [intervalMs, running]); |
| 27 | + |
| 28 | + const data = Array.from({ length: rows }, (_, index) => index); |
| 29 | + return ( |
| 30 | + <View style={styles.frame} testID="ticker-root"> |
| 31 | + <Text style={styles.title} testID="ticker-title"> |
| 32 | + Ticker {rows} rows every {intervalMs} ms |
| 33 | + </Text> |
| 34 | + <Pressable |
| 35 | + accessibilityRole="button" |
| 36 | + onPress={() => setRunning((value) => !value)} |
| 37 | + style={styles.button} |
| 38 | + testID="ticker-toggle" |
| 39 | + > |
| 40 | + <Text style={styles.buttonLabel}>{running ? 'Stop' : 'Start'}</Text> |
| 41 | + </Pressable> |
| 42 | + <FlatList |
| 43 | + data={data} |
| 44 | + keyExtractor={String} |
| 45 | + renderItem={({ item }) => ( |
| 46 | + <View style={styles.row} testID={`ticker-row-${item}`}> |
| 47 | + <Text style={styles.cell}>Row {item}</Text> |
| 48 | + <Text style={styles.cell} testID={`ticker-value-${item}`}> |
| 49 | + {tick + item} |
| 50 | + </Text> |
| 51 | + <Text style={styles.cell}>{(tick + item) * 2}</Text> |
| 52 | + <Text style={styles.cell}>{(tick + item) * 3}</Text> |
| 53 | + <Pressable accessibilityRole="button" style={styles.rowButton} testID={`ticker-a-${item}`}> |
| 54 | + <Text style={styles.rowButtonLabel}>A{item}</Text> |
| 55 | + </Pressable> |
| 56 | + <Pressable accessibilityRole="button" style={styles.rowButton} testID={`ticker-b-${item}`}> |
| 57 | + <Text style={styles.rowButtonLabel}>B{item}</Text> |
| 58 | + </Pressable> |
| 59 | + </View> |
| 60 | + )} |
| 61 | + /> |
| 62 | + </View> |
| 63 | + ); |
| 64 | +} |
| 65 | + |
| 66 | +function clampInteger(raw: string | undefined, fallback: number, min: number, max: number): number { |
| 67 | + const parsed = Number.parseInt(raw ?? '', 10); |
| 68 | + if (Number.isNaN(parsed)) return fallback; |
| 69 | + return Math.min(max, Math.max(min, parsed)); |
| 70 | +} |
| 71 | + |
| 72 | +function createStyles(colors: AppColors) { |
| 73 | + return StyleSheet.create({ |
| 74 | + frame: { flex: 1, backgroundColor: colors.surface, padding: 12 }, |
| 75 | + title: { color: colors.text, fontSize: 18, fontWeight: '600', marginBottom: 8 }, |
| 76 | + button: { alignSelf: 'flex-start', backgroundColor: colors.accent, borderRadius: 6, paddingHorizontal: 12, paddingVertical: 6, marginBottom: 8 }, |
| 77 | + buttonLabel: { color: colors.surface, fontWeight: '600' }, |
| 78 | + row: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingVertical: 4, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.line }, |
| 79 | + cell: { color: colors.text, minWidth: 44 }, |
| 80 | + rowButton: { borderWidth: 1, borderColor: colors.line, borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2 }, |
| 81 | + rowButtonLabel: { color: colors.text, fontSize: 12 }, |
| 82 | + }); |
| 83 | +} |
0 commit comments