React Instagram Clone
Introduction
This page is about the things I found on the instagram clone created by Karl Hadwin. This was a twelve hour course which aimed to reproduce the social media app Instagram. The repo can be found here
Making the enter key Work for button Click
<button
type="button"
title="Sign Out"
onClick={() => {
firebase.auth().signOut();
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
firebase.auth().signOut();
}
}}
>
Example of a listener
This listens for a change in firebase and returns the user logging on.
export default function useAuthListener() {
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')));
const { firebase } = useContext(FirebaseContext);
useEffect(() => {
const listener = firebase.auth().onAuthStateChanged((authUser) => {
if (authUser) {
localStorage.setItem('authUser', JSON.stringify(authUser));
setUser(authUser);
} else {
localStorage.removeItem('authUser');
setUser(null);
}
});
return () => listener();
}, [firebase]);
console.log(`Returning user:`, user);
return { user };
}
This listener is kicked off with the App and the result is passed down via the Context Provider Pattern
export default function App() {
const { user } = useAuthListener();
return (
<UserContext.Provider value={{ user }}>
<Router>
<Suspense fallback={<p>Loading ...</p>}>
<Routes>
<Route path={ROUTES.LOGIN} element={<Login />} />
<Route path={ROUTES.SIGN_UP} element={<SignUp />} />
<Route path={ROUTES.DASHBOARD} element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Router>
</UserContext.Provider>
);
}
This can now be accessed in components by using the useContext hook in any component being rendered in the app. In this case a component called header
import { useContext } from 'react';
export default function Header() {
const { user } = useContext(UserContext);
console.log(user)
return (
<header>
</header>
);
}
Testing
Testing Router
This was new to me so thought I would jot down this. When doing async test we need to use waitFor but with React we also need to wrap the tests in act. Below is the test we did for the Login Page for the Instagram clone project. Note that this is when using useNavigate and not useHistory as I was on React 17 at the time. It seems that you wrap the operation in act and then use waitFor as you would in Angular.
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate
}));
describe('<Login />', () => {
it('renders the login page with a form submission and logs the user in', async () => {
const succeedToLogin = jest.fn(() => Promise.resolve('I am signed in'));
const firebase = {
auth: jest.fn(() => ({
signInWithEmailAndPassword: succeedToLogin
}))
};
const { getByTestId, getByPlaceholderText, queryByTestId } = render(
<Router>
<FirebaseContext.Provider value={{ firebase }}>
<Login />
</FirebaseContext.Provider>
</Router>
);
await act(async () => {
await fireEvent.change(getByPlaceholderText('Email address'), {
target: { value: 'karl@gmail.com' }
});
await fireEvent.change(getByPlaceholderText('Password'), {
target: { value: 'test-password' }
});
fireEvent.submit(getByTestId('login'));
expect(document.title).toEqual('Login - Instagram');
expect(succeedToLogin).toHaveBeenCalled();
expect(succeedToLogin).toHaveBeenCalledWith(
'karl@gmail.com',
'test-password'
);
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.DASHBOARD);
expect(getByPlaceholderText('Email address').value).toBe(
'karl@gmail.com'
);
expect(getByPlaceholderText('Password').value).toBe('test-password');
expect(queryByTestId('error')).toBeFalsy();
});
});
});
});
Testing Split Text
Sometimes the Text is split in the code. E.g.
<p className="mr-10">
<span className="font-bold">{following.length}</span> following
</p>
An approach to this is use screen from the testing library. Gosh a lot of lines
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
...
screen.getByText((content, node) => {
const hasText = (node) => node.textContent === '1 following';
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
return nodeHasText && childrenDontHaveText;
});
General Testing With React
So most of the test seem to consist of
jest.mock('../../services/firebase');
jest.mock('../../hooks/use-user');
describe('<Profile />', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the profile page with a user profile', async () => {
// 1. Wrapping in Act
await act(async () => {
// 2. Mocking the functions
getUserByUsername.mockImplementation(() => [userFixture]);
getUserPhotosByUsername.mockImplementation(() => photosFixture);
useUser.mockImplementation(() => ({ user: userFixture }));
// 3. Doing the Render
const { getByText, getByTitle } = render(
<Router>
<FirebaseContext.Provider
value={{
firebase: {
auth: jest.fn(() => ({
signOut: jest.fn(() => ({
updateProfile: jest.fn(() => Promise.resolve({}))
}))
}))
}
}}
>
<UserContext.Provider
value={{
user: {
uid: 'NvPY9M9MzFTARQ6M816YAzDJxZ72',
displayName: 'karl'
}
}}
>
<Profile />
</UserContext.Provider>
</FirebaseContext.Provider>
</Router>
);
// 4. Waiting for Render to complete
await waitFor(() => {
// 5. Doing the assertions
expect(mockHistoryPush).not.toHaveBeenCalledWith(ROUTES.NOT_FOUND);
expect(getUserByUsername).toHaveBeenCalled();
expect(getUserByUsername).toHaveBeenCalledWith('orwell');
expect(getByTitle('Sign Out')).toBeTruthy();
expect(getByText('karl')).toBeTruthy();
expect(getByText('Karl Hadwen')).toBeTruthy();
screen.getByText((content, node) => {
const hasText = (node) => node.textContent === '5 photos';
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
return nodeHasText && childrenDontHaveText;
});
screen.getByText((content, node) => {
const hasText = (node) => node.textContent === '3 followers';
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
return nodeHasText && childrenDontHaveText;
});
screen.getByText((content, node) => {
const hasText = (node) => node.textContent === '1 following';
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every((child) => !hasText(child));
return nodeHasText && childrenDontHaveText;
});
});
// now sign the user out
fireEvent.click(getByTitle('Sign Out'));
fireEvent.keyDown(getByTitle('Sign Out'), {
key: 'Enter'
});
});
});
...
Cypress
Install with npm and add the script to packages.json
"scripts": {
"e2e": "cypress open",
...