From c8f88da079bf8faebcd477ed08b910efcccf54c9 Mon Sep 17 00:00:00 2001 From: Andrey Romanov Date: Sat, 30 Mar 2019 23:32:46 +0300 Subject: [PATCH] [#58] Cover feedback page with E2E tests --- .eslintrc | 3 + .gitignore | 1 + cypress/integration/feedback.spec.js | 52 +++++++++++++++++ cypress/objects/events-page.js | 4 ++ cypress/objects/feedback-page.js | 27 +++++++++ cypress/support/commands.js | 25 -------- cypress/support/commands/index.js | 14 +++++ source/scripts/components/form-input/index.js | 24 ++++---- source/scripts/components/top-bar/index.js | 45 +++++++------- source/scripts/pages/events/index.js | 12 ++++ .../pages/{events-page.js => events/view.js} | 58 ++++++++----------- source/scripts/pages/feedback-page.js | 10 ---- source/scripts/pages/feedback/index.js | 5 ++ .../index.js => pages/feedback/view.js} | 38 +++++++----- source/scripts/routes.js | 4 +- webpack/common.config.js | 3 + 16 files changed, 206 insertions(+), 119 deletions(-) create mode 100644 cypress/integration/feedback.spec.js create mode 100644 cypress/objects/feedback-page.js delete mode 100644 cypress/support/commands.js create mode 100644 cypress/support/commands/index.js create mode 100644 source/scripts/pages/events/index.js rename source/scripts/pages/{events-page.js => events/view.js} (70%) delete mode 100644 source/scripts/pages/feedback-page.js create mode 100644 source/scripts/pages/feedback/index.js rename source/scripts/{components/feedback/index.js => pages/feedback/view.js} (61%) diff --git a/.eslintrc b/.eslintrc index c8f6fc5..c9bbbc9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -38,6 +38,7 @@ "max": 1 } ], + "import/no-extraneous-dependencies": [ "error", { @@ -48,6 +49,8 @@ ] } ], + "import/no-default-export": 1, + "react/require-extension": "off", "react/jsx-indent": ["error", "tab"], "react/jsx-indent-props": ["error", "tab"], diff --git a/.gitignore b/.gitignore index 755db88..34dc2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ static/index.appcache .idea .vscode npm-debug.* +jsconfig.json diff --git a/cypress/integration/feedback.spec.js b/cypress/integration/feedback.spec.js new file mode 100644 index 0000000..f47e108 --- /dev/null +++ b/cypress/integration/feedback.spec.js @@ -0,0 +1,52 @@ +import { eventsPage } from '../objects/events-page'; +import { feedbackPage } from '../objects/feedback-page'; + +describe('Feedback page', () => { + beforeEach(() => { + cy.visit('/'); + }); + + specify('should be available from events page', () => { + eventsPage.root().should('be.visible'); + eventsPage.feedbackButton().should('be.visible'); + eventsPage.feedbackButton().click(); + + eventsPage.root().should('not.be.visible'); + feedbackPage.root().should('be.visible'); + feedbackPage.back().should('be.visible'); + feedbackPage.submit().should('be.visible'); + feedbackPage.feedbackField().should('be.visible'); + feedbackPage.emailField().should('be.visible'); + }); + + specify('should have working back button', () => { + cy.openFeedbackPage(); + + feedbackPage.back().should('be.visible'); + feedbackPage.back().click(); + + feedbackPage.root().should('not.be.visible'); + eventsPage.root().should('be.visible'); + }); + + specify('should return to events page after submit', () => { + cy.openFeedbackPage(); + + feedbackPage.feedbackField().type('Hello from E2E tests!'); + feedbackPage.emailField().type('e2e@tests.com'); + feedbackPage.submit().click(); + + feedbackPage.root().should('not.be.visible'); + eventsPage.root().should('be.visible'); + }); + + specify('should be able to submit review without email', () => { + cy.openFeedbackPage(); + + feedbackPage.feedbackField().type('Hello from E2E tests!'); + feedbackPage.submit().click(); + + feedbackPage.root().should('not.be.visible'); + eventsPage.root().should('be.visible'); + }); +}); diff --git a/cypress/objects/events-page.js b/cypress/objects/events-page.js index bdc18f1..025932c 100644 --- a/cypress/objects/events-page.js +++ b/cypress/objects/events-page.js @@ -10,6 +10,10 @@ class EventsPage { addEventButton() { return cy.get('[data-marker="events-page/add-event"]'); } + + feedbackButton() { + return cy.get('[data-marker="events-page/feedback"]'); + } } export const eventsPage = new EventsPage(); diff --git a/cypress/objects/feedback-page.js b/cypress/objects/feedback-page.js new file mode 100644 index 0000000..fb0cde9 --- /dev/null +++ b/cypress/objects/feedback-page.js @@ -0,0 +1,27 @@ +class FeedbackPage { + root() { + return cy.get('[data-marker="feedback-page"]'); + } + + title() { + return cy.get('[data-marker="feedback-page/title"]'); + } + + feedbackField() { + return cy.get('[data-marker="feedback-page/feedback-field"]'); + } + + emailField() { + return cy.get('[data-marker="feedback-page/email-field"]'); + } + + back() { + return cy.get('[data-marker="feedback-page/back"]'); + } + + submit() { + return cy.get('[data-marker="feedback-page/submit"]'); + } +} + +export const feedbackPage = new FeedbackPage(); diff --git a/cypress/support/commands.js b/cypress/support/commands.js deleted file mode 100644 index c1f5a77..0000000 --- a/cypress/support/commands.js +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/cypress/support/commands/index.js b/cypress/support/commands/index.js new file mode 100644 index 0000000..db7ee81 --- /dev/null +++ b/cypress/support/commands/index.js @@ -0,0 +1,14 @@ +import { eventsPage } from '../../objects/events-page'; +import { feedbackPage } from '../../objects/feedback-page'; + +Cypress.Commands.add('openFeedbackPage', () => { + cy.visit('/'); + feedbackPage.root().should('not.be.visible'); + eventsPage.root().should('be.visible'); + eventsPage.feedbackButton().should('be.visible'); + + eventsPage.feedbackButton().click(); + + eventsPage.root().should('not.be.visible'); + feedbackPage.root().should('be.visible'); +}); diff --git a/source/scripts/components/form-input/index.js b/source/scripts/components/form-input/index.js index f6a503a..0cc7895 100644 --- a/source/scripts/components/form-input/index.js +++ b/source/scripts/components/form-input/index.js @@ -3,19 +3,17 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import styles from './form-input.css'; -export default function FormInput(props) { - const { - id, - type, - size, - disabled, - invalid, - placeholder, - value, - onChange, - ...restProps - } = props; - +export default function FormInput({ + id, + type, + size, + disabled, + invalid, + placeholder, + value, + onChange, + ...restProps +}) { return ( {props.children}; } -export function TopBarHeading(props) { - const classes = ['top-bar__heading']; - - if (props.subtitle) { - classes.push('top-bar__heading--with-subtitle'); - } - +export function TopBarHeading({ subtitle, title, className, ...restProps }) { return ( -
-
{props.title}
- {props.subtitle && ( -
{props.subtitle}
- )} +
+
{title}
+ {subtitle &&
{subtitle}
}
); } @@ -31,18 +29,20 @@ TopBarHeading.propTypes = { subtitle: PropTypes.string, }; -export function TopBarIcon(props) { - const baseClass = 'top-bar__icon'; - const classes = [baseClass, `${baseClass}--${props.icon}`]; - - if (props.disabled) { - classes.push(`${baseClass}--disabled`); - } - +export function TopBarIcon({ + className, + icon, + disabled, + onClick, + ...restProps +}) { return (
); } @@ -65,6 +65,7 @@ TopBarIcon.propTypes = { 'mail', 'bordered-plus', ]).isRequired, + className: PropTypes.string, onClick: PropTypes.func, disabled: PropTypes.bool, }; diff --git a/source/scripts/pages/events/index.js b/source/scripts/pages/events/index.js new file mode 100644 index 0000000..1a4c6d0 --- /dev/null +++ b/source/scripts/pages/events/index.js @@ -0,0 +1,12 @@ +import { EventsPage as EventsPageView } from './view'; +import withRouter from 'react-router/lib/withRouter'; +import { connect } from 'react-redux'; + +const mapStateToProps = ({ events }) => ({ + isFetchingEvents: events.isFetchingEvents, + events: events.events, + eventsById: events.eventsById, + localEvents: events.localEvents, +}); + +export const EventsPage = connect(mapStateToProps)(withRouter(EventsPageView)); diff --git a/source/scripts/pages/events-page.js b/source/scripts/pages/events/view.js similarity index 70% rename from source/scripts/pages/events-page.js rename to source/scripts/pages/events/view.js index 65ef078..9d4a9c7 100644 --- a/source/scripts/pages/events-page.js +++ b/source/scripts/pages/events/view.js @@ -1,20 +1,24 @@ import React, { PureComponent } from 'react'; -import withRouter from 'react-router/lib/withRouter'; -import { connect } from 'react-redux'; - -import Page from '../components/page'; -import { TopBar, TopBarHeading, TopBarIcon } from '../components/top-bar'; -import EventsListItem from '../components/events-list-item'; -import ActionButton from '../components/action-button'; -import readEvents from '../actions/read-events'; -import FlexContainer from '../components/flex-container'; -import Poster from '../components/poster'; -import Spinner from '../components/spinner'; -import changeCurrentEvent from '../actions/change-current-event'; -import { getEventBalance } from '../modules/balance'; -import getLocalEvents from '../actions/get-local-events'; - -class EventsPage extends PureComponent { +import Page from '~/components/page'; +import { TopBar, TopBarHeading, TopBarIcon } from '~/components/top-bar'; +import EventsListItem from '~/components/events-list-item'; +import ActionButton from '~/components/action-button'; +import readEvents from '~/actions/read-events'; +import FlexContainer from '~/components/flex-container'; +import Poster from '~/components/poster'; +import Spinner from '~/components/spinner'; +import changeCurrentEvent from '~/actions/change-current-event'; +import { getEventBalance } from '~/modules/balance'; +import getLocalEvents from '~/actions/get-local-events'; + +function getEventData(eventsById) { + return eventId => ({ + eventId, + data: eventsById[eventId], + }); +} + +export class EventsPage extends PureComponent { state = { balance: {}, }; @@ -95,7 +99,11 @@ class EventsPage extends PureComponent { - + @@ -117,19 +125,3 @@ class EventsPage extends PureComponent { ); } } - -function getEventData(eventsById) { - return eventId => ({ - eventId, - data: eventsById[eventId], - }); -} - -const mapStateToProps = ({ events }) => ({ - isFetchingEvents: events.isFetchingEvents, - events: events.events, - eventsById: events.eventsById, - localEvents: events.localEvents, -}); - -export default connect(mapStateToProps)(withRouter(EventsPage)); diff --git a/source/scripts/pages/feedback-page.js b/source/scripts/pages/feedback-page.js deleted file mode 100644 index 1d5d08a..0000000 --- a/source/scripts/pages/feedback-page.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import Feedback from '../components/feedback'; - -class FeedbackPage extends React.Component { - render() { - return ; - } -} - -export default FeedbackPage; diff --git a/source/scripts/pages/feedback/index.js b/source/scripts/pages/feedback/index.js new file mode 100644 index 0000000..a6263ee --- /dev/null +++ b/source/scripts/pages/feedback/index.js @@ -0,0 +1,5 @@ +import withRouter from 'react-router/lib/withRouter'; +import { connect } from 'react-redux'; +import { FeedbackPage as FeedbackPageView } from './view'; + +export const FeedbackPage = connect()(withRouter(FeedbackPageView)); diff --git a/source/scripts/components/feedback/index.js b/source/scripts/pages/feedback/view.js similarity index 61% rename from source/scripts/components/feedback/index.js rename to source/scripts/pages/feedback/view.js index ab0e7b7..a546a09 100644 --- a/source/scripts/components/feedback/index.js +++ b/source/scripts/pages/feedback/view.js @@ -1,16 +1,14 @@ import React from 'react'; -import withRouter from 'react-router/lib/withRouter'; -import { connect } from 'react-redux'; -import FormRow from '../form-row'; -import FormLabel from '../form-label'; -import FormInput from '../form-input'; +import FormRow from '~/components/form-row'; +import FormLabel from '~/components/form-label'; +import FormInput from '~/components/form-input'; -import { saveFeedbackAsync } from '../../actions/save-feedback'; +import { saveFeedbackAsync } from '~/actions/save-feedback'; -import { TopBar, TopBarHeading, TopBarIcon } from '../top-bar'; -import Page from '../page'; +import { TopBar, TopBarHeading, TopBarIcon } from '~/components/top-bar'; +import Page from '~/components/page'; -class FeedBack extends React.Component { +export class FeedbackPage extends React.Component { state = { email: '', review: '', @@ -24,6 +22,10 @@ class FeedBack extends React.Component { saveFeedback = () => { const { state, props } = this; + if (state.review.trim() === '') { + return; + } + props.dispatch( saveFeedbackAsync({ mail: state.email, @@ -48,12 +50,20 @@ class FeedBack extends React.Component { const { email, review } = this.state; return ( - + - - + + Ваш отзыв