diff --git a/__tests__/ChatInput.test.tsx b/__tests__/ChatInput.test.tsx new file mode 100644 index 0000000..2ece3b5 --- /dev/null +++ b/__tests__/ChatInput.test.tsx @@ -0,0 +1,39 @@ +import { fireEvent, render, screen } from '@testing-library/react-native'; +import React from 'react'; +import { ChatInput } from '../components/chat/ChatInput'; + +describe('ChatInput', () => { + it('sends trimmed text, clears the field and disables send while empty', () => { + const onSend = jest.fn(); + render(); + + expect(screen.getByLabelText('Send message').props.accessibilityState.disabled).toBe(true); + + fireEvent.changeText(screen.getByLabelText('Message'), ' hello '); + fireEvent.press(screen.getByLabelText('Send message')); + + expect(onSend).toHaveBeenCalledWith('hello'); + expect(screen.getByLabelText('Message').props.value).toBe(''); + }); + + it('shows a stop button instead of send while loading', () => { + const onStop = jest.fn(); + render(); + + expect(screen.queryByLabelText('Send message')).toBeNull(); + fireEvent.press(screen.getByLabelText('Stop generating')); + expect(onStop).toHaveBeenCalledTimes(1); + }); + + it('fills the field when editing and lets the user cancel', () => { + const onCancelEdit = jest.fn(); + render(); + + expect(screen.getByText('Editing message')).toBeTruthy(); + expect(screen.getByLabelText('Message').props.value).toBe('old text'); + + fireEvent.press(screen.getByLabelText('Cancel editing')); + expect(onCancelEdit).toHaveBeenCalledTimes(1); + expect(screen.getByLabelText('Message').props.value).toBe(''); + }); +}); diff --git a/app/chat/_layout.tsx b/app/chat/_layout.tsx index 291f819..288c55f 100644 --- a/app/chat/_layout.tsx +++ b/app/chat/_layout.tsx @@ -36,6 +36,8 @@ export default function ChatLayout() { router.push('/chat/history')} + accessibilityRole="button" + accessibilityLabel="Chat history" style={[styles.headerBtn, { backgroundColor: colors.cardBackground }]} activeOpacity={0.7} > @@ -43,6 +45,8 @@ export default function ChatLayout() { @@ -53,6 +57,8 @@ export default function ChatLayout() { headerRight: () => ( router.push('/settings')} + accessibilityRole="button" + accessibilityLabel="Settings" style={[styles.headerBtn, { backgroundColor: colors.cardBackground }]} activeOpacity={0.7} > diff --git a/app/chat/history.tsx b/app/chat/history.tsx index afca7a3..6d438fb 100644 --- a/app/chat/history.tsx +++ b/app/chat/history.tsx @@ -20,7 +20,12 @@ export default function HistoryScreen() { return ( - router.back()} style={styles.backButton}> + router.back()} + style={styles.backButton} + accessibilityRole="button" + accessibilityLabel="Back" + > Chat History @@ -43,6 +48,8 @@ export default function HistoryScreen() { { backgroundColor: colors.cardBackground, borderColor: colors.border } ]} onPress={() => handleLoadChat(item.id)} + accessibilityRole="button" + accessibilityLabel={`Open chat: ${item.title}`} > @@ -55,6 +62,8 @@ export default function HistoryScreen() { deleteChatSession(item.id)} + accessibilityRole="button" + accessibilityLabel={`Delete chat: ${item.title}`} > diff --git a/components/chat/ChatInput.tsx b/components/chat/ChatInput.tsx index 761ee70..8e19bda 100644 --- a/components/chat/ChatInput.tsx +++ b/components/chat/ChatInput.tsx @@ -65,7 +65,12 @@ export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit {isEditing && ( Editing message - + @@ -80,6 +85,7 @@ export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit @@ -106,6 +114,9 @@ export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit onPress={handleSend} disabled={!text.trim()} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel="Send message" + accessibilityState={{ disabled: !text.trim() }} > diff --git a/components/chat/CodeBlock.tsx b/components/chat/CodeBlock.tsx index e03bbca..18d866d 100644 --- a/components/chat/CodeBlock.tsx +++ b/components/chat/CodeBlock.tsx @@ -16,7 +16,11 @@ export function CodeBlock({ language, content, copied, onCopy }: CodeBlockProps) {language} - onCopy(content)} style={styles.copyButton}> + onCopy(content)} + style={styles.copyButton} + accessibilityRole="button" + > {copied ? 'Copied!' : 'Copy'} diff --git a/components/chat/MessageActions.tsx b/components/chat/MessageActions.tsx index 9e0804a..9f59837 100644 --- a/components/chat/MessageActions.tsx +++ b/components/chat/MessageActions.tsx @@ -50,24 +50,24 @@ export function MessageActions({ return ( - + {isUser && onEdit && ( - + )} {!isUser && onRegenerate && ( - + )} {onDelete && ( - + )} diff --git a/components/chat/ModelSelector.tsx b/components/chat/ModelSelector.tsx index ddefbce..a5f2acb 100644 --- a/components/chat/ModelSelector.tsx +++ b/components/chat/ModelSelector.tsx @@ -42,6 +42,8 @@ export function ModelSelector() { style={[styles.headerButton, { backgroundColor: colors.cardBackground }]} onPress={() => setModalVisible(true)} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel={`Model: ${activeModelName}. Tap to change`} > @@ -70,6 +72,8 @@ export function ModelSelector() { onPress={handleRefresh} style={[styles.refreshButton, { backgroundColor: colors.cardBackground }]} disabled={isRefreshing} + accessibilityRole="button" + accessibilityLabel="Refresh models" > {isRefreshing ? ( @@ -108,6 +112,9 @@ export function ModelSelector() { isSelected && { borderColor: colors.tint } ]} onPress={() => handleSelect(item.id)} + accessibilityRole="button" + accessibilityLabel={item.id} + accessibilityState={{ selected: isSelected }} > diff --git a/components/settings/AppearanceCard.tsx b/components/settings/AppearanceCard.tsx index e86cda3..5acbf95 100644 --- a/components/settings/AppearanceCard.tsx +++ b/components/settings/AppearanceCard.tsx @@ -29,6 +29,7 @@ export function AppearanceCard() {