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() {