web/satellite routing updated, tests added (#3113)

This commit is contained in:
Yehor Butko 2019-09-27 17:41:04 +03:00 committed by GitHub
parent 2c5e169888
commit fd54cc80d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 235 additions and 76 deletions

View File

@ -6,7 +6,8 @@
"serve": "vue-cli-service serve",
"lint": "vue-cli-service lint",
"test": "vue-cli-service test:unit",
"build": "vue-cli-service build"
"build": "vue-cli-service build",
"dev": "vue-cli-service build --mode development"
},
"dependencies": {
"apollo-cache-inmemory": "1.6.3",

View File

@ -26,6 +26,9 @@ import RegisterArea from '@/views/register/RegisterArea.vue';
Vue.use(Router);
/**
* RouteConfig contains information about all routes and subroutes
*/
export abstract class RouteConfig {
// root paths
public static Root = new NavigationLink('/', 'Root');
@ -158,13 +161,13 @@ router.beforeEach((to, from, next) => {
}
}
if (navigateToFirstSubTab(to.matched, RouteConfig.Account, RouteConfig.Profile)) {
if (navigateToDefaultSubTab(to.matched, RouteConfig.Account)) {
next(RouteConfig.Account.with(RouteConfig.Profile).path);
return;
}
if (navigateToFirstSubTab(to.matched, RouteConfig.ProjectOverview, RouteConfig.ProjectDetails)) {
if (navigateToDefaultSubTab(to.matched, RouteConfig.ProjectOverview)) {
next(RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails).path);
return;
@ -172,6 +175,8 @@ router.beforeEach((to, from, next) => {
if (to.name === 'default') {
next(RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails).path);
return;
}
next();
@ -183,9 +188,8 @@ router.beforeEach((to, from, next) => {
* @param routes - array of RouteRecord from vue-router
* @param next - callback to process next route
* @param tabRoute - tabNavigator route
* @param subTabRoute - default sub route of the tabNavigator
*/
function navigateToFirstSubTab(routes: RouteRecord[], tabRoute: NavigationLink, subTabRoute: NavigationLink): boolean {
function navigateToDefaultSubTab(routes: RouteRecord[], tabRoute: NavigationLink): boolean {
return routes.length === 2 && (routes[1].name as string) === tabRoute.name;
}

View File

@ -1,9 +1,8 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ProjectUsageApiGql } from '@/api/usage';
import { StoreModule } from '@/store';
import { DateRange, ProjectUsage } from '@/types/usage';
import { DateRange, ProjectUsage, UsageApi } from '@/types/usage';
export const PROJECT_USAGE_ACTIONS = {
FETCH: 'fetchProjectUsage',
@ -26,7 +25,7 @@ class UsageState {
public endDate: Date = new Date();
}
export function makeUsageModule(api: ProjectUsageApiGql): StoreModule<UsageState> {
export function makeUsageModule(api: UsageApi): StoreModule<UsageState> {
return {
state: new UsageState(),
mutations: {

View File

@ -47,85 +47,89 @@ import { AppState } from '@/utils/constants/appStateEnum';
},
})
export default class DashboardArea extends Vue {
public mounted(): void {
setTimeout(async () => {
// TODO: combine all project related requests in one
try {
await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.ERROR);
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
await this.$router.push(RouteConfig.Login.path);
AuthToken.remove();
public async mounted(): Promise<void> {
// TODO: combine all project related requests in one
try {
await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.ERROR);
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
await this.$router.push(RouteConfig.Login.path);
AuthToken.remove();
return;
}
return;
}
let projects: Project[] = [];
let projects: Project[] = [];
try {
projects = await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
try {
projects = await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
return;
}
return;
}
if (!projects.length) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED_EMPTY);
if (!this.isCurrentRouteIsAccount) {
await this.$router.push(RouteConfig.ProjectOverview.path);
if (!projects.length) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED_EMPTY);
if (!this.isRouteAccessibleWithoutProject()) {
try {
await this.$router.push(RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails).path);
} catch (err) {
return;
}
await this.$router.push(RouteConfig.ProjectOverview.path);
}
await this.$store.dispatch(PROJECTS_ACTIONS.SELECT, projects[0].id);
return;
}
await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
try {
await this.$store.dispatch(PM_ACTIONS.FETCH, 1);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${error.message}`);
}
await this.$store.dispatch(PROJECTS_ACTIONS.SELECT, projects[0].id);
try {
await this.$store.dispatch(API_KEYS_ACTIONS.FETCH, 1);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch api keys. ${error.message}`);
}
await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
try {
await this.$store.dispatch(PM_ACTIONS.FETCH, 1);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${error.message}`);
}
try {
await this.$store.dispatch(PROJECT_USAGE_ACTIONS.FETCH_CURRENT_ROLLUP);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project usage. ${error.message}`);
}
try {
await this.$store.dispatch(API_KEYS_ACTIONS.FETCH, 1);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch api keys. ${error.message}`);
}
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, 1);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch buckets: ' + error.message);
}
try {
await this.$store.dispatch(PROJECT_USAGE_ACTIONS.FETCH_CURRENT_ROLLUP);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project usage. ${error.message}`);
}
const paymentMethodsResponse = await this.$store.dispatch(PROJECT_PAYMENT_METHODS_ACTIONS.FETCH);
if (!paymentMethodsResponse.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch payment methods: ' + paymentMethodsResponse.errorMessage);
}
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, 1);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch buckets. ${error.message}`);
}
this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
}, 800);
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
}
public get isLoading(): boolean {
return this.$store.state.appStateModule.appState.fetchState === AppState.LOADING;
}
public get isCurrentRouteIsAccount(): boolean {
const segments = this.$route.path.split('/').map(segment => segment.toLowerCase());
return segments.includes(RouteConfig.Account.name.toLowerCase());
/**
* This method checks if current route is available when user has no created projects
*/
private isRouteAccessibleWithoutProject(): boolean {
const awailableRoutes = [
RouteConfig.Account.with(RouteConfig.Billing).path,
RouteConfig.Account.with(RouteConfig.Profile).path,
RouteConfig.Account.with(RouteConfig.PaymentMethods).path,
RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails).path,
];
return awailableRoutes.includes(this.$router.currentRoute.path.toLowerCase());
}
}
</script>

View File

@ -14,7 +14,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ProjectsApiMock } from '../../mock/api/projects';
const api = new ProjectsApiMock();
api.setMockProject(new Project('1'));
api.setMockProjects([new Project('1')]);
const projectsModule = makeProjectsModule(api);
const localVue = createLocalVue();

View File

@ -0,0 +1,21 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ApiKey, ApiKeyCursor, ApiKeysApi, ApiKeysPage } from '@/types/apiKeys';
/**
* Mock for ApiKeysApi
*/
export class ApiKeysMock implements ApiKeysApi {
get(projectId: string, cursor: ApiKeyCursor): Promise<ApiKeysPage> {
throw new Error('Method not implemented');
}
create(projectId: string, name: string): Promise<ApiKey> {
throw new Error('Method not implemented');
}
delete(ids: string[]): Promise<void> {
throw new Error('Method not implemented');
}
}

View File

@ -0,0 +1,13 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { BucketCursor, BucketPage, BucketsApi } from '@/types/buckets';
/**
* Mock for BucketsApi
*/
export class BucketsMock implements BucketsApi {
get(projectId: string, before: Date, cursor: BucketCursor): Promise<BucketPage> {
throw new Error('Method not implemented.');
}
}

View File

@ -4,13 +4,13 @@
import { CreateProjectModel, Project, ProjectsApi } from '@/types/projects';
/**
* Mock for CreditsApi
* Mock for ProjectsApi
*/
export class ProjectsApiMock implements ProjectsApi {
private mockProject: Project;
private mockProjects: Project[];
public setMockProject(mockCredits: Project): void {
this.mockProject = mockCredits;
public setMockProjects(mockProjects: Project[]): void {
this.mockProjects = mockProjects;
}
create(createProjectModel: CreateProjectModel): Promise<Project> {
@ -22,10 +22,7 @@ export class ProjectsApiMock implements ProjectsApi {
}
get(): Promise<Project[]> {
const result = Array<Project>();
result.push(this.mockProject);
return Promise.resolve(result);
return Promise.resolve(this.mockProjects);
}
update(projectId: string, description: string): Promise<void> {

View File

@ -0,0 +1,13 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ProjectUsage, UsageApi } from '@/types/usage';
/**
* Mock for UsageApi
*/
export class ProjectUsageMock implements UsageApi {
get(projectId: string, since: Date, before: Date): Promise<ProjectUsage> {
throw new Error('Method not implemented.');
}
}

View File

@ -3,23 +3,69 @@
import Vuex from 'vuex';
import router, { RouteConfig } from '@/router';
import { makeApiKeysModule } from '@/store/modules/apiKeys';
import { appStateModule } from '@/store/modules/appState';
import { makeBucketsModule } from '@/store/modules/buckets';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { makeProjectMembersModule } from '@/store/modules/projectMembers';
import { makeProjectsModule } from '@/store/modules/projects';
import { makeUsageModule } from '@/store/modules/usage';
import { makeUsersModule } from '@/store/modules/users';
import { User } from '@/types/users';
import { AuthToken } from '@/utils/authToken';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { AppState } from '@/utils/constants/appStateEnum';
import DashboardArea from '@/views/DashboardArea.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
const localVue = createLocalVue();
import { ApiKeysMock } from '../mock/api/apiKeys';
import { BucketsMock } from '../mock/api/buckets';
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
import { ProjectsApiMock } from '../mock/api/projects';
import { ProjectUsageMock } from '../mock/api/usage';
import { UsersApiMock } from '../mock/api/users';
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store({ modules: { appStateModule } });
const usersApi = new UsersApiMock();
const projectsApi = new ProjectsApiMock();
usersApi.setMockUser(new User('1', '2', '3', '4', '5'));
projectsApi.setMockProjects([]);
const usersModule = makeUsersModule(usersApi);
const projectsModule = makeProjectsModule(projectsApi);
const apiKeysModule = makeApiKeysModule(new ApiKeysMock());
const teamMembersModule = makeProjectMembersModule(new ProjectMembersApiMock());
const bucketsModule = makeBucketsModule(new BucketsMock());
const usageModule = makeUsageModule(new ProjectUsageMock());
const notificationsModule = makeNotificationsModule();
const store = new Vuex.Store({
modules: {
notificationsModule,
usageModule,
bucketsModule,
apiKeysModule,
usersModule,
projectsModule,
appStateModule,
teamMembersModule,
},
});
describe('Dashboard', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('renders correctly when data is loading', () => {
const wrapper = shallowMount(DashboardArea, {
store,
localVue,
router,
});
expect(wrapper).toMatchSnapshot();
@ -33,10 +79,71 @@ describe('Dashboard', () => {
const wrapper = shallowMount(DashboardArea, {
store,
localVue,
router,
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.findAll('.loading-overlay active').length).toBe(0);
expect(wrapper.findAll('.dashboard-container__wrap').length).toBe(1);
});
it('loads routes correctly when authorithed without project with available routes', async () => {
jest.spyOn(AuthToken, 'get').mockReturnValue('authToken');
const availableWithoutProject = [
RouteConfig.Account.with(RouteConfig.Billing).path,
RouteConfig.Account.with(RouteConfig.Profile).path,
RouteConfig.Account.with(RouteConfig.PaymentMethods).path,
];
for (let i = 0; i < availableWithoutProject.length; i++) {
const wrapper = await shallowMount(DashboardArea, {
localVue,
router,
store,
});
setTimeout(() => {
expect(wrapper.vm.$router.currentRoute.path).toBe(availableWithoutProject[i]);
}, 50);
}
});
it('loads routes correctly when authorithed without project with unavailable routes', async () => {
jest.spyOn(AuthToken, 'get').mockReturnValue('authToken');
const unavailableWithoutProject = [
RouteConfig.ApiKeys.path,
RouteConfig.Buckets.path,
RouteConfig.Team.path,
RouteConfig.ProjectOverview.with(RouteConfig.UsageReport).path,
];
for (let i = 0; i < unavailableWithoutProject.length; i++) {
await router.push(unavailableWithoutProject[i]);
const wrapper = await shallowMount(DashboardArea, {
localVue,
router,
store,
});
setTimeout(() => {
expect(wrapper.vm.$router.currentRoute.path).toBe(RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails).path);
}, 50);
}
});
it('loads routes correctly when not authorithed', () => {
const wrapper = shallowMount(DashboardArea, {
store,
localVue,
router,
});
setTimeout(() => {
expect(wrapper.vm.$router.currentRoute.path).toBe(RouteConfig.Login.path);
}, 50);
});
});