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
13 changes: 0 additions & 13 deletions client/package-lock.json

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

110 changes: 110 additions & 0 deletions client/src/__tests__/index.view.edit.button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { userEvent } from '@testing-library/user-event';
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, screen } from '@testing-library/react';
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';

import { PATH } from '@recipe/constants';
import { mockGetTags } from '@recipe/graphql/queries/__mocks__/tag';
import { mockGetRecipeTwo } from '@recipe/graphql/queries/__mocks__/recipe';
import { mockCurrentUserNull } from '@recipe/graphql/queries/__mocks__/user';
import { MockedResponses, enterViewRecipePage, renderPage } from '@recipe/utils/tests';
import { mockGetIngredientComponents } from '@recipe/graphql/queries/__mocks__/recipe';
import { mockGetUnitConversions } from '@recipe/graphql/queries/__mocks__/unitConversion';
import { mockGetRecipeOne, mockGetRecipes } from '@recipe/graphql/queries/__mocks__/recipe';
import { mockCurrentUser, mockCurrentUserAdmin } from '@recipe/graphql/queries/__mocks__/user';
import { mockGetIngredientAndRecipeIngredients } from '@recipe/graphql/queries/__mocks__/recipe';

import { routes } from '../routes';

loadErrorMessages();
loadDevMessages();

const renderComponent = (mockedResponses: MockedResponses = []) => {
return renderPage(
routes,
[
mockGetRecipes,
mockGetTags,
mockGetIngredientAndRecipeIngredients,
mockGetUnitConversions,
...mockedResponses,
],
[PATH.ROOT]
);
};

describe('View Recipe Edit Button — admin user', () => {
afterEach(() => {
cleanup();
});

it('should show the edit button when logged in as admin', async () => {
renderComponent([mockCurrentUserAdmin, mockGetRecipeOne]);
const user = userEvent.setup();
await enterViewRecipePage(screen, user, 'Mock Recipe', 'Instruction one.');
expect(await screen.findByLabelText('Edit Mock Recipe')).not.toBeNull();
});
});

describe('View Recipe Edit Button — non-admin owner', () => {
afterEach(() => {
cleanup();
});

it('should show the edit button for a non-admin owner', async () => {
// mockCurrentUser has _id: mockUserId, mockRecipeTwo.owner is mockUserId
// mockRecipeTwo has isIngredient: true, pluralTitle: 'Mock Recipes Two', numServings: 3
// so the displayed title is the pluralTitle
renderComponent([mockCurrentUser, mockGetRecipeTwo]);
const user = userEvent.setup();
await enterViewRecipePage(screen, user, 'Mock Recipe Two', 'Instruction one.');
expect(await screen.findByLabelText('Edit Mock Recipes Two')).not.toBeNull();
});
});

describe('View Recipe Edit Button — not logged in', () => {
afterEach(() => {
cleanup();
});

it('should not show the edit button when not logged in', async () => {
renderComponent([mockCurrentUserNull, mockGetRecipeOne]);
const user = userEvent.setup();
await enterViewRecipePage(screen, user, 'Mock Recipe', 'Instruction one.');
expect(screen.queryByLabelText('Edit Mock Recipe')).toBeNull();
});
});

describe('View Recipe Edit Button — non-owner user', () => {
afterEach(() => {
cleanup();
});

it('should not show the edit button for a non-owner user', async () => {
// mockCurrentUser has _id: mockUserId, mockRecipeOne.owner is mockAdminId
renderComponent([mockCurrentUser, mockGetRecipeOne]);
const user = userEvent.setup();
await enterViewRecipePage(screen, user, 'Mock Recipe', 'Instruction one.');
expect(screen.queryByLabelText('Edit Mock Recipe')).toBeNull();
});
});

describe('View Recipe Edit Button — navigation', () => {
afterEach(() => {
cleanup();
});

it('should navigate to the edit recipe page when the edit button is clicked', async () => {
renderComponent([
mockCurrentUserAdmin,
mockGetRecipeOne,
mockGetRecipeOne,
mockGetIngredientComponents,
mockGetUnitConversions,
]);
const user = userEvent.setup();
await enterViewRecipePage(screen, user, 'Mock Recipe', 'Instruction one.');
await user.click(await screen.findByLabelText('Edit Mock Recipe'));
expect(await screen.findByText('Enter Recipe Title')).not.toBeNull();
});
});
26 changes: 26 additions & 0 deletions client/src/features/viewing/components/EditRecipeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Link } from 'react-router-dom';
import { MdEdit } from 'react-icons/md';
import { ActionIcon, Tooltip } from '@mantine/core';

import { PATH } from '@recipe/constants';

interface Props {
titleIdentifier: string;
recipeTitle: string;
}

export function EditRecipeButton({ titleIdentifier, recipeTitle }: Props) {
return (
<Tooltip label={`Edit ${recipeTitle}`} openDelay={500}>
<ActionIcon
variant='subtle'
size='lg'
aria-label={`Edit ${recipeTitle}`}
component={Link}
to={`${PATH.ROOT}/edit/recipe/${titleIdentifier}`}
>
<MdEdit size={20} />
</ActionIcon>
</Tooltip>
);
}
1 change: 1 addition & 0 deletions client/src/features/viewing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { IngredientsTab } from './components/IngredientsTab';
export { IngredientList } from './components/IngredientList';
export { InstructionsTab } from './components/InstructionsTab';
export { InstructionList } from './components/InstructionList';
export { EditRecipeButton } from './components/EditRecipeButton';
export { RecipeCardsContainer } from './components/RecipeCardsContainer';
18 changes: 15 additions & 3 deletions client/src/pages/ViewRecipe.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useQuery } from '@apollo/client';
import { useParams } from 'react-router-dom';
import { Box, Container, Grid, GridItem } from '@chakra-ui/react';
import { Box, Container, Flex, Grid, GridItem } from '@chakra-ui/react';

import { useUser } from '@recipe/features/user';
import { GET_RECIPE } from '@recipe/graphql/queries/recipe';
import { ImageViewerRecipe } from '@recipe/features/images';
import { IngredientsTab, InstructionsTab, Title } from '@recipe/features/viewing';
import { EditRecipeButton, IngredientsTab, InstructionsTab, Title } from '@recipe/features/viewing';

export function ViewRecipe() {
const { titleIdentifier } = useParams();
const { data, loading, error } = useQuery(GET_RECIPE, {
variables: { filter: { titleIdentifier } },
});
const { user } = useUser();

if (loading) {
return <div>Loading...</div>;
Expand All @@ -22,6 +24,8 @@ export function ViewRecipe() {
const { title, numServings, isIngredient, pluralTitle } = data.recipeOne;
const titleNormed =
isIngredient && pluralTitle ? (numServings > 1 ? pluralTitle : title) : title;
const hasEditPermission =
user !== null && (user._id === data.recipeOne.owner || user.role === 'admin');
return (
<Container maxW='container.xl' pt='60px'>
<Grid
Expand All @@ -43,7 +47,15 @@ export function ViewRecipe() {
fontWeight='bold'
>
<GridItem boxShadow='lg' p='6' area='title'>
<Title title={titleNormed} />
<Flex align='center' justify='space-between'>
<Title title={titleNormed} />
{hasEditPermission && (
<EditRecipeButton
titleIdentifier={data.recipeOne.titleIdentifier}
recipeTitle={titleNormed}
/>
)}
</Flex>
</GridItem>
<GridItem
boxShadow='lg'
Expand Down