Commit e2b1f8a7 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'mw-onboarding-tour-app' into 'master'

Onboarding app component

See merge request gitlab-org/gitlab-ee!13953
parents 269c1a39 951e9a74
import { s__, sprintf } from '~/locale';
export const ONBOARDING_DISMISSED_COOKIE_NAME = 'onboarding_dismissed'; export const ONBOARDING_DISMISSED_COOKIE_NAME = 'onboarding_dismissed';
export const STORAGE_KEY = 'onboarding_state'; export const STORAGE_KEY = 'onboarding_state';
...@@ -8,6 +10,15 @@ export const AVAILABLE_TOURS = { ...@@ -8,6 +10,15 @@ export const AVAILABLE_TOURS = {
INVITE_COLLEAGUES_TOUR: 3, INVITE_COLLEAGUES_TOUR: 3,
}; };
export const TOUR_TITLES = [
{ id: AVAILABLE_TOURS.GUIDED_GITLAB_TOUR, title: s__('UserOnboardingTour|Guided GitLab Tour') },
{ id: AVAILABLE_TOURS.CREATE_PROJECT_TOUR, title: s__('UserOnboardingTour|Create a project') },
{
id: AVAILABLE_TOURS.INVITE_COLLEAGUES_TOUR,
title: s__('UserOnboardingTour|Invite colleagues'),
},
];
export const ONBOARDING_PROPS_DEFAULTS = { export const ONBOARDING_PROPS_DEFAULTS = {
tourKey: AVAILABLE_TOURS.GUIDED_GITLAB_TOUR, tourKey: AVAILABLE_TOURS.GUIDED_GITLAB_TOUR,
lastStepIndex: -1, lastStepIndex: -1,
...@@ -19,3 +30,17 @@ export const ACCEPTING_MR_LABEL_TEXT = 'Accepting merge requests'; ...@@ -19,3 +30,17 @@ export const ACCEPTING_MR_LABEL_TEXT = 'Accepting merge requests';
export const LABEL_SEARCH_QUERY = `scope=all&state=opened&label_name[]=${encodeURIComponent( export const LABEL_SEARCH_QUERY = `scope=all&state=opened&label_name[]=${encodeURIComponent(
ACCEPTING_MR_LABEL_TEXT, ACCEPTING_MR_LABEL_TEXT,
)}`; )}`;
export const EXIT_TOUR_CONTENT = {
text: sprintf(
s__(
'UserOnboardingTour|Thanks for taking the guided tour. Remember, if you want to go through it again, you can start %{emphasisStart}Learn GitLab%{emphasisEnd} in the help menu on the top right.',
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
},
false,
),
buttons: [{ text: s__('UserOnboardingTour|Got it'), btnClass: 'btn-primary', exitTour: true }],
};
const renderPopover = () => {
// TODO: implementation will be added in a separate MR
};
const actionPopoverUtils = {
renderPopover,
};
export default actionPopoverUtils;
<script> <script>
export default {}; import _ from 'underscore';
import { mapState, mapActions, mapGetters } from 'vuex';
import { redirectTo } from '~/lib/utils/url_utility';
import OnboardingHelper from './onboarding_helper.vue';
import actionPopoverUtils from './../action_popover_utils';
import eventHub from '../event_hub';
export default {
components: {
OnboardingHelper,
},
props: {
tourTitles: {
type: Array,
required: true,
},
exitTourContent: {
type: Object,
required: true,
},
goldenTanukiSvgPath: {
type: String,
required: true,
},
},
data() {
return {
showStepContent: false,
initialShowPopover: false,
dismissPopover: false,
};
},
computed: {
...mapState([
'projectName',
'tourKey',
'tourData',
'lastStepIndex',
'helpContentIndex',
'exitTour',
'dismissed',
]),
...mapGetters([
'stepIndex',
'stepContent',
'helpContent',
'totalTourPartSteps',
'percentageCompleted',
'actionPopover',
]),
helpContentData() {
if (this.showStepContent) {
return this.exitTour ? this.exitTourContent : this.helpContent;
}
return null;
},
completedSteps() {
return Math.max(this.lastStepIndex, 0);
},
},
mounted() {
this.init();
},
methods: {
...mapActions([
'setTourKey',
'setLastStepIndex',
'setHelpContentIndex',
'switchTourPart',
'setExitTour',
'setDismissed',
]),
init() {
// ensure we show help content on consecutive pages only
if (this.tourKey) {
const nextStepIndex = this.lastStepIndex + 1;
// show help content when the current was the last visited page (e.g., user navigates away and comes back to current page)
if (this.lastStepIndex === this.stepIndex) {
this.showStepContent = true;
this.initActionPopover();
// show help content when this is the upcoming page in the content list (otherwise don't show the help content)
// and update the lastStepIndex
} else if (nextStepIndex === this.stepIndex) {
this.setLastStepIndex(nextStepIndex);
this.showStepContent = true;
this.initActionPopover();
}
}
},
initActionPopover() {
if (this.actionPopover) {
const { selector, text, placement } = this.actionPopover;
// immediately show the action popover if there's not helpContent for this step
const showPopover = !this.helpContent && !_.isUndefined(selector);
actionPopoverUtils.renderPopover(selector, text, placement, showPopover);
}
},
showActionPopover() {
eventHub.$emit('onboardingHelper.showActionPopover');
},
hideActionPopover() {
eventHub.$emit('onboardingHelper.hideActionPopover');
},
handleRestartStep() {
this.showExitTourContent(false);
eventHub.$emit('onboardingHelper.hideActionPopover');
},
handleSkipStep() {
if (this.actionPopover) {
const { selector } = this.actionPopover;
const popoverEl = selector ? document.querySelector(selector) : null;
if (popoverEl) {
popoverEl.click();
}
}
},
handleClickPopoverButton(button) {
const { showExitTourContent, exitTour, redirectPath, nextPart, dismissPopover } = button;
const helpContentItems = this.stepContent.getHelpContent({ projectName: this.projectName });
const showNextContentItem =
helpContentItems.length > 1 && this.helpContentIndex < helpContentItems.length - 1;
// display exit tour content
if (showExitTourContent) {
this.showExitTourContent(true);
return;
}
// quit tour
if (exitTour) {
this.handleExitTour();
return;
}
// dismiss popover if necessary
if (_.isUndefined(dismissPopover) || dismissPopover === true) {
this.dismissPopover = true;
}
// redirect to redirectPath
if (redirectPath) {
redirectTo(redirectPath);
return;
}
// switch to the next tour part
if (!_.isUndefined(nextPart)) {
this.switchTourPart(nextPart);
this.initActionPopover();
return;
}
// switch to next content item
if (showNextContentItem) {
this.setHelpContentIndex(this.helpContentIndex + 1);
return;
}
// show action popover
this.showActionPopover();
},
showExitTourContent(showExitTour) {
this.dismissPopover = false;
this.setExitTour(showExitTour);
},
handleExitTour() {
this.hideActionPopover();
this.setDismissed(true);
// remove popover event handlers
eventHub.$emit('onboardingHelper.destroyActionPopover');
},
afterAppearHook() {
this.initialShowPopover = true;
},
},
};
</script> </script>
<template> <template>
<div></div> <transition appear name="slide-in-fwd-bottom" @after-appear="afterAppearHook">
<onboarding-helper
v-if="!dismissed"
:tour-titles="tourTitles"
:active-tour="tourKey"
:completed-steps="completedSteps"
:help-content="helpContentData"
:percentage-completed="percentageCompleted"
:total-steps-for-tour="totalTourPartSteps"
:initial-show="initialShowPopover"
:dismiss-popover="dismissPopover"
:golden-tanuki-svg-path="goldenTanukiSvgPath"
@clickPopoverButton="handleClickPopoverButton"
@restartStep="handleRestartStep"
@skipStep="handleSkipStep"
@showExitTourContent="showExitTourContent"
@exitTour="handleExitTour"
/>
</transition>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex';
import OnboardingApp from './components/app.vue'; import OnboardingApp from './components/app.vue';
import createStore from './store';
import onboardingUtils from './../utils';
import { TOUR_TITLES, EXIT_TOUR_CONTENT } from './../constants';
import TOUR_PARTS from './../tour_parts';
export default function() { export default function() {
const el = document.getElementById('js-onboarding-helper'); const el = document.getElementById('js-onboarding-helper');
...@@ -8,15 +13,45 @@ export default function() { ...@@ -8,15 +13,45 @@ export default function() {
return false; return false;
} }
const { projectFullPath, projectName } = el.dataset;
const tourData = onboardingUtils.getOnboardingLocalStorageState();
const url = window.location.href;
if (!tourData) {
return false;
}
const { tourKey, lastStepIndex, createdProjectPath } = tourData;
const store = createStore();
return new Vue({ return new Vue({
el, el,
store,
components: { components: {
OnboardingApp, OnboardingApp,
}, },
created() {
if (tourKey) {
this.setInitialData({
url,
projectFullPath,
projectName,
tourData: TOUR_PARTS,
tourKey,
lastStepIndex,
createdProjectPath,
});
}
},
methods: {
...mapActions(['setInitialData']),
},
render(h) { render(h) {
return h(OnboardingApp, { return h(OnboardingApp, {
props: {}, props: {
tourTitles: TOUR_TITLES,
exitTourContent: EXIT_TOUR_CONTENT,
},
}); });
}, },
}); });
......
import Cookies from 'js-cookie';
import * as types from './mutation_types';
import { ONBOARDING_DISMISSED_COOKIE_NAME } from '../../constants';
import onboardingUtils from '../../utils';
export const setInitialData = ({ commit }, data) => {
commit(types.SET_INITIAL_DATA, data);
};
export const setTourKey = ({ commit }, tourKey) => {
commit(types.SET_TOUR_KEY, tourKey);
onboardingUtils.updateLocalStorage({ tourKey });
};
export const setLastStepIndex = ({ commit }, lastStepIndex) => {
commit(types.SET_LAST_STEP_INDEX, lastStepIndex);
onboardingUtils.updateLocalStorage({ lastStepIndex });
};
export const setHelpContentIndex = ({ commit }, helpContentIndex) => {
commit(types.SET_HELP_CONTENT_INDEX, helpContentIndex);
};
export const switchTourPart = ({ dispatch }, tourKey) => {
dispatch('setTourKey', tourKey);
dispatch('setLastStepIndex', 0);
dispatch('setHelpContentIndex', 0);
};
export const setExitTour = ({ commit }, exitTour) => {
commit(types.SET_EXIT_TOUR, exitTour);
};
export const setDismissed = ({ commit }, dismissed) => {
commit(types.SET_DISMISSED, dismissed);
Cookies.set(ONBOARDING_DISMISSED_COOKIE_NAME, dismissed);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const stepIndex = state => {
const { tourData, tourKey, url, projectFullPath, createdProjectPath } = state;
let idx = -1;
if (tourData && tourData[tourKey] && url !== '') {
idx = tourData[tourKey].findIndex(item =>
item.forUrl({ projectFullPath, createdProjectPath }).test(state.url),
);
}
return idx !== -1 ? idx : null;
};
export const stepContent = (state, getters) => {
const { tourData, tourKey } = state;
if (!tourData || !tourData[tourKey] || getters.stepIndex === null) {
return null;
}
return tourData[tourKey][getters.stepIndex] ? tourData[tourKey][getters.stepIndex] : null;
};
export const helpContent = (state, getters) => {
const { projectName, helpContentIndex } = state;
if (getters.stepContent === null) {
return null;
}
return getters.stepContent.getHelpContent
? getters.stepContent.getHelpContent({ projectName })[helpContentIndex]
: null;
};
export const totalTourPartSteps = state => {
if (state.tourData && state.tourKey && state.tourData[state.tourKey]) {
return state.tourData[state.tourKey].length;
}
return 0;
};
export const percentageCompleted = (state, getters) => {
const { tourData, tourKey } = state;
if (getters.stepIndex === null || !tourData || !tourData[tourKey]) {
return 0;
}
return tourData[tourKey][getters.stepIndex]
? tourData[tourKey][getters.stepIndex].percentageCompleted
: 0;
};
export const actionPopover = (state, getters) =>
getters.stepContent !== null && getters.stepContent.actionPopover
? getters.stepContent.actionPopover
: null;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state: state(),
});
export default createStore;
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const SET_TOUR_KEY = 'SET_TOUR_KEY';
export const SET_LAST_STEP_INDEX = 'SET_LAST_STEP_INDEX';
export const SET_HELP_CONTENT_INDEX = 'SET_HELP_CONTENT_INDEX';
export const SET_EXIT_TOUR = 'SET_EXIT_TOUR';
export const SET_DISMISSED = 'SET_DISMISSED';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, payload) {
Object.assign(state, payload);
},
[types.SET_TOUR_KEY](state, payload) {
state.tourKey = payload;
},
[types.SET_LAST_STEP_INDEX](state, payload) {
state.lastStepIndex = payload;
},
[types.SET_HELP_CONTENT_INDEX](state, payload) {
state.helpContentIndex = payload;
},
[types.SET_EXIT_TOUR](state, payload) {
state.exitTour = payload;
},
[types.SET_DISMISSED](state, payload) {
state.dismissed = payload;
},
};
import { AVAILABLE_TOURS } from '../../constants';
export default () => ({
url: '',
projectFullPath: '',
projectName: '',
tourData: [],
tourKey: AVAILABLE_TOURS.GUIDED_GITLAB_TOUR,
helpContentIndex: 0,
lastStepIndex: -1,
dismissed: false,
createdProjectPath: '',
exitTour: false,
});
import Vue from 'vue';
import OnboardingHelperApp from 'ee/onboarding/onboarding_helper/components/app.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import eventHub from 'ee/onboarding/onboarding_helper/event_hub';
import createStore from 'ee/onboarding/onboarding_helper/store';
import actionPopoverUtils from 'ee/onboarding/onboarding_helper/action_popover_utils';
import { mockTourData } from '../mock_data';
describe('User onboarding helper app', () => {
let vm;
let store;
const initialData = {
url: 'http://gitlab-org/gitlab-test/foo',
projectFullPath: 'http://gitlab-org/gitlab-test',
projectName: 'Mock Project',
tourData: mockTourData,
tourKey: 1,
lastStepIndex: -1,
createdProjectPath: '',
};
const tourTitles = [{ id: 1, title: 'First tour' }, { id: 2, title: 'Second tour' }];
const exitTourContent = {
text: 'exit tour content',
buttons: [{ text: 'OK', btnClass: 'btn-primary' }],
};
const defaultProps = {
tourTitles,
exitTourContent,
goldenTanukiSvgPath: 'illustrations/golden_tanuki.svg',
};
const createComponent = ({ props = defaultProps } = {}) => {
const Component = Vue.extend(OnboardingHelperApp);
store = createStore();
store.dispatch('setInitialData', initialData);
return mountComponentWithStore(Component, { props, store });
};
beforeEach(() => {
vm = createComponent();
spyOn(vm, 'init');
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('helpContentData', () => {
it('returns an object containing the help content data', () => {
const helpContent = mockTourData[initialData.tourKey][0].getHelpContent()[0];
expect(vm.helpContentData).toEqual(helpContent);
});
it('returns null if showStepContent is false', () => {
vm.showStepContent = false;
expect(vm.helpContentData).toBeNull();
});
it('returns an object containing exit tour content if exitTour is true', () => {
store.dispatch('setExitTour', true);
expect(vm.helpContentData).toEqual(exitTourContent);
});
});
describe('completedSteps', () => {
it('returns 3 if the lastStepIndex is 1', () => {
vm.$store.state.lastStepIndex = 3;
expect(vm.completedSteps).toBe(3);
});
it('returns 0 if the lastStepIndex is -1', () => {
vm.$store.state.lastStepIndex = -1;
expect(vm.completedSteps).toBe(0);
});
});
});
describe('mounted', () => {
it('calls the init method', () => {
expect(vm.init).toHaveBeenCalled();
expect(vm.showStepContent).toBe(true);
});
});
describe('methods', () => {
describe('initActionPopover', () => {
it('calls renderPopover with the correct data', () => {
spyOn(actionPopoverUtils, 'renderPopover');
const expected = {
selector: '.popup-trigger',
text: 'foo',
placement: 'top',
showPopover: false,
};
const { selector, text, placement, showPopover } = expected;
vm.initActionPopover();
expect(actionPopoverUtils.renderPopover).toHaveBeenCalledWith(
selector,
text,
placement,
showPopover,
);
});
it('calls renderPopover with showPopover=true if there is no helpContent data and no popover selector for the current url', () => {
spyOn(actionPopoverUtils, 'renderPopover');
vm.$store.state.url = 'http://gitlab-org/gitlab-test/xyz';
const expected = {
selector: null,
text: 'foo',
placement: 'top',
showPopover: true,
};
const { selector, text, placement, showPopover } = expected;
vm.initActionPopover();
expect(actionPopoverUtils.renderPopover).toHaveBeenCalledWith(
selector,
text,
placement,
showPopover,
);
});
});
describe('showActionPopover', () => {
it('emits the "onboardingHelper.showActionPopover" event', () => {
spyOn(eventHub, '$emit');
vm.showActionPopover();
expect(eventHub.$emit).toHaveBeenCalledWith('onboardingHelper.showActionPopover');
});
});
describe('hideActionPopover', () => {
it('emits the "onboardingHelper.hideActionPopover" event', () => {
spyOn(eventHub, '$emit');
vm.hideActionPopover();
expect(eventHub.$emit).toHaveBeenCalledWith('onboardingHelper.hideActionPopover');
});
});
describe('handleRestartStep', () => {
it('calls the "showExitTourContent" method', () => {
spyOn(vm, 'showExitTourContent');
vm.handleRestartStep();
expect(vm.showExitTourContent).toHaveBeenCalledWith(false);
});
it('emits the "onboardingHelper.hideActionPopover" event', () => {
spyOn(eventHub, '$emit');
vm.handleRestartStep();
expect(eventHub.$emit).toHaveBeenCalledWith('onboardingHelper.hideActionPopover');
});
});
describe('handleSkipStep', () => {
it('calls the click method on given popover selector if there is a stepContent', () => {
const {
actionPopover: { selector },
} = vm.stepContent;
const fakeLink = {
click: () => {},
};
spyOn(document, 'querySelector').and.returnValue(fakeLink);
spyOn(fakeLink, 'click');
vm.handleSkipStep();
expect(document.querySelector).toHaveBeenCalledWith(`${selector}`);
expect(fakeLink.click).toHaveBeenCalled();
});
});
describe('handleClickPopoverButton', () => {
it('shows the exitTour content', () => {
spyOn(vm, 'showExitTourContent');
const button = {
showExitTourContent: true,
};
vm.handleClickPopoverButton(button);
expect(vm.showExitTourContent).toHaveBeenCalledWith(true);
});
it('quits the tour', () => {
spyOn(vm, 'handleExitTour');
const button = {
exitTour: true,
};
vm.handleClickPopoverButton(button);
expect(vm.handleExitTour).toHaveBeenCalled();
});
it('sets dismissPopover to true when true/undefined on button config', () => {
let button = {
dismissPopover: true,
};
vm.handleClickPopoverButton(button);
expect(vm.dismissPopover).toBe(true);
button = {};
vm.handleClickPopoverButton(button);
expect(vm.dismissPopover).toBe(true);
});
it('does not set dismissPopover to true when false on button config', () => {
const button = {
dismissPopover: false,
};
vm.handleClickPopoverButton(button);
expect(vm.dismissPopover).toBe(false);
});
it('redirects to the redirectPath', () => {
const redirectSpy = spyOnDependency(OnboardingHelperApp, 'redirectTo');
const button = {
redirectPath: 'my-redirect/path',
};
vm.handleClickPopoverButton(button);
expect(redirectSpy).toHaveBeenCalledWith(button.redirectPath);
});
it('switches to the next tour part and calls initActionPopover', () => {
spyOn(vm.$store, 'dispatch');
spyOn(vm, 'initActionPopover');
const nextPart = 2;
const button = {
nextPart,
};
vm.handleClickPopoverButton(button);
expect(vm.$store.dispatch).toHaveBeenCalledWith('switchTourPart', nextPart);
expect(vm.initActionPopover).toHaveBeenCalled();
});
it('shows the next content item', () => {
spyOn(vm.$store, 'dispatch');
const button = {};
vm.$store.state.url = 'http://gitlab-org/gitlab-test/foo';
vm.$store.state.lastStepIndex = 0;
vm.handleClickPopoverButton(button);
expect(vm.$store.dispatch).toHaveBeenCalledWith('setHelpContentIndex', 1);
});
});
describe('showExitTourContent', () => {
it('sets the "dismissPopover" prop to false', () => {
vm.showExitTourContent(true);
expect(vm.dismissPopover).toBeFalsy();
});
it('calls the "setExitTour" method', () => {
spyOn(vm.$store, 'dispatch');
vm.showExitTourContent(true);
expect(vm.$store.dispatch).toHaveBeenCalledWith('setExitTour', true);
});
});
describe('handleExitTour', () => {
it('calls the "hideActionPopover" method', () => {
spyOn(vm, 'hideActionPopover');
vm.handleExitTour();
expect(vm.hideActionPopover).toHaveBeenCalled();
});
it('calls the "setDismissed" method with true', () => {
spyOn(vm.$store, 'dispatch');
vm.handleExitTour();
expect(vm.$store.dispatch).toHaveBeenCalledWith('setDismissed', true);
});
it('emits the "onboardingHelper.destroyActionPopover" event', () => {
spyOn(eventHub, '$emit');
vm.handleExitTour();
expect(eventHub.$emit).toHaveBeenCalledWith('onboardingHelper.destroyActionPopover');
});
});
});
});
export const mockTourData = {
1: [
{
forUrl: ({ projectFullPath }) => new RegExp(`${projectFullPath}/foo$`, ''),
getHelpContent: () => [
{
text: 'foo',
buttons: [{ text: 'button', btnClass: 'btn-primary' }],
},
{
text: 'next content item',
buttons: [{ text: 'button', btnClass: 'btn-primary' }],
},
],
actionPopover: {
selector: '.popup-trigger',
text: 'foo',
placement: 'top',
},
percentageCompleted: 10,
},
{
forUrl: ({ projectFullPath }) => new RegExp(`${projectFullPath}/foo/bar$`, ''),
getHelpContent: ({ projectName }) => [
{
text: `This is the ${projectName}`,
buttons: [{ text: 'button', btnClass: 'btn-primary' }],
},
],
actionPopover: {
selector: '',
text: 'bar',
},
percentageCompleted: 20,
},
{
forUrl: ({ projectFullPath }) => new RegExp(`${projectFullPath}/xyz`, ''),
getHelpContent: null,
actionPopover: {
selector: null,
text: 'foo',
placement: 'top',
},
percentageCompleted: 30,
},
],
};
export const mockData = {
url: 'http://gitlab-org/gitlab-test/foo',
projectFullPath: 'http://gitlab-org/gitlab-test',
projectName: 'Mock Project',
tourData: mockTourData,
tourKey: 1,
helpContentIndex: 0,
lastStepIndex: -1,
createdProjectPath: '',
};
import Cookies from 'js-cookie';
import testAction from 'spec/helpers/vuex_action_helper';
import createState from 'ee/onboarding/onboarding_helper/store/state';
import * as types from 'ee/onboarding/onboarding_helper/store/mutation_types';
import {
setInitialData,
setTourKey,
setLastStepIndex,
setHelpContentIndex,
switchTourPart,
setExitTour,
setDismissed,
} from 'ee/onboarding/onboarding_helper/store/actions';
import { ONBOARDING_DISMISSED_COOKIE_NAME } from 'ee/onboarding/constants';
import onboardingUtils from 'ee/onboarding/utils';
import mockData from './../mock_data';
describe('User onboarding helper store actions', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('setInitialData', () => {
it(`commits ${types.SET_INITIAL_DATA} mutation`, done => {
const initialData = mockData;
testAction(
setInitialData,
initialData,
state,
[{ type: types.SET_INITIAL_DATA, payload: initialData }],
[],
done,
);
});
});
describe('setTourKey', () => {
beforeEach(() => {
spyOn(onboardingUtils, 'updateLocalStorage').and.stub();
});
it(`commits ${types.SET_TOUR_KEY} mutation`, done => {
const tourKey = 2;
testAction(
setTourKey,
tourKey,
state,
[{ type: types.SET_TOUR_KEY, payload: tourKey }],
[],
done,
);
});
it('updates localStorage with the tourKey', () => {
const tourKey = 2;
setTourKey({ commit() {} }, tourKey);
expect(onboardingUtils.updateLocalStorage).toHaveBeenCalledWith({ tourKey });
});
});
describe('setLastStepIndex', () => {
beforeEach(() => {
spyOn(onboardingUtils, 'updateLocalStorage').and.stub();
});
it(`commits ${types.SET_LAST_STEP_INDEX} mutation`, done => {
const lastStepIndex = 1;
testAction(
setLastStepIndex,
lastStepIndex,
state,
[{ type: types.SET_LAST_STEP_INDEX, payload: lastStepIndex }],
[],
done,
);
});
it('updates localStorage with the lastStepIndex', () => {
const lastStepIndex = 1;
setLastStepIndex({ commit() {} }, lastStepIndex);
expect(onboardingUtils.updateLocalStorage).toHaveBeenCalledWith({ lastStepIndex });
});
});
describe('setHelpContentIndex', () => {
it(`commits ${types.SET_HELP_CONTENT_INDEX} mutation`, done => {
const helpContentIndex = 1;
testAction(
setHelpContentIndex,
helpContentIndex,
state,
[{ type: types.SET_HELP_CONTENT_INDEX, payload: helpContentIndex }],
[],
done,
);
});
});
describe('switchTourPart', () => {
it('should dispatch setTourKey, setLastStepIndex and', done => {
const nextPart = 2;
testAction(
switchTourPart,
nextPart,
state,
[],
[
{ type: 'setTourKey', payload: nextPart },
{ type: 'setLastStepIndex', payload: 0 },
{ type: 'setHelpContentIndex', payload: 0 },
],
done,
);
});
});
describe('setExitTour', () => {
it(`commits ${types.SET_EXIT_TOUR} mutation`, done => {
const exitTour = true;
testAction(
setExitTour,
exitTour,
state,
[{ type: types.SET_EXIT_TOUR, payload: exitTour }],
[],
done,
);
});
});
describe('setDismissed', () => {
it(`commits ${types.SET_DISMISSED} mutation`, done => {
const dismissed = true;
testAction(
setDismissed,
dismissed,
state,
[{ type: types.SET_DISMISSED, payload: dismissed }],
[],
() => {
setTimeout(() => {
expect(Cookies.get(ONBOARDING_DISMISSED_COOKIE_NAME)).toEqual(`${dismissed}`);
done();
}, 0);
},
);
});
});
});
import * as getters from 'ee/onboarding/onboarding_helper/store/getters';
import createStore from 'ee/onboarding/onboarding_helper/store/state';
import { mockTourData } from './../mock_data';
describe('User onboarding store getters', () => {
let localState;
beforeEach(() => {
localState = createStore();
localState.projectFullPath = 'http://gitlab-org/gitlab-test';
localState.tourData = mockTourData;
localState.tourKey = 1;
localState.url = 'http://gitlab-org/gitlab-test/foo/bar';
});
describe('stepIndex', () => {
it('returns the current step index if the url matches the data at a given tour key', () => {
expect(getters.stepIndex(localState)).toBe(1);
});
it('returns null if there is no tour data', () => {
localState.tourData = [];
expect(getters.stepIndex(localState)).toBe(null);
});
it('returns null if there is no tour key', () => {
localState.tourKey = null;
expect(getters.stepIndex(localState)).toBe(null);
});
it("returns null if the url doesn't match any data at a given tour key", () => {
localState.url = 'http://not-matching/url';
expect(getters.stepIndex(localState)).toBe(null);
});
it("returns null if the url doesn't match any data due to a different project full path", () => {
localState.projectFullPath = 'http://my-path/does/not/match';
expect(getters.stepIndex(localState)).toBe(null);
});
});
describe('stepContent', () => {
it('returns the correct step content for the active tour step', () => {
const tourKey = 1;
const stepIndex = 1;
const localGetters = {
stepIndex,
};
expect(getters.stepContent(localState, localGetters)).toBe(mockTourData[tourKey][stepIndex]);
});
it('returns null if there is no tour data', () => {
localState.tourData = [];
const localGetters = {
stepIndex: 1,
};
expect(getters.stepContent(localState, localGetters)).toBe(null);
});
it('returns null if there is no step index', () => {
const localGetters = {
stepIndex: null,
};
expect(getters.stepContent(localState, localGetters)).toBe(null);
});
});
describe('helpContent', () => {
it('returns the help content for a given index', () => {
const helpContentIndex = 0;
const stepContent = {
getHelpContent: () => [
{
text: 'foo',
buttons: [{ text: 'button', btnClass: 'btn-primary' }],
},
],
};
const localGetters = {
stepContent,
};
localState.helpContentIndex = helpContentIndex;
expect(getters.helpContent(localState, localGetters)).toEqual(
stepContent.getHelpContent()[helpContentIndex],
);
});
it('displays the project name in the help content text', () => {
const helpContentIndex = 0;
const stepContent = {
getHelpContent: ({ projectName }) => [
{
text: `This is the ${projectName}`,
buttons: [{ text: 'button', btnClass: 'btn-primary' }],
},
],
};
const localGetters = {
stepContent,
};
localState.helpContentIndex = helpContentIndex;
localState.projectName = 'Mock Project';
const helpContent = getters.helpContent(localState, localGetters);
expect(helpContent.text).toBe('This is the Mock Project');
});
it('returns null if there is no step content', () => {
const localGetters = {
stepContent: null,
};
localState.helpContentIndex = 0;
expect(getters.helpContent(localState, localGetters)).toBe(null);
});
it('returns null if there is no getHelpContent property on the step content', () => {
const stepContent = {
getHelpContent: null,
};
const localGetters = {
stepContent,
};
expect(getters.helpContent(localState, localGetters)).toBe(null);
});
});
describe('totalTourPartSteps', () => {
it('returns the correct number of total tour steps for the tour with key "1"', () => {
expect(getters.totalTourPartSteps(localState)).toBe(3);
});
it('returns 0 if there is no tour data', () => {
localState.tourData = [];
expect(getters.totalTourPartSteps(localState)).toBe(0);
});
it('returns 0 if there is no tour key', () => {
localState.tourKey = null;
expect(getters.totalTourPartSteps(localState)).toBe(0);
});
it('returns 0 if there is no data at a given tour key', () => {
localState.tourKey = 10;
expect(getters.totalTourPartSteps(localState)).toBe(0);
});
});
describe('percentageCompleted', () => {
it('returns the percentage completed for the current step', () => {
const localGetters = {
stepIndex: 1,
};
expect(getters.percentageCompleted(localState, localGetters)).toBe(20);
});
it('returns the 0 if there is no step index', () => {
const localGetters = {
stepIndex: null,
};
expect(getters.percentageCompleted(localState, localGetters)).toBe(0);
});
it('returns the 0 if there is no data for a given step index', () => {
const localGetters = {
stepIndex: 10,
};
expect(getters.percentageCompleted(localState, localGetters)).toBe(0);
});
});
describe('actionPopover', () => {
it("returns the step content's action popover if the step content exists", () => {
const stepContent = {
actionPopover: {
selector: '.popover-selector',
text: 'Some action popover content',
},
};
const localGetters = {
stepContent,
};
expect(getters.actionPopover(localState, localGetters)).toEqual(stepContent.actionPopover);
});
it('returns null if there is no step content', () => {
const localGetters = {
stepContent: null,
};
expect(getters.actionPopover(localState, localGetters)).toBeNull();
});
});
});
import createState from 'ee/onboarding/onboarding_helper/store/state';
import mutations from 'ee/onboarding/onboarding_helper/store/mutations';
import * as types from 'ee/onboarding/onboarding_helper/store/mutation_types';
import { mockTourData } from './../mock_data';
describe('User onboarding helper store mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('SET_INITIAL_DATA', () => {
it('sets all inital data', () => {
const initialData = {
url: 'http://gitlab-org/gitlab-test/foo',
projectFullPath: 'http://gitlab-org/gitlab-test',
projectName: 'Mock Project',
tourData: mockTourData,
tourKey: 1,
helpContentIndex: 0,
lastStepIndex: -1,
dismissed: false,
createdProjectPath: '',
exitTour: false,
};
mutations[types.SET_INITIAL_DATA](state, initialData);
expect(state).toEqual(initialData);
});
});
describe('SET_TOUR_KEY', () => {
it('sets the tour key', () => {
const tourKey = 2;
mutations[types.SET_TOUR_KEY](state, tourKey);
expect(state.tourKey).toEqual(tourKey);
});
});
describe('SET_LAST_STEP_INDEX', () => {
it('sets the last step index', () => {
const lastStepIndex = 1;
mutations[types.SET_LAST_STEP_INDEX](state, lastStepIndex);
expect(state.lastStepIndex).toEqual(lastStepIndex);
});
});
describe('SET_HELP_CONTENT_INDEX', () => {
it('sets the help content index', () => {
const helpContentIndex = 1;
mutations[types.SET_HELP_CONTENT_INDEX](state, helpContentIndex);
expect(state.helpContentIndex).toEqual(helpContentIndex);
});
});
describe('SET_EXIT_TOUR', () => {
it('sets the exitTour property to true', () => {
mutations[types.SET_EXIT_TOUR](state, true);
expect(state.exitTour).toBeTruthy();
});
});
describe('SET_DISMISSED', () => {
it('sets the dismissed property to true', () => {
mutations[types.SET_DISMISSED](state, true);
expect(state.dismissed).toBeTruthy();
});
});
});
...@@ -14589,12 +14589,18 @@ msgstr "" ...@@ -14589,12 +14589,18 @@ msgstr ""
msgid "UserOnboardingTour|Commits are shown in chronological order and can be filtered by the commit message or by the branch." msgid "UserOnboardingTour|Commits are shown in chronological order and can be filtered by the commit message or by the branch."
msgstr "" msgstr ""
msgid "UserOnboardingTour|Create a project"
msgstr ""
msgid "UserOnboardingTour|Exit 'Learn GitLab'" msgid "UserOnboardingTour|Exit 'Learn GitLab'"
msgstr "" msgstr ""
msgid "UserOnboardingTour|Got it" msgid "UserOnboardingTour|Got it"
msgstr "" msgstr ""
msgid "UserOnboardingTour|Guided GitLab Tour"
msgstr ""
msgid "UserOnboardingTour|Here you can compare the changes of this branch to another one. Changes are divided by files so that it's easier to see what was changed where." msgid "UserOnboardingTour|Here you can compare the changes of this branch to another one. Changes are divided by files so that it's easier to see what was changed where."
msgstr "" msgstr ""
...@@ -14613,6 +14619,9 @@ msgstr "" ...@@ -14613,6 +14619,9 @@ msgstr ""
msgid "UserOnboardingTour|Here's an overview of branches in the %{emphasisStart}%{projectName}%{emphasisEnd} project. They're split into Active and Stale.%{lineBreak}%{lineBreak}From here, you can create a new merge request, from a branch or compare the branch to any other branch in the project. By default, it will compare it to the master branch." msgid "UserOnboardingTour|Here's an overview of branches in the %{emphasisStart}%{projectName}%{emphasisEnd} project. They're split into Active and Stale.%{lineBreak}%{lineBreak}From here, you can create a new merge request, from a branch or compare the branch to any other branch in the project. By default, it will compare it to the master branch."
msgstr "" msgstr ""
msgid "UserOnboardingTour|Invite colleagues"
msgstr ""
msgid "UserOnboardingTour|Issues are great for communicating and keeping track of progess in GitLab. These are all issues that are open in the %{emphasisStart}%{projectName}%{emphasisEnd}.%{lineBreak}%{lineBreak}You can help us improve GitLab by contributing work to issues that are labeled <span class=\"badge color-label accept-mr-label\">Accepting merge requests</span>.%{lineBreak}%{lineBreak}This list can be filtered by labels, milestones, assignees, authors... We'll show you how it looks like when the list is filtered by a label." msgid "UserOnboardingTour|Issues are great for communicating and keeping track of progess in GitLab. These are all issues that are open in the %{emphasisStart}%{projectName}%{emphasisEnd}.%{lineBreak}%{lineBreak}You can help us improve GitLab by contributing work to issues that are labeled <span class=\"badge color-label accept-mr-label\">Accepting merge requests</span>.%{lineBreak}%{lineBreak}This list can be filtered by labels, milestones, assignees, authors... We'll show you how it looks like when the list is filtered by a label."
msgstr "" msgstr ""
...@@ -14652,6 +14661,9 @@ msgstr "" ...@@ -14652,6 +14661,9 @@ msgstr ""
msgid "UserOnboardingTour|Take a look. Here's a nifty menu for quickly creating issues, merge requests, snippets, projects and groups. Click on it and select \"New project\" from the \"GitLab\" section to get started." msgid "UserOnboardingTour|Take a look. Here's a nifty menu for quickly creating issues, merge requests, snippets, projects and groups. Click on it and select \"New project\" from the \"GitLab\" section to get started."
msgstr "" msgstr ""
msgid "UserOnboardingTour|Thanks for taking the guided tour. Remember, if you want to go through it again, you can start %{emphasisStart}Learn GitLab%{emphasisEnd} in the help menu on the top right."
msgstr ""
msgid "UserOnboardingTour|That's it for issues. Let'st take a look at %{emphasisStart}Merge Requests%{emphasisEnd}." msgid "UserOnboardingTour|That's it for issues. Let'st take a look at %{emphasisStart}Merge Requests%{emphasisEnd}."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment