mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-10 18:12:10 +05:30
Some minor fixes and e2e tests for creating of website app
This commit is contained in:
parent
19453584d0
commit
0ad3499609
@ -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 };
|
||||||
}),
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}>
|
||||||
|
@ -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);
|
||||||
|
@ -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}
|
||||||
|
@ -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}>
|
||||||
|
@ -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}
|
||||||
|
@ -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} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -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}) => (
|
||||||
|
@ -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 />
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
28
tests-e2e/cypress/integration/dev/guest.test.js
Normal file
28
tests-e2e/cypress/integration/dev/guest.test.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
42
tests-e2e/cypress/integration/dev/user.test.js
Normal file
42
tests-e2e/cypress/integration/dev/user.test.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
@ -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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user