web/satellite: navigation, button and project members unit tests (#2904)

This commit is contained in:
Yehor Butko 2019-08-29 17:49:34 +03:00 committed by GitHub
parent 368f6cc320
commit 5bb51c9876
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 735 additions and 318 deletions

View File

@ -29,8 +29,7 @@
private readonly isDeletion: boolean;
@Prop({default: false})
private isDisabled: boolean;
// TODO: improve default implementation
@Prop({default: () => console.error('onPress is not reinitialized')})
@Prop({default: new Function()})
private readonly onPress: Function;
public get style(): Object {

View File

@ -5,16 +5,10 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import AddUserPopup from '@/components/team/AddUserPopup.vue';
import { NavigationLink } from '@/types/navigation';
import { RouteConfig } from '@/router';
@Component({
components: {
AddUserPopup,
}
})
@Component
export default class NavigationArea extends Vue {
// TODO: Use SvgLoaderComponent to reduce markup lines
public readonly navigation: NavigationLink[] = [
@ -61,10 +55,6 @@
location.reload();
}
public get isAddTeamMembersPopupShown(): boolean {
return this.$store.state.appStateModule.appState.isAddTeamMembersPopupShown;
}
public get isProjectNotSelected(): boolean {
return this.$store.state.projectsModule.selectedProject.id === '';
}

View File

@ -49,5 +49,4 @@
</div>
</a>
</div>
<AddUserPopup v-if="isAddTeamMembersPopupShown"/>
</div>

View File

@ -15,7 +15,7 @@
<Button class="button" label="Cancel" width="122px" height="48px" isWhite="true" :onPress="onClearSelection"/>
</div>
<div class="header-after-delete-click" v-if="headerState === 1 && isDeleteClicked">
<span>Are you sure you want to delete {{selectedProjectMembers}} {{userCountTitle}} ?</span>
<span>Are you sure you want to delete {{selectedProjectMembersCount}} {{userCountTitle}}?</span>
<div class="header-after-delete-click__button-area">
<Button class="button deletion" label="Delete" width="122px" height="48px" :onPress="onDelete"/>
<Button class="button" label="Cancel" width="122px" height="48px" isWhite="true" :onPress="onClearSelection"/>
@ -25,6 +25,7 @@
<div class="blur-content" v-if="isDeleteClicked"></div>
<div class="blur-search" v-if="isDeleteClicked"></div>
</div>
<AddUserPopup v-if="isAddTeamMembersPopupShown"/>
</div>
</template>
@ -34,7 +35,8 @@
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
import Button from '@/components/common/Button.vue';
import HeaderComponent from '@/components/common/HeaderComponent.vue';
import { ProjectMember } from '@/types/projectMembers';
import { ProjectMember, ProjectMemberHeaderState } from '@/types/projectMembers';
import AddUserPopup from '@/components/team/AddUserPopup.vue';
declare interface ClearSearch {
clearSearch: () => void;
@ -44,24 +46,25 @@
components: {
Button,
HeaderComponent,
AddUserPopup,
}
})
export default class HeaderArea extends Vue {
@Prop({default: ProjectMemberHeaderState.DEFAULT})
private readonly headerState: ProjectMemberHeaderState;
@Prop({default: 0})
private readonly headerState: number;
@Prop({default: 0})
private readonly selectedProjectMembers: number;
public readonly selectedProjectMembersCount: number;
private FIRST_PAGE = 1;
private isDeleteClicked: boolean = false;
public isDeleteClicked: boolean = false;
public $refs!: {
headerComponent: HeaderComponent & ClearSearch
};
public get userCountTitle(): string {
if (this.selectedProjectMembers === 1) {
if (this.selectedProjectMembersCount === 1) {
return 'user';
}
@ -84,7 +87,7 @@
}
public async onDelete(): Promise<void> {
const projectMemberEmails = this.$store.getters.selectedProjectMembers.map((member: ProjectMember) => {
const projectMemberEmails: string[] = this.$store.getters.selectedProjectMembers.map((member: ProjectMember) => {
return member.user.email;
});
@ -110,6 +113,10 @@
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${err.message}`);
}
}
public get isAddTeamMembersPopupShown(): boolean {
return this.$store.state.appStateModule.appState.isAddTeamMembersPopupShown;
}
}
</script>

View File

@ -4,7 +4,7 @@
<template>
<div class="team-area">
<div class="team-area__header">
<HeaderArea :headerState="headerState" :selectedProjectMembers="selectedProjectMembers.length"/>
<HeaderArea :headerState="headerState" :selectedProjectMembersCount="selectedProjectMembers.length"/>
</div>
<div class="team-area__container" id="team-container" v-if="projectMembersCount > 0 || projectMembersTotalCount > 0">
<SortingListHeader :onHeaderClickCallback="onHeaderSectionClickCallback"/>
@ -64,15 +64,10 @@
import HeaderArea from '@/components/team/HeaderArea.vue';
import ProjectMemberListItem from '@/components/team/ProjectMemberListItem.vue';
import SortingListHeader from '@/components/team/SortingListHeader.vue';
import { ProjectMember, ProjectMemberOrderBy } from '@/types/projectMembers';
import { ProjectMember, ProjectMemberHeaderState, ProjectMemberOrderBy } from '@/types/projectMembers';
import { SortDirection } from '@/types/common';
import { NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
enum HeaderState {
DEFAULT = 0,
ON_SELECT,
}
@Component({
components: {
HeaderArea,
@ -114,10 +109,10 @@
public get headerState(): number {
if (this.selectedProjectMembers.length > 0) {
return HeaderState.ON_SELECT;
return ProjectMemberHeaderState.ON_SELECT;
}
return HeaderState.DEFAULT;
return ProjectMemberHeaderState.DEFAULT;
}
public async onPageClick(index: number):Promise<void> {

View File

@ -1,8 +1,7 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ProjectsApiGql } from '@/api/projects';
import { CreateProjectModel, Project, UpdateProjectModel } from '@/types/projects';
import { CreateProjectModel, Project, ProjectsApi, UpdateProjectModel } from '@/types/projects';
import { StoreModule } from '@/store';
export const PROJECTS_ACTIONS = {
@ -48,7 +47,7 @@ const {
CLEAR_PROJECTS,
} = PROJECTS_MUTATIONS;
export function makeProjectsModule(api: ProjectsApiGql): StoreModule<ProjectsState> {
export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState> {
return {
state: new ProjectsState(),
mutations: {

View File

@ -13,6 +13,18 @@ export enum ProjectMemberOrderBy {
CREATED_AT,
}
/**
* Contains values of project members header component state
* used in ProjectMembersArea and HeaderArea.
*/
export enum ProjectMemberHeaderState {
DEFAULT = 0,
/**
* Used when some project members selected
*/
ON_SELECT,
}
/**
* ProjectMembersApi is a graphql implementation of ProjectMembers API.
* Exposes all ProjectMembers-related functionality

View File

@ -81,20 +81,20 @@
await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
try {
await this.$store.dispatch(PM_ACTIONS.FETCH, 1);
} catch (err) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${err.message}`);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${error.message}`);
}
try {
await this.$store.dispatch(API_KEYS_ACTIONS.FETCH);
} catch {
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch api keys');
}
try {
await this.$store.dispatch(PROJECT_USAGE_ACTIONS.FETCH_CURRENT_ROLLUP);
} catch (e) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project usage. ${e.message}`);
} catch (error) {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project usage. ${error.message}`);
}
try {

View File

@ -55,7 +55,7 @@
let decoded = '';
try {
decoded = atob(ids);
} catch {
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Invalid Referral URL');
this.loadingClassName = LOADING_CLASSES.LOADING_OVERLAY;

View File

@ -8,17 +8,20 @@ import * as sinon from 'sinon';
describe('Button.vue', () => {
it('renders correctly', () => {
const wrapper = shallowMount(Button);
const wrapper = shallowMount(Button, {
propsData: {
onPress: new Function(),
},
});
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with isWhite prop', () => {
const wrapper = shallowMount(Button, {
propsData: {
isWhite: true
isWhite: true,
onPress: new Function(),
}
});
@ -26,10 +29,10 @@ describe('Button.vue', () => {
});
it('renders correctly with isDisabled prop', () => {
const wrapper = shallowMount(Button, {
propsData: {
isDisabled: true
isDisabled: true,
onPress: new Function(),
}
});
@ -42,7 +45,12 @@ describe('Button.vue', () => {
let height = '20px';
const wrapper = shallowMount(Button, {
propsData: {label, width, height},
propsData: {
label,
width,
height,
onPress: new Function(),
},
});
expect(wrapper.element.style.width).toMatch(width);
@ -51,8 +59,11 @@ describe('Button.vue', () => {
});
it('renders correctly with default props', () => {
const wrapper = shallowMount(Button);
const wrapper = shallowMount(Button, {
propsData: {
onPress: new Function(),
},
});
expect(wrapper.element.style.width).toMatch('inherit');
expect(wrapper.element.style.height).toMatch('inherit');

View File

@ -17,7 +17,6 @@ const navigation: NavigationLink[] = [
];
describe('TabNavigation', () => {
it('snapshot not changed', () => {
const wrapper = shallowMount(TabNavigation, {
localVue,

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,77 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import NavigationArea from '@/components/navigation/NavigationArea.vue';
import { makeProjectsModule } from '@/store/modules/projects';
import { ProjectsApiMock } from '../../mock/api/projects';
import { Project } from '@/types/projects';
import { NavigationLink } from '@/types/navigation';
import { RouteConfig } from '@/router';
const api = new ProjectsApiMock();
api.setMockProject(new Project('1'));
const projectsModule = makeProjectsModule(api);
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store({ modules: { projectsModule } });
const expectedLinks: NavigationLink[] = [
RouteConfig.ProjectOverview.with(RouteConfig.ProjectDetails),
RouteConfig.Team,
RouteConfig.ApiKeys,
RouteConfig.Buckets,
];
describe('NavigationArea', () => {
it('snapshot not changed without project', () => {
const wrapper = shallowMount(NavigationArea, {
store,
localVue,
});
const navigationElements = wrapper.findAll('.navigation-area__item-container');
const disabledElements = wrapper.findAll('.navigation-area__item-container.disabled');
expect(navigationElements.length).toBe(6);
expect(disabledElements.length).toBe(4);
expect(wrapper).toMatchSnapshot();
});
it('snapshot not changed with project', async () => {
const projects = await store.dispatch('fetchProjects');
await store.dispatch('selectProject', projects[0].id);
const wrapper = shallowMount(NavigationArea, {
store,
localVue,
});
const navigationElements = wrapper.findAll('.navigation-area__item-container');
const disabledElements = wrapper.findAll('.navigation-area__item-container.disabled');
expect(navigationElements.length).toBe(6);
expect(disabledElements.length).toBe(0);
expect(wrapper).toMatchSnapshot();
});
it('navigation links are correct', () => {
const wrapper = shallowMount(NavigationArea, {
store,
localVue,
});
const navigationLinks = (wrapper.vm as any).navigation;
expect(navigationLinks.length).toBe(expectedLinks.length);
expectedLinks.forEach((link, i) => {
expect(navigationLinks[i].name).toBe(expectedLinks[i].name);
expect(navigationLinks[i].path).toBe(expectedLinks[i].path);
});
});
});

View File

@ -0,0 +1,19 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { CreditsApi, CreditUsage } from '@/types/credits';
/**
* Mock for CreditsApi
*/
export class CreditsApiMock implements CreditsApi {
private mockCredits: CreditUsage;
get(): Promise<CreditUsage> {
return Promise.resolve(this.mockCredits);
}
public setMockCredits(mockCredits: CreditUsage): void {
this.mockCredits = mockCredits;
}
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ProjectMemberCursor, ProjectMembersApi, ProjectMembersPage } from '@/types/projectMembers';
/**
* Mock for ProjectMembersApi
*/
export class ProjectMembersApiMock implements ProjectMembersApi {
public cursor: ProjectMemberCursor;
public page: ProjectMembersPage;
public setMockPage(page: ProjectMembersPage): void {
this.page = page;
}
add(projectId: string, emails: string[]): Promise<void> {
throw new Error('not implemented');
}
delete(projectId: string, emails: string[]): Promise<void> {
throw new Error('not implemented');
}
get(projectId: string, cursor: ProjectMemberCursor): Promise<ProjectMembersPage> {
return Promise.resolve(this.page);
}
}

View File

@ -0,0 +1,34 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ProjectsApi, Project, CreateProjectModel } from '@/types/projects';
/**
* Mock for CreditsApi
*/
export class ProjectsApiMock implements ProjectsApi {
private mockProject: Project;
public setMockProject(mockCredits: Project): void {
this.mockProject = mockCredits;
}
create(createProjectModel: CreateProjectModel): Promise<Project> {
throw new Error('not implemented');
}
delete(projectId: string): Promise<void> {
throw new Error('not implemented');
}
get(): Promise<Project[]> {
const result = Array<Project>();
result.push(this.mockProject);
return Promise.resolve(result);
}
update(projectId: string, description: string): Promise<void> {
throw new Error('not implemented');
}
}

View File

@ -0,0 +1,23 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { UpdatedUser, User, UsersApi } from '@/types/users';
/**
* Mock for UsersApi
*/
export class UsersApiMock implements UsersApi {
private mockUser: User;
public setMockUser(mockUser: User): void {
this.mockUser = mockUser;
}
public get(): Promise<User> {
return Promise.resolve(this.mockUser);
}
public update(user: UpdatedUser): Promise<void> {
throw new Error('not implemented');
}
}

View File

@ -1,243 +1,145 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import sinon from 'sinon';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { appStateModule } from '@/store/modules/appState';
import { makeProjectMembersModule } from '@/store/modules/projectMembers';
import { makeNotificationsModule } from '@/store/modules/notifications';
import HeaderArea from '@/components/team/HeaderArea.vue';
import { ProjectMember } from '@/types/projectMembers';
import { ProjectMember, ProjectMemberHeaderState, ProjectMembersPage } from '@/types/projectMembers';
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
const localVue = createLocalVue();
const projectMembers: ProjectMember[] = [
new ProjectMember('f1', 's1', '1@example.com', '', '1'),
new ProjectMember('f2', 's2', '2@example.com', '', '2'),
new ProjectMember('f3', 's3', '3@example.com', '', '3'),
];
const api = new ProjectMembersApiMock();
api.setMockPage(new ProjectMembersPage(projectMembers));
const notificationsModule = makeNotificationsModule();
const projectMembersModule = makeProjectMembersModule(api);
localVue.use(Vuex);
describe('appState Actions', () => {
let store;
let actions;
const store = new Vuex.Store({ modules: { appStateModule, projectMembersModule, notificationsModule } });
beforeEach(() => {
actions = {
toggleAddTeamMembersPopup: jest.fn()
};
store = new Vuex.Store({
modules: {
appStateModule: {
actions
}
}
});
});
it('action on onAddUsersClick works correctly', () => {
const wrapper = mount(HeaderArea, { store, localVue });
wrapper.vm.onAddUsersClick();
expect(actions.toggleAddTeamMembersPopup.mock.calls).toHaveLength(1);
});
});
describe('projectMembers/notification Actions', () => {
let store;
let actions;
let getters;
let state;
let teamMember = new ProjectMember('test', 'test', 'test@test.test', 'test');
let teamMember1 = new ProjectMember('test1', 'test1', 'test1@test.test', 'test1');
let searchQuery = 'test';
beforeEach(() => {
getters = {
selectedProjectMembers: () => [teamMember, teamMember1]
};
state = {
searchParameters: {
searchQuery: ''
},
};
actions = {
clearProjectMemberSelection: jest.fn(),
deleteProjectMembers: async () => {
return {
errorMessage: '',
isSuccess: true,
data: null
};
},
fetchProjectMembers: async () => {
return {
errorMessage: '',
isSuccess: true,
data: [teamMember, teamMember1]
};
},
setProjectMembersSearchQuery: () => {
state.searchParameters.searchQuery = searchQuery;
},
success: jest.fn()
};
store = new Vuex.Store({
modules: {
module: {
actions,
getters,
state
},
}
});
});
it('action on onClearSelection works correctly', () => {
let clearSearchSpy = sinon.spy();
const wrapper = mount(HeaderArea, { store, localVue });
wrapper.vm.$refs.headerComponent.clearSearch = clearSearchSpy;
wrapper.vm.onClearSelection();
expect(actions.clearProjectMemberSelection.mock.calls).toHaveLength(1);
expect(wrapper.vm.$data.isDeleteClicked).toBe(false);
expect(clearSearchSpy.callCount).toBe(1);
});
it('actions on onDelete works correctly', async () => {
let clearSearchSpy = sinon.spy();
const wrapper = mount(HeaderArea, {
store,
localVue
});
wrapper.vm.$data.headerState = 1;
wrapper.vm.$data.isDeleteClicked = true;
wrapper.vm.$refs.headerComponent.clearSearch = clearSearchSpy;
await wrapper.vm.onDelete();
const projectMembersEmails = await store.getters.selectedProjectMembers.map((member) => {
return member.user.email;
});
expect(projectMembersEmails).toHaveLength(2);
expect(actions.deleteProjectMembers).resolves;
expect(actions.success.mock.calls).toHaveLength(1);
expect(wrapper.vm.$data.isDeleteClicked).toBe(false);
expect(clearSearchSpy.callCount).toBe(1);
});
it('action on processSearchQuery works correctly', async () => {
const wrapper = mount(HeaderArea, {
store,
localVue
});
await wrapper.vm.processSearchQuery(searchQuery);
expect(state.searchParameters.searchQuery).toMatch('test');
expect(actions.fetchProjectMembers).resolves;
});
});
describe('error conditions', () => {
let store;
let actions;
let getters;
let searchQuery = 'test';
beforeEach(() => {
getters = {
selectedProjectMembers: () => []
};
actions = {
deleteProjectMembers: async () => {
throw new Error('testError');
},
fetchProjectMembers: async () => {
throw new Error('testError');
},
setProjectMembersSearchQuery: () => {
return;
},
error: jest.fn()
};
store = new Vuex.Store({
modules: {
module: {
actions,
getters
}
}
});
});
it('function customUserCount if there is only 1 selected user', () => {
const wrapper = mount(HeaderArea, { store, localVue });
wrapper.vm.$data.selectedProjectMembers = 1;
expect(wrapper.vm.userCountTitle).toMatch('user');
});
it('function onDelete if delete is not successful', async () => {
const wrapper = mount(HeaderArea, {
store,
localVue
});
wrapper.vm.$data.headerState = 1;
wrapper.vm.$data.isDeleteClicked = true;
await wrapper.vm.onDelete();
expect(actions.error.mock.calls).toHaveLength(1);
});
it('function processSearchQuery if fetch is not successful', async () => {
const wrapper = mount(HeaderArea, {
store,
localVue
});
await wrapper.vm.processSearchQuery(searchQuery);
expect(actions.error.mock.calls).toHaveLength(1);
});
});
describe('HeaderArea.vue', () => {
describe('Team HeaderArea', () => {
it('renders correctly', () => {
const wrapper = shallowMount(HeaderArea);
const wrapper = shallowMount(HeaderArea, {
store,
localVue,
});
const addNewTemMemberPopup = wrapper.findAll('adduserpopup-stub');
expect(wrapper).toMatchSnapshot();
expect(addNewTemMemberPopup.length).toBe(0);
expect(wrapper.findAll('.header-default-state').length).toBe(1);
expect(wrapper.findAll('.blur-content').length).toBe(0);
expect(wrapper.findAll('.blur-search').length).toBe(0);
expect(wrapper.vm.isDeleteClicked).toBe(false);
});
it('renders correctly with default props', () => {
const wrapper = mount(HeaderArea);
it('renders correctly with opened Add team member popup', () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
expect(wrapper.vm.$props.headerState).toBe(0);
expect(wrapper.vm.$props.selectedProjectMembers).toBe(0);
const wrapper = shallowMount(HeaderArea, {
store,
localVue,
});
const addNewTemMemberPopup = wrapper.findAll('adduserpopup-stub');
expect(wrapper).toMatchSnapshot();
expect(addNewTemMemberPopup.length).toBe(1);
expect(wrapper.findAll('.header-default-state').length).toBe(1);
expect(wrapper.vm.isDeleteClicked).toBe(false);
expect(wrapper.findAll('.blur-content').length).toBe(0);
expect(wrapper.findAll('.blur-search').length).toBe(0);
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
});
it('function customUserCount work correctly', () => {
const wrapper = mount(HeaderArea);
it('renders correctly with selected users', () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
expect(wrapper.vm.userCountTitle).toMatch('users');
const selectedUsersCount = 2;
const wrapper = shallowMount(HeaderArea, {
store,
localVue,
propsData: {
selectedProjectMembersCount: selectedUsersCount,
headerState: ProjectMemberHeaderState.ON_SELECT,
},
});
expect(wrapper.findAll('.header-selected-members').length).toBe(1);
expect(wrapper).toMatchSnapshot();
expect(wrapper.vm.selectedProjectMembersCount).toBe(selectedUsersCount);
expect(wrapper.vm.isDeleteClicked).toBe(false);
expect(wrapper.findAll('.blur-content').length).toBe(0);
expect(wrapper.findAll('.blur-search').length).toBe(0);
});
it('function onFirstDeleteClick work correctly', () => {
const wrapper = mount(HeaderArea);
it('renders correctly with 2 selected users and delete clicked once', () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
const selectedUsersCount = 2;
const wrapper = shallowMount(HeaderArea, {
store,
localVue,
propsData: {
selectedProjectMembersCount: selectedUsersCount,
headerState: ProjectMemberHeaderState.ON_SELECT,
},
});
wrapper.vm.onFirstDeleteClick();
expect(wrapper.vm.$data.isDeleteClicked).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(wrapper.vm.selectedProjectMembersCount).toBe(selectedUsersCount);
expect(wrapper.vm.userCountTitle).toBe('users');
expect(wrapper.vm.isDeleteClicked).toBe(true);
expect(wrapper.findAll('.blur-content').length).toBe(1);
expect(wrapper.findAll('.blur-search').length).toBe(1);
const expectedSectionRendered = wrapper.find('.header-after-delete-click');
expect(expectedSectionRendered.text()).toBe(`Are you sure you want to delete ${selectedUsersCount} users?`);
});
});
it('renders correctly with 1 selected user and delete clicked once', () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
const selectedUsersCount = 1;
const wrapper = shallowMount(HeaderArea, {
store,
localVue,
propsData: {
selectedProjectMembersCount: selectedUsersCount,
headerState: ProjectMemberHeaderState.ON_SELECT,
},
});
wrapper.vm.onFirstDeleteClick();
expect(wrapper).toMatchSnapshot();
expect(wrapper.vm.selectedProjectMembersCount).toBe(selectedUsersCount);
expect(wrapper.vm.userCountTitle).toBe('user');
expect(wrapper.vm.isDeleteClicked).toBe(true);
expect(wrapper.findAll('.blur-content').length).toBe(1);
expect(wrapper.findAll('.blur-search').length).toBe(1);
const expectedSectionRendered = wrapper.find('.header-after-delete-click');
expect(expectedSectionRendered.text()).toBe(`Are you sure you want to delete ${selectedUsersCount} user?`);
});
});

View File

@ -1,13 +1,13 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { ProjectMembersApiGql } from '@/api/projectMembers';
import ProjectMembersArea from '@/components/team/ProjectMembersArea.vue';
import { makeProjectMembersModule, PROJECT_MEMBER_MUTATIONS } from '@/store/modules/projectMembers';
import { ProjectMember, ProjectMembersPage } from '@/types/projectMembers';
import { appStateModule } from '@/store/modules/appState';
const localVue = createLocalVue();
@ -20,7 +20,7 @@ describe('ProjectMembersArea.vue', () => {
const pmApi = new ProjectMembersApiGql();
const projectMembersModule = makeProjectMembersModule(pmApi);
const store = new Vuex.Store({modules: { projectMembersModule }});
const store = new Vuex.Store({modules: { projectMembersModule, appStateModule }});
it('renders correctly', () => {
const wrapper = shallowMount(ProjectMembersArea, {
@ -31,6 +31,21 @@ describe('ProjectMembersArea.vue', () => {
expect(wrapper).toMatchSnapshot();
});
it('empty search result area render correctly', function () {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue
});
const emptySearchResultArea = wrapper.findAll('.team-area__empty-search-result-area');
expect(emptySearchResultArea.length).toBe(1);
const teamContainer = wrapper.findAll('.team-area__container');
expect(teamContainer.length).toBe(0);
expect(wrapper).toMatchSnapshot();
});
it('function fetchProjectMembers works correctly', () => {
const testProjectMembersPage = new ProjectMembersPage();
testProjectMembersPage.projectMembers = [projectMember1];
@ -39,33 +54,39 @@ describe('ProjectMembersArea.vue', () => {
store.commit(PROJECT_MEMBER_MUTATIONS.FETCH, testProjectMembersPage);
const wrapper = mount(ProjectMembersArea, {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue,
mocks: {
$route: {
query: {
pageNumber: null
}
},
$router: {
replace: () => false
}
}
});
expect(wrapper.vm.projectMembers.length).toBe(1);
});
it('team area renders correctly', function () {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue
});
const emptySearchResultArea = wrapper.findAll('.team-area__empty-search-result-area');
expect(emptySearchResultArea.length).toBe(0);
const teamContainer = wrapper.findAll('.team-area__container');
expect(teamContainer.length).toBe(1);
const sortingListHeaderStub = wrapper.findAll('sortinglistheader-stub');
expect(sortingListHeaderStub.length).toBe(1);
const listStub = wrapper.findAll('list-stub');
expect(listStub.length).toBe(1);
expect(wrapper).toMatchSnapshot();
});
it('action on toggle works correctly', () => {
const wrapper = mount(ProjectMembersArea, {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue,
mocks: {
$route: {
query: {}
}
}
});
wrapper.vm.onMemberClick(projectMember1);
@ -74,14 +95,9 @@ describe('ProjectMembersArea.vue', () => {
});
it('clear selection works correctly', () => {
const wrapper = mount(ProjectMembersArea, {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue,
mocks: {
$route: {
query: {}
}
}
});
wrapper.vm.onMemberClick(projectMember1);
@ -97,14 +113,9 @@ describe('ProjectMembersArea.vue', () => {
store.commit(PROJECT_MEMBER_MUTATIONS.FETCH, testProjectMembersPage);
const wrapper = mount(ProjectMembersArea, {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue,
mocks: {
$route: {
query: {}
}
}
});
expect(wrapper.vm.projectMembers[0].user.id).toBe(projectMember1.user.id);

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderArea.vue renders correctly 1`] = `
exports[`Team HeaderArea renders correctly 1`] = `
<div class="team-header-container">
<h1>Project Members</h1>
<div class="team-header-container__wrapper">
@ -14,5 +14,85 @@ exports[`HeaderArea.vue renders correctly 1`] = `
<!---->
<!---->
</div>
<!---->
</div>
`;
exports[`Team HeaderArea renders correctly with 1 selected user and delete clicked once 1`] = `
<div class="team-header-container">
<h1>Project Members</h1>
<div class="team-header-container__wrapper">
<headercomponent-stub placeholder="Team Members" search="function () { [native code] }">
<!---->
<!---->
<div class="header-after-delete-click"><span>Are you sure you want to delete 1 user?</span>
<div class="header-after-delete-click__button-area">
<button-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></button-stub>
<button-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></button-stub>
</div>
</div>
</headercomponent-stub>
<div class="blur-content"></div>
<div class="blur-search"></div>
</div>
<adduserpopup-stub></adduserpopup-stub>
</div>
`;
exports[`Team HeaderArea renders correctly with 2 selected users and delete clicked once 1`] = `
<div class="team-header-container">
<h1>Project Members</h1>
<div class="team-header-container__wrapper">
<headercomponent-stub placeholder="Team Members" search="function () { [native code] }">
<!---->
<!---->
<div class="header-after-delete-click"><span>Are you sure you want to delete 2 users?</span>
<div class="header-after-delete-click__button-area">
<button-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></button-stub>
<button-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></button-stub>
</div>
</div>
</headercomponent-stub>
<div class="blur-content"></div>
<div class="blur-search"></div>
</div>
<!---->
</div>
`;
exports[`Team HeaderArea renders correctly with opened Add team member popup 1`] = `
<div class="team-header-container">
<h1>Project Members</h1>
<div class="team-header-container__wrapper">
<headercomponent-stub placeholder="Team Members" search="function () { [native code] }">
<div class="header-default-state"><span>The only project role currently available is Admin, which gives <b>full access</b> to the project.</span>
<button-stub label="+Add" width="122px" height="48px" onpress="function () { [native code] }" class="button"></button-stub>
</div>
<!---->
<!---->
</headercomponent-stub>
<!---->
<!---->
</div>
<adduserpopup-stub></adduserpopup-stub>
</div>
`;
exports[`Team HeaderArea renders correctly with selected users 1`] = `
<div class="team-header-container">
<h1>Project Members</h1>
<div class="team-header-container__wrapper">
<headercomponent-stub placeholder="Team Members" search="function () { [native code] }">
<!---->
<div class="header-selected-members">
<button-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></button-stub>
<button-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></button-stub>
</div>
<!---->
</headercomponent-stub>
<!---->
<!---->
</div>
<adduserpopup-stub></adduserpopup-stub>
</div>
`;

View File

@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProjectMembersArea.vue renders correctly 1`] = `
exports[`ProjectMembersArea.vue empty search result area render correctly 1`] = `
<div class="team-area">
<div class="team-area__header">
<headerarea-stub headerstate="0" selectedprojectmembers="0"></headerarea-stub>
<headerarea-stub headerstate="0" selectedprojectmemberscount="0"></headerarea-stub>
</div>
<!---->
<div class="team-area__empty-search-result-area">
@ -40,3 +40,62 @@ exports[`ProjectMembersArea.vue renders correctly 1`] = `
</div>
</div>
`;
exports[`ProjectMembersArea.vue renders correctly 1`] = `
<div class="team-area">
<div class="team-area__header">
<headerarea-stub headerstate="0" selectedprojectmemberscount="0"></headerarea-stub>
</div>
<!---->
<div class="team-area__empty-search-result-area">
<h1>No results found</h1> <svg width="380" height="295" viewBox="0 0 380 295" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M168 295C246.997 295 311 231.2 311 152.5C311 73.8 246.997 10 168 10C89.0028 10 25 73.8 25 152.5C25 231.2 89.0028 295 168 295Z" fill="#E8EAF2"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3168 98C21.4071 98 20 96.5077 20 94.6174C20.9046 68.9496 31.8599 45.769 49.0467 28.7566C66.2335 11.7442 89.6518 0.900089 115.583 0.00470057C117.492 -0.094787 119 1.39753 119 3.28779V32.4377C119 34.2284 117.593 35.6213 115.784 35.7208C99.7025 36.5167 85.2294 43.3813 74.4751 53.927C63.8213 64.5722 56.8863 78.8984 56.0822 94.8164C55.9817 96.6072 54.5746 98 52.7655 98H23.3168Z" fill="#B0B6C9"></path>
<path d="M117.5 30C124.404 30 130 25.0751 130 19C130 12.9249 124.404 8 117.5 8C110.596 8 105 12.9249 105 19C105 25.0751 110.596 30 117.5 30Z" fill="#8F96AD"></path>
<path d="M112.5 97C116.09 97 119 94.3137 119 91C119 87.6863 116.09 85 112.5 85C108.91 85 106 87.6863 106 91C106 94.3137 108.91 97 112.5 97Z" fill="#B0B6C9"></path>
<path d="M15.0005 282C23.226 282 30 274.575 30 265.5C30 256.425 23.226 249 15.0005 249C6.77499 249 0.00102409 256.425 0.00102409 265.5C-0.0957468 274.678 6.67822 282 15.0005 282Z" fill="#8F96AD"></path>
<path d="M15.5 274C19.0286 274 22 270.9 22 267C22 263.2 19.1214 260 15.5 260C11.9714 260 9 263.1 9 267C9 270.9 11.8786 274 15.5 274Z" fill="white"></path>
<path d="M282.587 111H307.413C309.906 111 312 108.955 312 106.5C312 104.045 309.906 102 307.413 102H282.587C280.094 102 278 104.045 278 106.5C278 108.955 280.094 111 282.587 111Z" fill="white"></path>
<path d="M282.585 93H289.415C291.951 93 294 91.02 294 88.5C294 85.98 291.951 84 289.415 84H282.585C280.049 84 278 85.98 278 88.5C278 91.02 279.951 93 282.585 93Z" fill="#E8EAF2"></path>
<path d="M252.872 92H260.128C262.823 92 265 90.4091 265 88.5C265 86.5909 262.823 85 260.128 85H252.872C250.177 85 248 86.5909 248 88.5C248 90.4091 250.177 92 252.872 92Z" fill="#363840"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45 166C48.8182 166 52 162.818 52 159C52 155.182 48.8182 152 45 152C41.1818 152 38 155.182 38 159C38 162.818 41.1818 166 45 166Z" fill="#B0B6C9"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M217 232C220.818 232 224 228.818 224 225C224 221.182 220.818 218 217 218C213.182 218 210 221.182 210 225C210 228.818 213.182 232 217 232Z" fill="#2683FF"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 142C29.8182 142 33 139.045 33 135.5C33 131.955 29.8182 129 26 129C22.1818 129 19 131.955 19 135.5C19 139.045 22.1818 142 26 142Z" fill="white"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45 142C48.8182 142 52 139.045 52 135.5C52 131.955 48.8182 129 45 129C41.1818 129 38 131.955 38 135.5C38 139.045 41.1818 142 45 142Z" fill="#E8EAF2"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64 142C67.8182 142 71 139.045 71 135.5C71 131.955 67.8182 129 64 129C60.1818 129 57 131.955 57 135.5C57 139.045 60.1818 142 64 142Z" fill="white"></path>
<path d="M107.014 129.651C107.014 129.651 152.017 118.395 199.527 125.169C212.857 127.061 224.785 134.831 232.001 146.186C245.031 166.606 263.374 203.062 259.465 241.112L239.018 246.093C239.018 246.093 224.885 200.97 209.049 182.643C209.049 182.643 190.205 225.275 191.208 248.683C191.208 249.38 191.308 249.977 191.308 250.575C193.513 273.485 101 254.858 101 254.858L107.014 129.651Z" fill="#F5F6FA"></path>
<path d="M143 89.7894L145.01 121.569C145.211 124.568 147.12 127.066 149.833 127.865C156.063 129.664 167.821 131.863 179.276 127.266C181.387 126.466 182.492 123.968 181.789 121.669L166.514 73L143 89.7894Z" fill="#8F96AD"></path>
<path d="M189 61.014C189 61.014 186.474 85.2772 181.219 95.8484C175.964 106.42 174.448 114.272 161.412 109.641C148.376 105.01 141.707 93.5328 142.01 80.2434C142.01 80.2434 142.414 59.7052 147.972 54.3692C153.631 49.0333 189 61.014 189 61.014Z" fill="#B0B6C9"></path>
<path d="M150.596 75.686L152.115 76.4754C152.115 76.4754 153.128 60.6872 159.814 61.4766C166.5 62.266 190.609 69.8641 199.625 64.9303C208.235 60.1938 191.521 44.2082 180.074 40.4585C163.866 35.0313 150.798 35.5247 144.822 45.2936C144.416 45.8857 143.606 45.8857 143.201 45.2936C142.492 44.0108 128.209 53.9772 132.97 65.917C133.172 66.5091 138.946 83.4815 140.567 83.9748C140.972 84.0735 141.479 83.8762 141.681 83.4815L146.24 74.4032C146.442 73.9098 147.05 73.7125 147.557 74.0085L150.596 75.686Z" fill="#0F002D"></path>
<path d="M149.877 78.0283C149.877 78.0283 154.31 62.6808 145.56 63.0051C136.81 63.3293 139.844 79.7576 144.744 83L149.877 78.0283Z" fill="#B0B6C9"></path>
<path d="M106.635 221.07C104.63 206.983 119.272 186.154 125.289 178.305C126.994 176.092 127.996 173.274 127.996 170.457C128.197 150.433 119.773 137.553 106.335 129C106.335 129 57.5953 185.953 70.0308 229.724C71.3345 234.453 73.4406 238.478 76.048 242C78.0538 225.397 97.1082 221.875 106.635 221.07Z" fill="#F5F6FA"></path>
<path d="M107.966 215L106 214.798C107.655 200.851 120.172 183.67 125.448 177L127 178.112C121.828 184.681 109.621 201.559 107.966 215Z" fill="#0F002D"></path>
<path d="M107.128 221.954C106.926 221.337 106.825 220.617 106.725 220C97.054 220.823 78.0147 224.423 76 241.29C97.8599 270.808 158 260.111 158 260.111V248.592C158.101 248.695 111.862 239.953 107.128 221.954Z" fill="#B0B6C9"></path>
<path d="M152 257C152 257 160.863 236.189 176.575 243.593C192.187 250.997 190.978 255.799 190.978 255.799L152 257Z" fill="#B0B6C9"></path>
<path d="M271.213 238H136.787C134.194 238 132 235.787 132 233.172V139.828C132 137.213 134.194 135 136.787 135H271.213C273.806 135 276 137.213 276 139.828V233.172C276 235.787 273.906 238 271.213 238Z" fill="#363840"></path>
<path d="M217.252 258H195.744C193.109 258 191 256 191 253.5V190.5C191 188 193.109 186 195.744 186H217.252C219.888 186 221.996 188 221.996 190.5V253.5C222.102 255.9 219.888 258 217.252 258Z" fill="#363840"></path>
<path d="M246.189 254H150.811C149.305 254 148 255.444 148 257.111V258.889C148 260.556 149.305 262 150.811 262H246.189C247.695 262 249 260.556 249 258.889V257.111C249 255.444 247.795 254 246.189 254Z" fill="#363840"></path>
<path d="M350.452 224.555C349.952 224.555 349.553 224.555 349.154 224.654C348.355 224.754 347.557 224.256 347.257 223.56C337.873 206.543 319.705 195 298.742 195C279.775 195 263.004 204.454 253.121 218.883C252.622 219.579 251.724 219.878 250.925 219.778C248.429 219.281 245.834 218.982 243.239 218.982C223.772 219.082 208 234.605 208 253.91C208 253.91 208 253.91 208 254.01C208 255.104 208.898 256 210.096 256H377.904C379.002 256 380 255.104 380 254.01V253.91C379.8 237.591 366.623 224.555 350.452 224.555Z" fill="#B0B6C9"></path>
<path d="M206 195C210.418 195 214 191.194 214 186.5C214 181.806 210.418 178 206 178C201.582 178 198 181.806 198 186.5C198 191.194 201.582 195 206 195Z" fill="white"></path>
</svg>
</div>
</div>
`;
exports[`ProjectMembersArea.vue team area renders correctly 1`] = `
<div class="team-area">
<div class="team-area__header">
<headerarea-stub headerstate="0" selectedprojectmemberscount="0"></headerarea-stub>
</div>
<div id="team-container" class="team-area__container">
<sortinglistheader-stub onheaderclickcallback="function () { [native code] }"></sortinglistheader-stub>
<div class="team-area__container__content">
<list-stub itemcomponent="function VueComponent (options) {
this._init(options);
}" onitemclick="function () { [native code] }" dataset="[object Object]"></list-stub>
</div>
<pagination-stub totalpagecount="1" onpageclickcallback="function () { [native code] }" class="pagination-area"></pagination-stub>
</div>
<!---->
</div>
`;

View File

@ -4,10 +4,12 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import ReferralStats from '@/components/referral/ReferralStats.vue';
import { CreditsApi, CreditUsage } from '@/types/credits';
import { UpdatedUser, User, UsersApi } from '@/types/users';
import { CreditUsage } from '@/types/credits';
import { User } from '@/types/users';
import { makeCreditsModule } from '@/store/modules/credits';
import { makeUsersModule, USER_ACTIONS } from '@/store/modules/users';
import { UsersApiMock } from '../mock/api/users';
import { CreditsApiMock } from '../mock/api/credits';
const {
GET,
@ -19,24 +21,10 @@ localVue.use(Vuex);
const mockUser = new User('1', 'full name', 'short name');
const mockCredits = new CreditUsage(1, 2, 3);
class UsersApiMock implements UsersApi {
get(): Promise<User> {
return Promise.resolve(mockUser);
}
update(user: UpdatedUser): Promise<void> {
throw new Error('not implemented');
}
}
class CreditsApiMock implements CreditsApi {
get(): Promise<CreditUsage> {
return Promise.resolve(mockCredits);
}
}
const creditsApi = new CreditsApiMock();
creditsApi.setMockCredits(mockCredits);
const usersApi = new UsersApiMock();
usersApi.setMockUser(mockUser);
const usersModule = makeUsersModule(usersApi);
const creditsModule = makeCreditsModule(creditsApi);