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,
email: string,
token: string,
refreshToken: ?string,
refreshToken: ?string
};
export type State = {
active: ?number,
available: Array<Account>,
available: Array<Account>
};
export type AddAction = { type: 'accounts:add', payload: Account};
export type RemoveAction = { type: 'accounts:remove', payload: Account};
export type ActivateAction = { type: 'accounts:activate', payload: Account};
export type UpdateTokenAction = { type: 'accounts:updateToken', payload: string };
export type AddAction = { type: 'accounts:add', payload: Account };
export type RemoveAction = { type: 'accounts:remove', payload: Account };
export type ActivateAction = { type: 'accounts:activate', payload: Account };
export type UpdateTokenAction = {
type: 'accounts:updateToken',
payload: string
};
export type ResetAction = { type: 'accounts:reset' };
type Action =
@ -27,14 +29,14 @@ type Action =
| ResetAction;
export function getActiveAccount(state: { accounts: State }): ?Account {
const activeAccount = 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;
const accountId = state.accounts.active;
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;
}
@ -47,8 +49,14 @@ export default function accounts(
): State {
switch (action.type) {
case 'accounts:add': {
if (!action.payload || !action.payload.id || !action.payload.token) {
throw new Error('Invalid or empty payload passed for accounts.add');
if (
!action.payload
|| !action.payload.id
|| !action.payload.token
) {
throw new Error(
'Invalid or empty payload passed for accounts.add'
);
}
const { payload } = action;
@ -68,8 +76,14 @@ export default function accounts(
}
case 'accounts:activate': {
if (!action.payload || !action.payload.id || !action.payload.token) {
throw new Error('Invalid or empty payload passed for accounts.add');
if (
!action.payload
|| !action.payload.id
|| !action.payload.token
) {
throw new Error(
'Invalid or empty payload passed for accounts.add'
);
}
const { payload } = action;
@ -77,10 +91,10 @@ export default function accounts(
return {
available: state.available.map((account) => {
if (account.id === payload.id) {
return {...payload};
return { ...payload };
}
return {...account};
return { ...account };
}),
active: payload.id
};
@ -94,14 +108,18 @@ export default function accounts(
case 'accounts:remove': {
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;
return {
...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) {
return {
...account,
token: payload,
token: payload
};
}
return {...account};
}),
return { ...account };
})
};
}

View File

@ -48,7 +48,7 @@ export class ContactForm extends Component {
const {onClose} = this.props;
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.header}>
<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 ContactForm from './ContactForm';
function ContactLink({createContactPopup, ...props}: {
function ContactLink({
createContactPopup,
...props
}: {
createContactPopup: () => void,
props: Object
}) {
return (
<a href="#" onClick={(event) => {
event.preventDefault();
<a
href="#"
data-e2e-button="feedbackPopup"
onClick={(event) => {
event.preventDefault();
createContactPopup();
}} {...props} />
createContactPopup();
}}
{...props}
/>
);
}
export default connect(null, {
createContactPopup: () => createPopup(ContactForm),
})(ContactLink);
export default connect(
null,
{
createContactPopup: () => createPopup(ContactForm)
}
)(ContactLink);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,13 @@
{
"account1": {
"id": "7",
"username": "SleepWalker",
"email": "danilenkos@auroraglobal.com",
"login": "SleepWalker",
"password": "qwer1234"
},
"account2": {
"id": 102,
"username": "test",
"email": "admin@udf.su",
"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
// create various custom commands and overwrite
@ -23,3 +25,69 @@
//
// -- This is will overwrite an existing command --
// 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'
}
};
}