Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions __tests__/app/screen-options.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Stack, Tabs } from 'expo-router';

import Feed from '@/app/(app)/index';
import Style from '@/app/(app)/style';
import Onboarding from '@/app/onboarding';
import Login from '@/app/sign-in';
import { render, screen } from '@/lib/test-utils';

jest.mock('expo-router', () => ({
Stack: { Screen: jest.fn(() => null) },
Tabs: { Screen: jest.fn(() => null) },
Link: ({ children }: { children: React.ReactNode }) => children,
useRouter: jest.fn(() => ({ replace: jest.fn() })),
}));

jest.mock('@/components/ui', () => ({
...jest.requireActual('@/components/ui'),
FocusAwareStatusBar: () => null,
}));

jest.mock('@/components/ui/icons', () => ({
Feed: () => null,
Style: () => null,
}));

jest.mock('@/components/buttons', () => ({ Buttons: () => null }));
jest.mock('@/components/colors', () => ({ Colors: () => null }));
jest.mock('@/components/inputs', () => ({ Inputs: () => null }));
jest.mock('@/components/typography', () => ({ Typography: () => null }));
jest.mock('@/components/cover', () => ({ Cover: () => null }));
jest.mock('@/components/login-form', () => ({ LoginForm: () => null }));

jest.mock('@/lib/hooks', () => ({
useIsFirstTime: jest.fn(() => [true, jest.fn()]),
}));

jest.mock('@/api/auth/use-login', () => ({
useLogin: jest.fn(() => ({ mutate: jest.fn(), isPending: false })),
}));

const mockStackScreen = Stack.Screen as unknown as jest.Mock;
const mockTabsScreen = Tabs.Screen as unknown as jest.Mock;

const optionsOf = (mock: jest.Mock) => mock.mock.calls[0][0].options;

describe('screen options declared by each screen', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('titles the feed tab and adds a create link to its header', () => {
render(<Feed />);

const options = optionsOf(mockTabsScreen);
expect(options).toMatchObject({
title: 'Feed',
tabBarButtonTestID: 'feed-tab',
});

render(options.headerRight());
expect(screen.getByText('Create')).toBeOnTheScreen();
});

it('titles the style tab', () => {
render(<Style />);

expect(optionsOf(mockTabsScreen)).toMatchObject({
title: 'Style',
tabBarButtonTestID: 'style-tab',
});
});

it('hides the header on onboarding', () => {
render(<Onboarding />);

expect(optionsOf(mockStackScreen)).toEqual({ headerShown: false });
});

it('hides the header on sign in', () => {
render(<Login />);

expect(optionsOf(mockStackScreen)).toEqual({ headerShown: false });
});
});
85 changes: 85 additions & 0 deletions __tests__/app/www.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import { WebView } from 'react-native-webview';

import WWW from '@/app/www';
import { render, screen } from '@/lib/test-utils';

jest.mock('expo-router', () => ({
Stack: { Screen: jest.fn(() => null) },
useLocalSearchParams: jest.fn(),
useRouter: jest.fn(),
}));

jest.mock('react-native-webview', () => ({
WebView: jest.fn(() => null),
}));

const mockStackScreen = Stack.Screen as unknown as jest.Mock;
const mockWebView = WebView as unknown as jest.Mock;
const mockUseLocalSearchParams = useLocalSearchParams as jest.Mock;
const mockUseRouter = useRouter as jest.Mock;

describe('WWW', () => {
const back = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
mockUseRouter.mockReturnValue({ back });
});

it('opens the url in a modal titled with the title param', () => {
mockUseLocalSearchParams.mockReturnValue({
url: 'https://example.com',
title: 'Example',
});

render(<WWW />);

expect(mockStackScreen).toHaveBeenCalledTimes(1);
expect(mockStackScreen.mock.calls[0][0].options).toEqual({
presentation: 'modal',
title: 'Example',
});
expect(mockWebView.mock.calls[0][0].source).toEqual({
uri: 'https://example.com',
});
});

it('leaves the title empty when the title param is missing', () => {
mockUseLocalSearchParams.mockReturnValue({ url: 'https://example.com' });

render(<WWW />);

expect(mockStackScreen.mock.calls[0][0].options.title).toBe('');
});

it('goes back when the page fails to load', () => {
mockUseLocalSearchParams.mockReturnValue({ url: 'https://example.com' });

render(<WWW />);
mockWebView.mock.calls[0][0].onError();

expect(back).toHaveBeenCalledTimes(1);
});

it('shows an error instead of the page when the url is missing', () => {
mockUseLocalSearchParams.mockReturnValue({});

render(<WWW />);

expect(screen.getByText('Invalid Url')).toBeOnTheScreen();
expect(mockWebView).not.toHaveBeenCalled();
expect(mockStackScreen).toHaveBeenCalledTimes(1);
});

it('shows an error when the url param is repeated', () => {
mockUseLocalSearchParams.mockReturnValue({
url: ['https://a.example', 'https://b.example'],
});

render(<WWW />);

expect(screen.getByText('Invalid Url')).toBeOnTheScreen();
expect(mockWebView).not.toHaveBeenCalled();
});
});
49 changes: 5 additions & 44 deletions src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { Link, Redirect, SplashScreen, Tabs } from 'expo-router';
import { Redirect, SplashScreen, Stack, Tabs } from 'expo-router';
import { useCallback, useEffect } from 'react';

import { useAuth } from '@/components/providers/auth';
import { Pressable, Text } from '@/components/ui';
import {
Feed as FeedIcon,
Settings as SettingsIcon,
Style as StyleIcon,
} from '@/components/ui/icons';
import { useIsFirstTime } from '@/lib';

