Skip to content

Commit

Permalink
Make userprofile image visible in navs (#15)
Browse files Browse the repository at this point in the history
Fixes #12
  • Loading branch information
lyncasterc authored Mar 4, 2024
1 parent 2bfad28 commit d01b645
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 5 deletions.
1 change: 1 addition & 0 deletions frontend/cypress/fixtures/test-image-data-url.ts

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions frontend/cypress/integration/navbar.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import testImageDataUrl from '../fixtures/test-image-data-url';

const user = Cypress.env('user1');

beforeEach(() => {
Expand Down Expand Up @@ -36,3 +38,45 @@ it('when logged on mobile, mobile navs are rendered and visible', () => {
cy.get('[data-testid="home-nav"]').should('be.visible');
cy.get('[data-cy="bottom-nav"]').should('be.visible');
});

it('when a user has a profile image, it is displayed in mobile nav', () => {
cy.viewport('iphone-x');
cy.createUser(user);
cy.login({ username: user.username, password: user.password });
cy.editUser({ imageDataUrl: testImageDataUrl });
cy.reload();

cy.get('[data-testid="bottom-nav-avatar"] svg').should('not.exist');
cy.get('[data-testid="bottom-nav-avatar"] img').should('be.visible');
});

it('when a user has no profile image, a default image is displayed in mobile nav', () => {
cy.viewport('iphone-x');
cy.createUser(user);
cy.login({ username: user.username, password: user.password });
cy.reload();

cy.get('[data-testid="bottom-nav-avatar"] svg').should('be.visible');
cy.get('[data-testid="bottom-nav-avatar"] img').should('not.exist');
});

it('when a user has a profile image, it is displayed in desktop nav', () => {
cy.viewport('macbook-13');
cy.createUser(user);
cy.login({ username: user.username, password: user.password });
cy.editUser({ imageDataUrl: testImageDataUrl });
cy.reload();

cy.get('[data-testid="desktop-nav-avatar"] svg').should('not.exist');
cy.get('[data-testid="desktop-nav-avatar"] img').should('be.visible');
});

it('when a user has no profile image, a default image is displayed in desktop nav', () => {
cy.viewport('macbook-13');
cy.createUser(user);
cy.login({ username: user.username, password: user.password });
cy.reload();

cy.get('[data-testid="desktop-nav-avatar"] svg').should('be.visible');
cy.get('[data-testid="desktop-nav-avatar"] img').should('not.exist');
});
24 changes: 23 additions & 1 deletion frontend/cypress/support/commands.ts

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion frontend/cypress/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

import { NewUserFields, LoginFields } from '../../src/app/types';
import { NewUserFields, LoginFields, UpdatedUserFields } from '../../src/app/types';

declare global {
namespace Cypress {
Expand All @@ -46,6 +46,12 @@ declare global {
* Uses the logged in user's token to authenticate the request.
*/
createPost(): Chainable<Element>,
/**
* Custom Cypress command to edit a user.
* Uses the logged in user's token to authenticate the request.
* @param updatedUserFields - Object containing fields to update.
*/
editUser(updatedUserFields: UpdatedUserFields): Chainable<Element>,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Link, Navigate } from 'react-router-dom';
import { Avatar, Group, UnstyledButton } from '@mantine/core';
import { useState } from 'react';
import useStyles from './BottomNavbar.styles';
import { useAppSelector } from '../../../hooks/selector-dispatch-hooks';
import { selectUserByUsername } from '../../../../app/apiSlice';

interface BottomNavBarProps {
user: string | null,
Expand All @@ -17,6 +19,9 @@ interface BottomNavBarProps {
function BottomNavBar({ user }: BottomNavBarProps) {
const { classes } = useStyles();
const [image, setImage] = useState('');
const selectedUser = user ? useAppSelector(
(state) => selectUserByUsername(state, user),
) : undefined;

const handleFileInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
Expand Down Expand Up @@ -110,6 +115,8 @@ function BottomNavBar({ user }: BottomNavBarProps) {
to={`/${user}`}
radius="xl"
data-testid="bottom-nav-avatar"
src={selectedUser?.image?.url}
size="sm"
/>

</Group>
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/common/components/UserMenu/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@ import {
import { Link } from 'react-router-dom';
import useStyles from './UserMenu.styles';
import useAuth from '../../hooks/useAuth';
import { useAppSelector } from '../../hooks/selector-dispatch-hooks';
import { selectUserByUsername } from '../../../app/apiSlice';

// TODO: add user prop
function UserMenu() {
const { classes } = useStyles();
const [user, { logout }] = useAuth();
const selectedUser = user ? useAppSelector(
(state) => selectUserByUsername(state, user),
) : undefined;
const avatar = (
<Avatar
radius="xl"
classNames={{
root: classes.avatarRoot,
}}
data-testid="desktop-nav-avatar"
src={selectedUser?.image?.url}
size="sm"
/>
);

Expand Down
105 changes: 105 additions & 0 deletions frontend/src/test/int/profile-image-nav.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { rest } from 'msw';
import {
screen,
renderWithRouter,
mockLogin,
mockLogout,
waitFor,
within,
} from '../utils/test-utils';
import App from '../../app/App';
import '@testing-library/jest-dom/extend-expect';
import { fakeUser } from '../mocks/handlers';
import server from '../mocks/server';
import { apiSlice } from '../../app/apiSlice';
import { store } from '../../app/store';

beforeAll(() => server.listen());
beforeEach(() => {
const fakeTokenInfo = {
username: fakeUser.username,
token: 'supersecrettoken',
};
mockLogin({ fakeTokenInfo });
});
afterEach(() => {
mockLogout({ resetApiState: true });
server.resetHandlers();
});
afterAll(() => server.close());

test('when user has a profile image, it is displayed in mobile nav', async () => {
const imageSrc = 'https://i.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U';
server.use(
rest.get('/api/users', (req, res, ctx) => res(ctx.status(200), ctx.json([{
...fakeUser,
image: {
url: imageSrc,
publicId: 'fakePublicId',
},
}]))),
);

store.dispatch(apiSlice.endpoints.getUsers.initiate());
renderWithRouter(<App />, { route: '/' });

await waitFor(() => {
const avatar = screen.getByTestId('bottom-nav-avatar');
const profileImage = within(avatar).getByRole('img');

expect(profileImage).toHaveAttribute('src', imageSrc);
});
});

test('when user has no profile image, a default image is displayed in mobile nav', async () => {
store.dispatch(apiSlice.endpoints.getUsers.initiate());
renderWithRouter(<App />, { route: '/' });

const avatar = await screen.findByTestId('bottom-nav-avatar');

await waitFor(() => {
const image = within(avatar).queryByRole('img');
const placeholderSvg = avatar.querySelector('svg');

expect(placeholderSvg).not.toBeNull();
expect(image).toBeNull();
});
});

test('when user has no profile image, a default image is displayed in desktop nav', async () => {
store.dispatch(apiSlice.endpoints.getUsers.initiate());
renderWithRouter(<App />, { route: '/' });

const avatar = await screen.findByTestId('desktop-nav-avatar');

await waitFor(() => {
const image = within(avatar).queryByRole('img');
const placeholderSvg = avatar.querySelector('svg');

expect(placeholderSvg).not.toBeNull();
expect(image).toBeNull();
});
});

test('when user has a profile image, it is displayed in desktop nav', async () => {
const imageSrc = 'https://i.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U';
server.use(
rest.get('/api/users', (req, res, ctx) => res(ctx.status(200), ctx.json([{
...fakeUser,
image: {
url: imageSrc,
publicId: 'fakePublicId',
},
}]))),
);

store.dispatch(apiSlice.endpoints.getUsers.initiate());
renderWithRouter(<App />, { route: '/' });

await waitFor(() => {
const avatar = screen.getByTestId('bottom-nav-avatar');
const profileImage = within(avatar).getByRole('img');

expect(profileImage).toHaveAttribute('src', imageSrc);
});
});
10 changes: 7 additions & 3 deletions frontend/src/test/int/userprofileedit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ test('user with an image can upload a new one', async () => {
store.dispatch(apiSlice.endpoints.getUsers.initiate());
const { user } = renderWithRouter(<App />, { route: '/accounts/edit' });

const avatarImage = await screen.findByRole('img');
expect(avatarImage).toHaveAttribute('src', oldImage);
const avatarImages = await screen.findAllByRole('img');
avatarImages.forEach((avatarImage) => {
expect(avatarImage).toHaveAttribute('src', oldImage);
});

await user.click(screen.getByText(/change profile photo/i));

Expand All @@ -59,7 +61,9 @@ test('user with an image can upload a new one', async () => {
});

await waitFor(async () => {
expect(avatarImage).not.toHaveAttribute('src', oldImage);
avatarImages.forEach((avatarImage) => {
expect(avatarImage).not.toHaveAttribute('src', oldImage);
});

// Asserts that Alert appears on screen, indicating successful image upload.
expect(screen.getByText(/profile photo added/i)).toBeVisible();
Expand Down

0 comments on commit d01b645

Please sign in to comment.