Skip to content
This repository has been archived by the owner on Apr 22, 2021. It is now read-only.

Commit

Permalink
Create the edit and approve page (#113)
Browse files Browse the repository at this point in the history
* Create the edit and approve page

- Added form validation to Edit & Approve form

- Added required fields as discussed: headline, excerpt, topic

- Added current list of topics to select dropdown

- Added tests both for the form component and the page

Closes #98.

* Replace fireEvent() calls with userEvent() ones

* Update prospect URL
  • Loading branch information
nina-py authored Jan 26, 2021
1 parent 4275e05 commit 0e3efd4
Show file tree
Hide file tree
Showing 17 changed files with 944 additions and 291 deletions.
1 change: 1 addition & 0 deletions public/brokenImage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function App(): JSX.Element {
</Route>
<Route
exact
path="/:feed/prospects/article/edit-and-approve/:articleid/"
path="/:feed/prospects/:prospectid/edit-and-approve/"
>
<EditAndApproveStoryPage />
</Route>
Expand Down
10 changes: 7 additions & 3 deletions src/components/AddStory/AddStory.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Divider, Button, TextField } from '@material-ui/core';
import { Box, Divider, Button, TextField, Grid } from '@material-ui/core';
import { useForm } from 'react-hook-form';

interface AddStoryProps {
Expand All @@ -8,7 +8,7 @@ interface AddStoryProps {
*
* @param data
*/
onSubmit: (data: { url: string }) => void;
onSubmit: (data: AddStoryFormData) => void;
}

export interface AddStoryFormData {
Expand Down Expand Up @@ -57,7 +57,11 @@ export const AddStory: React.FC<AddStoryProps> = (props): JSX.Element => {
}}
variant="outlined"
/>
<Divider />
<Grid item xs={12}>
<Box py={3}>
<Divider />
</Box>
</Grid>
<Box display="flex" justifyContent="flex-end" mt="1rem">
<Button color="primary" variant="contained" type="submit">
Parse
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Props extends ButtonProps {
| 'hollow-neutral'; // white with grey border
buttonRef?: React.RefObject<HTMLButtonElement>;
component?: any;
to?: string;
to?: string | { pathname: string; state: any };
}

export const Button = ({
Expand Down
11 changes: 9 additions & 2 deletions src/components/Card/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import { Card } from './Card';
import { Prospect } from '../../services/types/Prospect';
import { MemoryRouter } from 'react-router-dom';

describe('The Card component', () => {
it('renders with image', () => {
const prospect: Prospect = {
id: '12345',
altText: 'Test alt text',
author: 'Just An Author',
category: 'History',
excerpt: 'A short description of this article',
feedId: 'abcdefg',
imageUrl: 'https://images.dog.ceo/breeds/bulldog-english/jager-2.jpg',
publisher: 'CNN',
source: 'CNN',
snoozedUntil: null,
title: 'Any random title',
topic: 'History',
url: 'https://cnn.com/any-random-title/234567/',
};

render(<Card prospect={prospect} />);
render(
<MemoryRouter>
<Card prospect={prospect} url="/en-US/prospects/123c-456b-789c/" />
</MemoryRouter>
);

const image = screen.getByAltText(prospect.altText);
expect(image).toBeInTheDocument();
Expand Down
39 changes: 31 additions & 8 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import {
Card as MuiCard,
CardMedia,
Expand Down Expand Up @@ -57,6 +58,11 @@ interface CardProps {
* The Prospect object that holds all the data we need to display.
*/
prospect: Prospect;

/**
* The base URL for action buttons
*/
url: string;
}

/**
Expand All @@ -65,7 +71,7 @@ interface CardProps {
*/
export const Card: React.FC<CardProps> = (props) => {
const classes = useStyles();
const { prospect } = props;
const { prospect, url } = props;

return (
<MuiCard variant="outlined" square className={classes.root}>
Expand All @@ -80,9 +86,9 @@ export const Card: React.FC<CardProps> = (props) => {
</Grid>
<Grid item className={classes.rightColumn}>
<CardText
author={prospect.author || 'No author supplied'}
excerpt={prospect.excerpt}
publisher={prospect.publisher}
author={prospect.author ?? ''}
excerpt={prospect.excerpt ?? ''}
publisher={prospect.publisher ?? ''}
title={prospect.title}
url={prospect.url}
/>
Expand All @@ -97,17 +103,34 @@ export const Card: React.FC<CardProps> = (props) => {
component="span"
align="left"
>
{prospect.source} &middot; {prospect.category || 'Uncategorized'}
{prospect.source} &middot; {prospect.topic || 'Uncategorized'}
</Typography>
</Grid>
<Grid
item
className={`${classes.rightColumn} ${classes.bottomRightCell}`}
>
<div className={classes.bottomRightButtonsContainer}>
<Button buttonType="negative">Reject</Button>
<Button buttonType="neutral">Snooze</Button>
<Button>Edit &amp; Approve</Button>
<Button
buttonType="negative"
component={Link}
to={{ pathname: `${url}reject/`, state: { prospect } }}
>
Reject
</Button>
<Button
buttonType="neutral"
component={Link}
to={{ pathname: `${url}snooze/`, state: { prospect } }}
>
Snooze
</Button>
<Button
component={Link}
to={{ pathname: `${url}edit-and-approve/`, state: { prospect } }}
>
Edit &amp; Approve
</Button>
</div>
</Grid>
</Grid>
Expand Down
134 changes: 107 additions & 27 deletions src/components/EditAndApproveStory/EditAndApproveStory.test.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,149 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { EditAndApproveStory } from './EditAndApproveStory';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
EditAndApproveStory,
EditAndApproveStoryFormData,
} from './EditAndApproveStory';
import { Prospect } from '../../services/types/Prospect';

describe('The EditAndApproveStory component', () => {
beforeEach(() => {
render(
<EditAndApproveStory
publisher="Test publisher"
author="Test author"
url="https://getpocket.com"
title="Test title"
excerpt="This is a short description."
altText="Test alt text"
thumbnailUrl="https://assets.getpocket.com/web/yir/2020/images/[email protected]"
source="Syndication"
topic="Health"
/>
);
let mockSubmit: any;
let mockProspect: Prospect;

beforeEach(async () => {
mockProspect = {
id: 'abcdefg',
altText: 'Test alt text',
author: 'Test author',
excerpt: 'This is a short description',
feedId: 'abcdefg',
imageUrl:
'https://assets.getpocket.com/web/yir/2020/images/[email protected]',
publisher: 'Test publisher',
source: 'Test source',
snoozedUntil: null,
title: 'Test title',
topic: 'Business',
url: 'https://getpocket.com',
};

mockSubmit = jest.fn((data: EditAndApproveStoryFormData) => {
return Promise.resolve(data);
});

await waitFor(() => {
render(
<EditAndApproveStory prospect={mockProspect} onSubmit={mockSubmit} />
);
});
});

it('renders successfully', () => {
// there is at least a heading and nothing falls over
const heading = screen.getByRole('heading');
expect(heading).toBeInTheDocument();
it('renders successfully', async () => {
// there is at least a form and nothing falls over
const form = screen.getByRole('form');
expect(form).toBeInTheDocument();
});

it('has the requisite buttons', () => {
it('has the requisite buttons', async () => {
// check that all action buttons are present
expect(screen.getByText('Cancel')).toBeInTheDocument();
expect(screen.getByText('Reject')).toBeInTheDocument();
expect(screen.getByText('Snooze')).toBeInTheDocument();
expect(screen.getByText('Approve')).toBeInTheDocument();
});

it('shows basic story data', () => {
it('shows basic story data', async () => {
const storyUrlField = screen.getByLabelText('Story URL');
expect(storyUrlField).toBeInTheDocument();
expect(storyUrlField).toBeDisabled();

const publisherField = screen.getByLabelText('Publisher');
expect(publisherField).toBeInTheDocument();
expect(publisherField).toBeDisabled();

const authorField = screen.getByLabelText('Author');
expect(authorField).toBeInTheDocument();
expect(authorField).toBeDisabled();

expect(screen.getByLabelText('Headline')).toBeInTheDocument();
expect(screen.getByLabelText('Excerpt')).toBeInTheDocument();
});

it('shows thumbnail and associated data', () => {
it('shows thumbnail and associated data', async () => {
const image = screen.getByRole('img');
expect(image).toBeInTheDocument();

expect(screen.getByLabelText('Thumbnail URL')).toBeInTheDocument();
expect(screen.getByLabelText('Alt Text')).toBeInTheDocument();
});

it('shows additional information about the story', () => {
it('shows additional information about the story', async () => {
const sourceField = screen.getByLabelText('Source');
expect(sourceField).toBeInTheDocument();
expect(sourceField).toBeDisabled();

expect(screen.getByLabelText('Topic')).toBeInTheDocument();
expect(screen.getByLabelText('History')).toBeInTheDocument();
});

it('displays errors when required fields are empty', async () => {
userEvent.clear(screen.getByLabelText(/headline/i));
userEvent.clear(screen.getByLabelText(/excerpt/i));
userEvent.selectOptions(screen.getByLabelText(/topic/i), '');

await waitFor(() => {
userEvent.click(screen.getByText(/approve/i));
});

expect(screen.getByText(/please enter a title/i)).toBeInTheDocument();
expect(
screen.getByText(/please enter a short description/i)
).toBeInTheDocument();
expect(screen.getByText(/please choose a topic/i)).toBeInTheDocument();
expect(mockSubmit).not.toBeCalled();
});

it('displays matching error when thumbnail URL is malformed', async () => {
const input = screen.getByLabelText(/thumbnail url/i) as HTMLInputElement;

userEvent.clear(input);
userEvent.type(input, 'This is not a valid link!!!');

await waitFor(() => {
userEvent.click(screen.getByText(/approve/i));
});

expect(screen.getByText(/please enter a valid url/i)).toBeInTheDocument();
expect(mockSubmit).not.toBeCalled();
});

it('proceeds with form submission if all fields are valid', async () => {
await waitFor(() => {
userEvent.click(screen.getByText(/approve/i));
});

expect(screen.queryByText(/please enter a title/i)).not.toBeInTheDocument();
expect(
screen.queryByText(/please enter a short description/i)
).not.toBeInTheDocument();
expect(
screen.queryByText(/please choose a topic/i)
).not.toBeInTheDocument();
expect(
screen.queryByText(/please enter a valid url/i)
).not.toBeInTheDocument();
expect(mockSubmit).toBeCalled();
});

it('shows a broken image icon if thumbnail URL is invalid', async () => {
const input = screen.getByLabelText(/thumbnail url/i) as HTMLInputElement;

userEvent.clear(input);
userEvent.type(input, 'http//:www.not-a-valid-domain$/.com/image.png');

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

const thumbnailImage = document.querySelector('img') as HTMLImageElement;
expect(thumbnailImage.src).toContain('not-a-valid-domain');
});
});
Loading

0 comments on commit 0e3efd4

Please sign in to comment.