export default function TabLayout() {
Expand All @@ -30,42 +24,9 @@ export default function TabLayout() {
return <Redirect href="/sign-in" />;
}
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Feed',
tabBarIcon: ({ color }) => <FeedIcon color={color} />,
headerRight: () => <CreateNewPostLink />,
tabBarButtonTestID: 'feed-tab',
}}
/>
<Tabs.Screen
name="style"
options={{
title: 'Style',
tabBarIcon: ({ color }) => <StyleIcon color={color} />,
tabBarButtonTestID: 'style-tab',
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color }) => <SettingsIcon color={color} />,
tabBarButtonTestID: 'settings-tab',
}}
/>
</Tabs>
);
}

function CreateNewPostLink() {
return (
<Link href="/feed/add-post" asChild>
<Pressable>
<Text className="px-3 text-primary-300">Create</Text>
</Pressable>
</Link>
<>
<Stack.Screen options={{ headerShown: false }} />
<Tabs screenOptions={{ headerShown: true }} />
</>
);
}
33 changes: 32 additions & 1 deletion src/app/(app)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
import { FocusAwareStatusBar, View } from '@/components/ui';
import { Link, Tabs } from 'expo-router';
import { useTranslation } from 'react-i18next';

import {
FocusAwareStatusBar,
Pressable,
Text,
View,
} from '@/components/ui';
import { Feed as FeedIcon } from '@/components/ui/icons';

export default function Feed() {
const { t } = useTranslation();

return (
<View className="flex-1 ">
<Tabs.Screen
options={{
title: t('tabs.feed'),
tabBarIcon: ({ color }) => <FeedIcon color={color} />,
headerRight: () => <CreateNewPostLink />,
tabBarButtonTestID: 'feed-tab',
}}
Comment thread
fernandatoledo marked this conversation as resolved.
/>
<FocusAwareStatusBar />
</View>
);
}

function CreateNewPostLink() {
const { t } = useTranslation();

return (
<Link href="/feed/add-post" asChild>
<Pressable>
<Text className="px-3 text-primary-300">{t('tabs.create')}</Text>
</Pressable>
</Link>
);
}
11 changes: 9 additions & 2 deletions src/app/(app)/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-lines-per-function */
import { Link } from 'expo-router';
import { Link, Tabs } from 'expo-router';
import { useColorScheme } from 'nativewind';
import * as React from 'react';
import { showMessage } from 'react-native-flash-message';
Expand All @@ -18,7 +18,7 @@ import {
Text,
View,
} from '@/components/ui';
import { Website } from '@/components/ui/icons';
import { Settings as SettingsIcon, Website } from '@/components/ui/icons';
import { translate } from '@/lib';
import { Env } from '@/lib/env';

Expand All @@ -44,6 +44,13 @@ export default function Settings() {

return (
<>
<Tabs.Screen
options={{
title: translate('settings.title'),
tabBarIcon: ({ color }) => <SettingsIcon color={color} />,
tabBarButtonTestID: 'settings-tab',
}}
/>
Comment thread
Copilot marked this conversation as resolved.
<FocusAwareStatusBar />
<ScrollView>
<View className="flex-1 gap-2 p-4">
Expand Down
12 changes: 12 additions & 0 deletions src/app/(app)/style.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { Tabs } from 'expo-router';
import * as React from 'react';
import { useTranslation } from 'react-i18next';

import { Buttons } from '@/components/buttons';
import { Colors } from '@/components/colors';
import { Inputs } from '@/components/inputs';
import { Typography } from '@/components/typography';
import { FocusAwareStatusBar, SafeAreaView, ScrollView } from '@/components/ui';
import { Style as StyleIcon } from '@/components/ui/icons';

export default function Style() {
const { t } = useTranslation();

return (
<>
<Tabs.Screen
options={{
title: t('tabs.style'),
tabBarIcon: ({ color }) => <StyleIcon color={color} />,
tabBarButtonTestID: 'style-tab',
}}
Comment thread
fernandatoledo marked this conversation as resolved.
/>
<FocusAwareStatusBar />
<ScrollView className="px-4">
<SafeAreaView className="flex-1">
Expand Down
23 changes: 4 additions & 19 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet } from 'react-native';
import FlashMessage from 'react-native-flash-message';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
Expand Down Expand Up @@ -37,38 +36,24 @@ SplashScreen.setOptions({

function GuardedStack() {
const { isAuthenticated } = useAuth();
const { t } = useTranslation();
const [isFirstTime] = useIsFirstTime();

return (
<Stack>
<Stack.Protected guard={isFirstTime}>
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
<Stack.Screen name="onboarding" />
</Stack.Protected>

<Stack.Protected guard={isAuthenticated}>
<Stack.Screen name="(app)" options={{ headerShown: false }} />
<Stack.Screen
name="update-password"
options={{
title: t('updatePassword.title'),
}}
/>
<Stack.Screen name="(app)" />
<Stack.Screen name="update-password" />
</Stack.Protected>
Comment thread
fernandatoledo marked this conversation as resolved.

<Stack.Protected guard={!isAuthenticated}>
<Stack.Screen name="sign-in" options={{ headerShown: false }} />
<Stack.Screen name="sign-in" />
<Stack.Screen name="sign-up" />
<Stack.Screen name="forgot-password" />
</Stack.Protected>

<Stack.Screen
name="www"
options={{
presentation: 'modal',
title: '',
}}
/>
</Stack>
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRouter } from 'expo-router';
import { Stack, useRouter } from 'expo-router';

import { Cover } from '@/components/cover';
import {
Expand All @@ -17,6 +17,7 @@ export default function Onboarding() {

return (
<View className="flex h-full items-center justify-center">
<Stack.Screen options={{ headerShown: false }} />
<FocusAwareStatusBar />
<View className="w-full flex-1">
<Cover />
Expand Down
Loading
Loading