web/satellite routing updated, tests added (#3113)
This commit is contained in:
parent
2c5e169888
commit
fd54cc80d0
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
21
web/satellite/tests/unit/mock/api/apiKeys.ts
Normal file
21
web/satellite/tests/unit/mock/api/apiKeys.ts
Normal 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');
|
||||
}
|
||||
}
|
13
web/satellite/tests/unit/mock/api/buckets.ts
Normal file
13
web/satellite/tests/unit/mock/api/buckets.ts
Normal 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.');
|
||||
}
|
||||
}
|
@ -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> {
|
||||
|
13
web/satellite/tests/unit/mock/api/usage.ts
Normal file
13
web/satellite/tests/unit/mock/api/usage.ts
Normal 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.');
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user