Some minor fixes and e2e tests for creating of website app

This commit is contained in:
SleepWalker 2018-11-10 11:03:47 +02:00
parent 19453584d0
commit 0ad3499609
14 changed files with 240 additions and 46 deletions

View File

@ -4,19 +4,21 @@ export type Account = {
username: string, username: string,
email: string, email: string,
token: string, token: string,
refreshToken: ?string, refreshToken: ?string
}; };
export type State = { export type State = {
active: ?number, active: ?number,
available: Array<Account>, available: Array<Account>
}; };
export type AddAction = { type: 'accounts:add', payload: Account };
export type AddAction = { type: 'accounts:add', payload: Account}; export type RemoveAction = { type: 'accounts:remove', payload: Account };
export type RemoveAction = { type: 'accounts:remove', payload: Account}; export type ActivateAction = { type: 'accounts:activate', payload: Account };
export type ActivateAction = { type: 'accounts:activate', payload: Account}; export type UpdateTokenAction = {
export type UpdateTokenAction = { type: 'accounts:updateToken', payload: string }; type: 'accounts:updateToken',
payload: string
};
export type ResetAction = { type: 'accounts:reset' }; export type ResetAction = { type: 'accounts:reset' };
type Action = type Action =
@ -27,14 +29,14 @@ type Action =
| ResetAction; | ResetAction;
export function getActiveAccount(state: { accounts: State }): ?Account { export function getActiveAccount(state: { accounts: State }): ?Account {
const activeAccount = state.accounts.active; const accountId = state.accounts.active;
// TODO: remove activeAccount.id, when will be sure, that magor part of users have migrated to new state structure
const accountId: number | void = typeof activeAccount === 'number' ? activeAccount : (activeAccount || {}).id;
return state.accounts.available.find((account) => account.id === accountId); return state.accounts.available.find((account) => account.id === accountId);
} }
export function getAvailableAccounts(state: { accounts: State }): Array<Account> { export function getAvailableAccounts(state: {
accounts: State
}): Array<Account> {
return state.accounts.available; return state.accounts.available;
} }
@ -47,8 +49,14 @@ export default function accounts(
): State { ): State {
switch (action.type) { switch (action.type) {
case 'accounts:add': { case 'accounts:add': {
if (!action.payload || !action.payload.id || !action.payload.token) { if (
throw new Error('Invalid or empty payload passed for accounts.add'); !action.payload
|| !action.payload.id
|| !action.payload.token
) {
throw new Error(
'Invalid or empty payload passed for accounts.add'
);
} }
const { payload } = action; const { payload } = action;
@ -68,8 +76,14 @@ export default function accounts(
} }
case 'accounts:activate': { case 'accounts:activate': {
if (!action.payload || !action.payload.id || !action.payload.token) { if (
throw new Error('Invalid or empty payload passed for accounts.add'); !action.payload
|| !action.payload.id
|| !action.payload.token
) {
throw new Error(
'Invalid or empty payload passed for accounts.add'
);
} }
const { payload } = action; const { payload } = action;
@ -77,10 +91,10 @@ export default function accounts(
return { return {
available: state.available.map((account) => { available: state.available.map((account) => {
if (account.id === payload.id) { if (account.id === payload.id) {
return {...payload}; return { ...payload };
} }
return {...account}; return { ...account };
}), }),
active: payload.id active: payload.id
}; };
@ -94,14 +108,18 @@ export default function accounts(
case 'accounts:remove': { case 'accounts:remove': {
if (!action.payload || !action.payload.id) { if (!action.payload || !action.payload.id) {
throw new Error('Invalid or empty payload passed for accounts.remove'); throw new Error(
'Invalid or empty payload passed for accounts.remove'
);
} }
const { payload } = action; const { payload } = action;
return { return {
...state, ...state,
available: state.available.filter((account) => account.id !== payload.id) available: state.available.filter(
(account) => account.id !== payload.id
)
}; };
} }
@ -118,12 +136,12 @@ export default function accounts(
if (account.id === state.active) { if (account.id === state.active) {
return { return {
...account, ...account,
token: payload, token: payload
}; };
} }
return {...account}; return { ...account };
}), })
}; };
} }

View File

@ -48,7 +48,7 @@ export class ContactForm extends Component {
const {onClose} = this.props; const {onClose} = this.props;
return ( return (
<div className={isSuccessfullySent ? styles.successState : styles.contactForm}> <div data-e2e="feedbackPopup" className={isSuccessfullySent ? styles.successState : styles.contactForm}>
<div className={popupStyles.popup}> <div className={popupStyles.popup}>
<div className={popupStyles.header}> <div className={popupStyles.header}>
<h2 className={popupStyles.headerTitle}> <h2 className={popupStyles.headerTitle}>

View File

@ -4,19 +4,30 @@ import { connect } from 'react-redux';
import { create as createPopup } from 'components/ui/popup/actions'; import { create as createPopup } from 'components/ui/popup/actions';
import ContactForm from './ContactForm'; import ContactForm from './ContactForm';
function ContactLink({createContactPopup, ...props}: { function ContactLink({
createContactPopup,
...props
}: {
createContactPopup: () => void, createContactPopup: () => void,
props: Object props: Object
}) { }) {
return ( return (
<a href="#" onClick={(event) => { <a
href="#"
data-e2e-button="feedbackPopup"
onClick={(event) => {
event.preventDefault(); event.preventDefault();
createContactPopup(); createContactPopup();
}} {...props} /> }}
{...props}
/>
); );
} }
export default connect(null, { export default connect(
createContactPopup: () => createPopup(ContactForm), null,
})(ContactLink); {
createContactPopup: () => createPopup(ContactForm)
}
)(ContactLink);

View File

@ -22,7 +22,7 @@ type Props = {
applications: Array<OauthAppResponse>, applications: Array<OauthAppResponse>,
isLoading: bool, isLoading: bool,
deleteApp: string => Promise<any>, deleteApp: string => Promise<any>,
resetApp: (string, bool) => Promise<any>, resetApp: (string, bool) => Promise<any>
}; };
export default class ApplicationsIndex extends Component<Props> { export default class ApplicationsIndex extends Component<Props> {
@ -139,7 +139,7 @@ function Guest() {
function NoApps() { function NoApps() {
return ( return (
<div className={styles.emptyState}> <div data-e2e="noApps" className={styles.emptyState}>
<img src={cubeIcon} className={styles.emptyStateIcon} /> <img src={cubeIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}> <div className={styles.emptyStateText}>
<div> <div>
@ -152,6 +152,7 @@ function NoApps() {
<LinkButton <LinkButton
to="/dev/applications/new" to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew} label={messages.addNew}
color={COLOR_GREEN} color={COLOR_GREEN}
className={styles.emptyStateActionButton} className={styles.emptyStateActionButton}

View File

@ -63,6 +63,8 @@ export default class ApplicationItem extends Component<
className={classNames(styles.appItemContainer, { className={classNames(styles.appItemContainer, {
[styles.appExpanded]: expand [styles.appExpanded]: expand
})} })}
data-e2e="appItem"
data-e2e-app={app.clientId}
> >
<div className={styles.appItemTile} onClick={this.onTileToggle}> <div className={styles.appItemTile} onClick={this.onTileToggle}>
<div className={styles.appTileTitle}> <div className={styles.appTileTitle}>

View File

@ -49,6 +49,7 @@ export default class ApplicationsList extends React.Component<Props, State> {
</div> </div>
<LinkButton <LinkButton
to="/dev/applications/new" to="/dev/applications/new"
data-e2e="newApp"
label={messages.addNew} label={messages.addNew}
color={COLOR_GREEN} color={COLOR_GREEN}
className={styles.appsListAddNewAppBtn} className={styles.appsListAddNewAppBtn}

View File

@ -5,10 +5,10 @@ import { Link } from 'react-router-dom';
import Button from './Button'; import Button from './Button';
export default function LinkButton(props: ElementProps<typeof Button> & ElementProps<typeof Link>) { export default function LinkButton(
const {to, ...restProps} = props; props: ElementProps<typeof Button> & ElementProps<typeof Link>
) {
const { to, ...restProps } = props;
return ( return <Button component={Link} to={to} {...restProps} />;
<Button component={Link} to={to} {...restProps} />
);
} }

View File

@ -7,7 +7,7 @@ import type { ComponentType } from 'react';
import type { Account } from 'components/accounts'; import type { Account } from 'components/accounts';
const PrivateRoute = ({account, component: Component, ...rest}: { const PrivateRoute = ({account, component: Component, ...rest}: {
component: ComponentType<*>, component: ComponentType<any>,
account: ?Account account: ?Account
}) => ( }) => (
<Route {...rest} render={(props: {location: string}) => ( <Route {...rest} render={(props: {location: string}) => (

View File

@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom'; import { Redirect, Route, Switch } from 'react-router-dom';
import { FooterMenu } from 'components/footerMenu'; import { FooterMenu } from 'components/footerMenu';
import PrivateRoute from 'containers/PrivateRoute';
import styles from './dev.scss'; import styles from './dev.scss';
import ApplicationsListPage from './ApplicationsListPage'; import ApplicationsListPage from './ApplicationsListPage';
@ -11,12 +12,25 @@ import UpdateApplicationPage from './UpdateApplicationPage';
export default function DevPage() { export default function DevPage() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div data-e2e-content>
<Switch> <Switch>
<Route path="/dev/applications" exact component={ApplicationsListPage} /> <Route
<Route path="/dev/applications/new" exact component={CreateNewApplicationPage} /> path="/dev/applications"
<Route path="/dev/applications/:clientId" component={UpdateApplicationPage} /> exact
component={ApplicationsListPage}
/>
<PrivateRoute
path="/dev/applications/new"
exact
component={CreateNewApplicationPage}
/>
<PrivateRoute
path="/dev/applications/:clientId"
component={UpdateApplicationPage}
/>
<Redirect to="/dev/applications" /> <Redirect to="/dev/applications" />
</Switch> </Switch>
</div>
<div className={styles.footer}> <div className={styles.footer}>
<FooterMenu /> <FooterMenu />

View File

@ -49,7 +49,7 @@ type FormPayloads = {
minecraftServerIp?: string, minecraftServerIp?: string,
}; };
export default { const api = {
validate(oauthData: OauthData) { validate(oauthData: OauthData) {
return request.get( return request.get(
'/api/oauth2/v1/validate', '/api/oauth2/v1/validate',
@ -116,6 +116,13 @@ export default {
return request.delete(`/api/v1/oauth2/${clientId}`); return request.delete(`/api/v1/oauth2/${clientId}`);
}, },
}; };
if (window.Cypress) {
window.oauthApi = api;
}
export default api;
/** /**
* @param {object} oauthData * @param {object} oauthData
* @param {string} oauthData.clientId * @param {string} oauthData.clientId

View File

@ -1,11 +1,13 @@
{ {
"account1": { "account1": {
"id": "7",
"username": "SleepWalker", "username": "SleepWalker",
"email": "danilenkos@auroraglobal.com", "email": "danilenkos@auroraglobal.com",
"login": "SleepWalker", "login": "SleepWalker",
"password": "qwer1234" "password": "qwer1234"
}, },
"account2": { "account2": {
"id": 102,
"username": "test", "username": "test",
"email": "admin@udf.su", "email": "admin@udf.su",
"login": "test", "login": "test",

View File

@ -0,0 +1,28 @@
describe('/dev/applications - guest', () => {
it('should render login button', () => {
cy.visit('/dev/applications');
cy.get('[data-e2e-content] [href="/login"]').click();
cy.url().should('include', '/login');
});
it('should not allow create new app', () => {
cy.visit('/dev/applications/new');
cy.url().should('include', '/login');
});
it('should not allow edit app', () => {
cy.visit('/dev/applications/foo-bar');
cy.url().should('include', '/login');
});
it('should have feedback popup link', () => {
cy.visit('/dev/applications');
cy.get('[data-e2e-content] [data-e2e-button="feedbackPopup"]').click();
cy.get('[data-e2e="feedbackPopup"]').should('be.visible');
});
});

View File

@ -0,0 +1,42 @@
describe('/dev/applications - user', () => {
before(() => {
cy.login({ account: 'default' }).then(({ user }) => {
cy.visit('/dev/applications');
// remove all previousely added apps
cy.window().then(async ({ oauthApi }) => {
const apps = await oauthApi.getAppsByUser(user.id);
await Promise.all(
apps.map((app) => oauthApi.delete(app.clientId))
);
});
});
});
// TODO: test the first screen is without any list rendered
// TODO: test validation
it('should add website app', () => {
cy.visit('/dev/applications');
cy.get('[data-e2e="noApps"]').should('exist');
cy.get('[data-e2e="newApp"]').click();
cy.url().should('include', '/dev/applications/new');
cy.get('[value="application"]').check({ force: true });
cy.get('[name="name"]').type('The Foo');
cy.get('[name="description"]').type('The Foo Description');
cy.get('[name="websiteUrl"]').type('https://ely.by');
cy.get('[name="redirectUri"]').type('https://ely.by/the/redirect/uri');
cy.get('[type="submit"]').click();
cy.url().should('include', '/dev/applications#the-foo');
cy.get('[data-e2e-app="the-foo"]').should('exist');
});
});

View File

@ -1,3 +1,5 @@
import { account1, account2 } from '../fixtures/accounts.json';
// *********************************************** // ***********************************************
// This example commands.js shows you how to // This example commands.js shows you how to
// create various custom commands and overwrite // create various custom commands and overwrite
@ -23,3 +25,69 @@
// //
// -- This is will overwrite an existing command -- // -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
const accountsMap = {
// default: account1,
default: account2
};
Cypress.Commands.add('login', async ({ login, password, account }) => {
let credentials;
if (account) {
credentials = accountsMap[account];
if (!credentials) {
throw new Error(`Unknown account name: ${account}`);
}
}
const resp = await fetch('/api/authentication/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: `login=${credentials.login}&password=${credentials.password}&rememberMe=1`
}).then((resp) => resp.json());
const state = createState([
{
id: credentials.id,
username: credentials.username,
email: credentials.email,
token: resp.access_token,
refreshToken: resp.refresh_token
}
]);
localStorage.setItem('redux-storage', JSON.stringify(state));
return state;
});
function createState(accounts) {
return {
accounts: {
available: accounts,
active: accounts[0].id
},
user: {
id: 102,
uuid: 'e49cafdc-6e0c-442d-b608-dacdb864ee34',
username: 'test',
token: '',
email: 'admin@udf.su',
maskedEmail: '',
avatar: '',
lang: 'en',
isActive: true,
isOtpEnabled: true,
shouldAcceptRules: false,
passwordChangedAt: 1478961317,
hasMojangUsernameCollision: true,
isGuest: false,
registeredAt: 1478961317,
elyProfileLink: 'http://ely.by/u102'
}
};
}