React Instagram Clone

From bibbleWiki
Jump to navigation Jump to search

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",
...