Skip to content
Merged
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
5 changes: 5 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@ module.exports = function (api) {
api.cache(true);
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
env: {
test: {
plugins: ['dynamic-import-node'],
},
},
};
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"@wdio/spec-reporter": "^9.20.0",
"appium": "^2.19.0",
"appium-uiautomator2-driver": "^4.2.9",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-preset-expo": "^54.0.5",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions src/__tests__/components/TweetComposer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
getInfoAsync: jest.fn().mockResolvedValue({ exists: true, size: 1024 * 1024 }), // 1MB mock
}));

jest.mock('expo-file-system/legacy', () => ({
getInfoAsync: jest.fn().mockResolvedValue({ exists: true, size: 1024 * 1024 }),
}));

jest.mock('expo-video', () => {
const addListener = jest.fn(() => ({ remove: jest.fn() }));
const React = jest.requireActual('react');
Expand Down Expand Up @@ -1136,4 +1140,79 @@
});
});
});

it('handles navigation to author profile', () => {
const onPressAuthor = jest.fn();
const onClose = jest.fn();

const mockReplyToTweet = {
author: { username: 'author', displayName: 'Author', avatarUrl: null },
createdAt: new Date().toISOString(),
content: 'Original tweet',
};

const { getByTestId } = renderWithProviders(
<TweetComposer
visible
onClose={onClose}
onPressAuthor={onPressAuthor}
replyToAuthor={{ username: 'author', displayName: 'Author' }}
replyToTweet={mockReplyToTweet}
/>
);

const authorLink = getByTestId('replying-to-username');
fireEvent.press(authorLink);
expect(onPressAuthor).toHaveBeenCalledWith('author');
expect(onClose).toHaveBeenCalled();
});

it('alerts when camera permission is denied', async () => {
const mockRequestPermission = ImagePicker.requestCameraPermissionsAsync as jest.Mock;
mockRequestPermission.mockResolvedValueOnce({ status: 'denied' });
const mockAlert = jest.spyOn(Alert, 'alert');

const { getByTestId } = renderWithProviders(<TweetComposer visible onClose={() => {}} />);
const cameraIcon = getByTestId('camera-icon');

fireEvent.press(cameraIcon);

await waitFor(() => {
expect(mockAlert).toHaveBeenCalledWith('Permission needed', expect.any(String));
});
mockAlert.mockRestore();
});

it('alerts when file is too large', async () => {
const mockLaunchImageLibrary = ImagePicker.launchImageLibraryAsync as jest.Mock;
mockLaunchImageLibrary.mockResolvedValue({
canceled: false,
assets: [
{
uri: 'file://large-image.jpg',
fileName: 'large-image.jpg',
width: 1000,
height: 1000,
},
],
});

const mockGetInfo = require('expo-file-system/legacy').getInfoAsync;

Check warning on line 1200 in src/__tests__/components/TweetComposer.test.tsx

View workflow job for this annotation

GitHub Actions / Code Style & Quality

A `require()` style import is forbidden
mockGetInfo.mockResolvedValue({ exists: true, size: 10 * 1024 * 1024 }); // 10MB > 5MB limit

const mockAlert = jest.spyOn(Alert, 'alert');

const { getByTestId } = renderWithProviders(<TweetComposer visible onClose={() => {}} />);
const pictureIcon = getByTestId('media-icon');

fireEvent.press(pictureIcon);

await waitFor(() => {
expect(mockAlert).toHaveBeenCalledWith(
'File Too Large',
expect.stringContaining('exceed size limits')
);
});
mockAlert.mockRestore();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Alert } from 'react-native';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render } from '@testing-library/react-native';
import { act, fireEvent, render } from '@testing-library/react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import { FollowNotificationItem } from '@/components/notifications/types/FollowNotificationItem';
Expand Down Expand Up @@ -123,7 +123,6 @@ describe('FollowNotificationItem', () => {
</Wrapper>
);

// Button text is rendered with lowercase from nativewind
const followButton = getByText('Follow back');
expect(followButton).toBeTruthy();
});
Expand Down Expand Up @@ -185,12 +184,13 @@ describe('FollowNotificationItem', () => {
const followButton = getByText('Follow back');
fireEvent.press(followButton);

// Simulate error by calling the error callback
const mutateCall = mockFollowMutation.mutate.mock.calls[0];
const errorCallback = mutateCall[1]?.onError;

if (errorCallback) {
errorCallback(new Error('Network error'));
act(() => {
errorCallback(new Error('Network error'));
});
}

expect(mockAlert).toHaveBeenCalledWith('Unable to update follow', 'Network error');
Expand Down Expand Up @@ -292,9 +292,46 @@ describe('FollowNotificationItem', () => {

const nameText = getByText('Follower One');

// The name text is within a pressable container
fireEvent.press(nameText.parent?.parent || nameText);

expect(mockOnPress).toHaveBeenCalled();
});

it('navigates to user profile when avatar is pressed', () => {
const { getByTestId } = render(
<Wrapper>
<FollowNotificationItem notification={mockNotification} onPress={mockOnPress} />
</Wrapper>
);

const avatarGroup = getByTestId('actor-avatar-group');
fireEvent.press(avatarGroup);

expect(mockOnPress).toHaveBeenCalled();
});

it('should use default error message if error has no message', () => {
const mockAlert = jest.spyOn(Alert, 'alert');

const { getByText } = render(
<Wrapper>
<FollowNotificationItem notification={mockNotification} onPress={mockOnPress} />
</Wrapper>
);

const followButton = getByText('Follow back');
fireEvent.press(followButton);

const mutateCall = mockFollowMutation.mutate.mock.calls[0];
const errorCallback = mutateCall[1]?.onError;

if (errorCallback) {
act(() => {
errorCallback({});
});
}

expect(mockAlert).toHaveBeenCalledWith('Unable to update follow', 'Please try again.');
mockAlert.mockRestore();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@
}));

jest.mock('@/components/ui/ActorAvatarGroup', () => 'ActorAvatarGroup');
jest.mock('@/components/notifications/types/TweetActionBar', () => ({
TweetActionBar: 'TweetActionBar',
}));
jest.mock('@/components/notifications/types/TweetActionBar', () => {
const { Pressable, Text } = require('react-native');

Check warning on line 27 in src/__tests__/components/notifications/TweetQuoteNotificationItem.test.tsx

View workflow job for this annotation

GitHub Actions / Code Style & Quality

A `require()` style import is forbidden
return {
TweetActionBar: ({ onReplyPress }: { onReplyPress: () => void }) => (
<Pressable testID="reply-button" onPress={onReplyPress}>
<Text>Reply</Text>
</Pressable>
),
};
});

describe('TweetQuoteNotificationItem', () => {
const mockOnPress = jest.fn((callback) => callback());
Expand Down Expand Up @@ -184,4 +191,22 @@
fireEvent.press(getByText('User One'));
expect(mockOnPress).toHaveBeenCalled();
});

it('handles reply press', () => {
const notification = createNotification();
const { getByTestId } = render(
<TweetQuoteNotificationItem notification={notification} onPress={mockOnPress} />
);

const replyButton = getByTestId('reply-button');
fireEvent.press(replyButton);

expect(mockRootNavigate).toHaveBeenCalledWith(ROOT.TWEET, {
screen: TWEET.DETAIL,
params: {
tweetId: 'quoted-tweet1',
initialOpenComposer: true,
},
});
});
});
Loading
Loading