diff --git a/src/app/screens/HomeScreen.tsx b/src/app/screens/HomeScreen.tsx index 62c9782..b2af104 100644 --- a/src/app/screens/HomeScreen.tsx +++ b/src/app/screens/HomeScreen.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import { View, StyleSheet } from 'react-native'; +import { theme } from '../../theme'; import { PageHeader } from '../../components/layout/PageHeader'; import { TabBar, HomeTab } from '../../components/layout/TabBar'; import { ScreenContainer } from '../../components/layout/ScreenContainer'; @@ -25,7 +26,7 @@ export default function HomeScreen() { {activeTab === 'myWork' ? ( - + ) : ( )} @@ -37,5 +38,6 @@ export default function HomeScreen() { const styles = StyleSheet.create({ content: { flex: 1, + backgroundColor: theme.colors.background, }, }); diff --git a/src/app/tabs/MyWorkTab.test.tsx b/src/app/tabs/MyWorkTab.test.tsx new file mode 100644 index 0000000..af1a757 --- /dev/null +++ b/src/app/tabs/MyWorkTab.test.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react-native'; +import { MyWorkTab } from './MyWorkTab'; +import { MyWorkChapter } from '../../types/db/types'; + +jest.mock('@react-navigation/native', () => ({ + useNavigation: () => ({ + navigate: jest.fn(), + }), +})); + +jest.mock('react-native-svg', () => { + const MockReact = require('react'); + const { View } = require('react-native'); + const MockSvg = ({ children }: { children?: unknown }) => + MockReact.createElement(View, null, children); + return { + __esModule: true, + default: MockSvg, + Circle: MockSvg, + }; +}); + +jest.mock('lucide-react-native', () => { + const MockReact = require('react'); + const { View } = require('react-native'); + const MockIcon = () => MockReact.createElement(View); + return { + BookOpen: MockIcon, + ListChecks: MockIcon, + Cloud: MockIcon, + CloudCheck: MockIcon, + CloudUpload: MockIcon, + Circle: MockIcon, + Mic: MockIcon, + UserCheck: MockIcon, + ChevronRight: MockIcon, + }; +}); + +jest.mock('../../hooks/useMyWorkChapters', () => ({ + useMyWorkChapters: jest.fn(), +})); + +const { useMyWorkChapters } = jest.requireMock( + '../../hooks/useMyWorkChapters', +) as { + useMyWorkChapters: jest.Mock; +}; + +const sampleChapter: MyWorkChapter = { + id: 10, + displayLabel: 'Luke 4', + bookName: 'Luke', + chapterNumber: 4, + workflowStage: 'draft', + syncState: 'synced', + completedVerses: 3, + totalVerses: 5, + downloadedVerses: 5, + lastActivityLabel: 'Jun 1, 2024', + projectName: 'Gospel of Luke', + targetLanguageName: 'Baka', +}; + +const notStartedChapter: MyWorkChapter = { + id: 11, + displayLabel: 'Luke 16', + bookName: 'Luke', + chapterNumber: 16, + workflowStage: 'not_started', + syncState: 'none', + completedVerses: 0, + totalVerses: 5, + downloadedVerses: 0, + projectName: 'Gospel of Luke', + targetLanguageName: 'Baka', +}; + +describe('MyWorkTab', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders empty state when there are no chapters', async () => { + useMyWorkChapters.mockReturnValue({ + chapters: [], + loading: false, + refreshing: false, + refresh: jest.fn(), + }); + + render(); + + expect( + await screen.findByText( + "You don't have any chapters to work on right now. Check the Projects tab to find available work.", + ), + ).toBeTruthy(); + }); + + it('renders chapter title, badge, and activity date', async () => { + useMyWorkChapters.mockReturnValue({ + chapters: [sampleChapter], + loading: false, + refreshing: false, + refresh: jest.fn(), + }); + + render(); + + expect(await screen.findByText('Luke 4')).toBeTruthy(); + expect(await screen.findByText('Draft')).toBeTruthy(); + expect(await screen.findByText('Jun 1, 2024')).toBeTruthy(); + }); + + it('renders not started badge when source is not downloaded', async () => { + useMyWorkChapters.mockReturnValue({ + chapters: [notStartedChapter], + loading: false, + refreshing: false, + refresh: jest.fn(), + }); + + render(); + + expect(await screen.findByText('Luke 16')).toBeTruthy(); + expect(await screen.findByText('Not Started')).toBeTruthy(); + expect(screen.queryByText('Source not downloaded')).toBeNull(); + }); +}); diff --git a/src/app/tabs/MyWorkTab.tsx b/src/app/tabs/MyWorkTab.tsx index ffd6c1a..350780e 100644 --- a/src/app/tabs/MyWorkTab.tsx +++ b/src/app/tabs/MyWorkTab.tsx @@ -1,7 +1,63 @@ import React from 'react'; -import { EmptyState } from '../../components/ui/EmptyState'; +import { FlatList, StyleSheet } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; import { MY_WORK_EMPTY_MESSAGE } from '../../constants/messages'; +import { useMyWorkChapters } from '../../hooks/useMyWorkChapters'; +import { RootStackParamList } from '../../types/navigation/types'; +import { EmptyState } from '../../components/ui/EmptyState'; +import { LoadingSpinner } from '../../components/ui/LoadingSpinner'; +import { MyWorkRow } from '../../components/ui/MyWorkRow'; +import { theme } from '../../theme'; + +type Nav = StackNavigationProp; -export function MyWorkTab() { - return ; +interface MyWorkTabProps { + refreshKey?: number; + isSyncing?: boolean; } + +export function MyWorkTab({ + refreshKey = 0, + isSyncing = false, +}: MyWorkTabProps) { + const navigation = useNavigation