提交 19c433fa 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 0e54270e
......@@ -10,14 +10,16 @@ import {
GlDropdownItem,
GlTabs,
GlTab,
GlBadge,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import getAlerts from '../graphql/queries/getAlerts.query.graphql';
import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
......@@ -88,8 +90,8 @@ export default {
GlIcon,
GlTabs,
GlTab,
GlBadge,
},
mixins: [glFeatureFlagsMixin()],
props: {
projectPath: {
type: String,
......@@ -114,6 +116,7 @@ export default {
},
apollo: {
alerts: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getAlerts,
variables() {
return {
......@@ -122,12 +125,23 @@ export default {
};
},
update(data) {
return data.project.alertManagementAlerts.nodes;
return data.project?.alertManagementAlerts?.nodes;
},
error() {
this.errored = true;
},
},
alertsCount: {
query: getAlertsCountByStatus,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
return data.project?.alertManagementAlertStatusCounts;
},
},
},
data() {
return {
......@@ -139,7 +153,9 @@ export default {
},
computed: {
showNoAlertsMsg() {
return !this.errored && !this.loading && !this.alerts?.length && !this.isAlertDismissed;
return (
!this.errored && !this.loading && this.alertsCount?.all === 0 && !this.isAlertDismissed
);
},
showErrorMsg() {
return this.errored && !this.isErrorAlertDismissed;
......@@ -171,6 +187,7 @@ export default {
})
.then(() => {
this.$apollo.queries.alerts.refetch();
this.$apollo.queries.alertsCount.refetch();
})
.catch(() => {
createFlash(
......@@ -196,10 +213,13 @@ export default {
{{ $options.i18n.errorMsg }}
</gl-alert>
<gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterAlertsByStatus">
<gl-tabs @input="filterAlertsByStatus">
<gl-tab v-for="tab in $options.statusTabs" :key="tab.status">
<template slot="title">
<span>{{ tab.title }}</span>
<gl-badge v-if="alertsCount" pill size="sm" class="gl-tab-counter-badge">
{{ alertsCount[tab.status.toLowerCase()] }}
</gl-badge>
</template>
</gl-tab>
</gl-tabs>
......
#import "./listItem.fragment.graphql"
#import "./list_item.fragment.graphql"
fragment AlertDetailItem on AlertManagementAlert {
...AlertListItem
......
#import "../fragments/listItem.fragment.graphql"
#import "../fragments/list_item.fragment.graphql"
query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!]) {
project(fullPath: $projectPath) {
......
query getAlertsCount($projectPath: ID!) {
project(fullPath: $projectPath) {
alertManagementAlertStatusCounts {
all
open
acknowledged
resolved
triggered
}
}
}
......@@ -147,7 +147,7 @@ export default {
)
}}</span>
</template>
<template slot="modal-footer">
<template #modal-footer>
<gl-deprecated-button variant="secondary" @click="handleCancel">{{
s__('Cancel')
}}</gl-deprecated-button>
......
......@@ -507,9 +507,6 @@ export const cacheTreeListWidth = (_, size) => {
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
};
export const requestFullDiff = ({ commit }, filePath) => commit(types.REQUEST_FULL_DIFF, filePath);
export const receiveFullDiffSucess = ({ commit }, { filePath }) =>
commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath });
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
......@@ -600,7 +597,7 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
}
};
export const fetchFullDiff = ({ dispatch }, file) =>
export const fetchFullDiff = ({ commit, dispatch }, file) =>
axios
.get(file.context_lines_path, {
params: {
......@@ -609,15 +606,16 @@ export const fetchFullDiff = ({ dispatch }, file) =>
},
})
.then(({ data }) => {
dispatch('receiveFullDiffSucess', { filePath: file.file_path });
commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath: file.file_path });
dispatch('setExpandedDiffLines', { file, data });
})
.catch(() => dispatch('receiveFullDiffError', file.file_path));
export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
export const toggleFullDiff = ({ dispatch, commit, getters, state }, filePath) => {
const file = state.diffFiles.find(f => f.file_path === filePath);
dispatch('requestFullDiff', filePath);
commit(types.REQUEST_FULL_DIFF, filePath);
if (file.isShowingFullFile) {
dispatch('loadCollapsedDiff', file)
......
......@@ -158,13 +158,13 @@ export default {
:deploy-boards-help-path="deployBoardsHelpPath"
@onChangePage="onChangePage"
>
<empty-state
v-if="!isLoading && state.environments.length === 0"
slot="emptyState"
:new-path="newEnvironmentPath"
:help-path="helpPagePath"
:can-create-environment="canCreateEnvironment"
/>
<template v-if="!isLoading && state.environments.length === 0" #emptyState>
<empty-state
:new-path="newEnvironmentPath"
:help-path="helpPagePath"
:can-create-environment="canCreateEnvironment"
/>
</template>
</container>
</div>
</template>
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { otherSide } from '../utils';
import { SIDE_RIGHT } from '../constants';
export default {
directives: {
tooltip: GlTooltipDirective,
},
components: {
GlIcon,
},
props: {
tabs: {
type: Array,
required: true,
},
side: {
type: String,
required: true,
},
currentView: {
type: String,
required: false,
default: '',
},
isOpen: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
otherSide() {
return otherSide(this.side);
},
},
methods: {
isActiveTab(tab) {
return this.isOpen && tab.views.some(view => view.name === this.currentView);
},
buttonClasses(tab) {
return [
{
'is-right': this.side === SIDE_RIGHT,
active: this.isActiveTab(tab),
},
...(tab.buttonClasses || []),
];
},
clickTab(e, tab) {
e.currentTarget.blur();
this.$root.$emit('bv::hide::tooltip');
if (this.isActiveTab(tab)) {
this.$emit('close');
} else {
this.$emit('open', tab.views[0]);
}
},
},
};
</script>
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-for="tab of tabs" :key="tab.title">
<button
v-tooltip="{ container: 'body', placement: otherSide }"
:title="tab.title"
:aria-label="tab.title"
:class="buttonClasses(tab)"
:data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
class="ide-sidebar-link"
type="button"
@click="clickTab($event, tab)"
>
<gl-icon :size="16" :name="tab.icon" />
</button>
</li>
</ul>
</nav>
</template>
......@@ -3,7 +3,7 @@ import { mapActions, mapState } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import ResizablePanel from '../resizable_panel.vue';
import { GlSkeletonLoading } from '@gitlab/ui';
import IdeSidebarNav from '../ide_sidebar_nav.vue';
export default {
name: 'CollapsibleSidebar',
......@@ -13,7 +13,7 @@ export default {
components: {
Icon,
ResizablePanel,
GlSkeletonLoading,
IdeSidebarNav,
},
props: {
extensionTabs: {
......@@ -31,7 +31,6 @@ export default {
},
},
computed: {
...mapState(['loading']),
...mapState({
isOpen(state) {
return state[this.namespace].isOpen;
......@@ -39,9 +38,6 @@ export default {
currentView(state) {
return state[this.namespace].currentView;
},
isActiveView(state, getters) {
return getters[`${this.namespace}/isActiveView`];
},
isAliveView(_state, getters) {
return getters[`${this.namespace}/isAliveView`];
},
......@@ -59,9 +55,6 @@ export default {
aliveTabViews() {
return this.tabViews.filter(view => this.isAliveView(view.name));
},
otherSide() {
return this.side === 'right' ? 'left' : 'right';
},
},
methods: {
...mapActions({
......@@ -72,25 +65,6 @@ export default {
return dispatch(`${this.namespace}/open`, view);
},
}),
clickTab(e, tab) {
e.target.blur();
if (this.isActiveTab(tab)) {
this.toggleOpen();
} else {
this.open(tab.views[0]);
}
},
isActiveTab(tab) {
return tab.views.some(view => this.isActiveView(view.name));
},
buttonClasses(tab) {
return [
this.side === 'right' ? 'is-right' : '',
this.isActiveTab(tab) && this.isOpen ? 'active' : '',
...(tab.buttonClasses || []),
];
},
},
};
</script>
......@@ -110,40 +84,23 @@ export default {
class="multi-file-commit-panel-inner"
>
<div class="h-100 d-flex flex-column align-items-stretch">
<slot v-if="isOpen" name="header"></slot>
<div
v-for="tabView in aliveTabViews"
v-show="isActiveView(tabView.name)"
v-show="tabView.name === currentView"
:key="tabView.name"
class="flex-fill gl-overflow-hidden js-tab-view"
>
<component :is="tabView.component" />
</div>
<slot name="footer"></slot>
</div>
</resizable-panel>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li>
<slot name="header-icon"></slot>
</li>
<li v-for="tab of tabs" :key="tab.title">
<button
v-tooltip
:title="tab.title"
:aria-label="tab.title"
:class="buttonClasses(tab)"
data-container="body"
:data-placement="otherSide"
:data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
class="ide-sidebar-link"
type="button"
@click="clickTab($event, tab)"
>
<icon :size="16" :name="tab.icon" />
</button>
</li>
</ul>
</nav>
<ide-sidebar-nav
:tabs="tabs"
:side="side"
:current-view="currentView"
:is-open="isOpen"
@open="open"
@close="toggleOpen"
/>
</div>
</template>
......@@ -92,3 +92,6 @@ export const commitActionTypes = {
};
export const packageJsonPath = 'package.json';
export const SIDE_LEFT = 'left';
export const SIDE_RIGHT = 'right';
export const isActiveView = state => view => state.currentView === view;
export const isAliveView = (state, getters) => view =>
state.keepAliveViews[view] || (state.isOpen && getters.isActiveView(view));
// eslint-disable-next-line import/prefer-default-export
export const isAliveView = state => view =>
state.keepAliveViews[view] || (state.isOpen && state.currentView === view);
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
import { languages } from 'monaco-editor';
import { flatten } from 'lodash';
......@@ -73,3 +74,5 @@ export function registerLanguages(def, ...defs) {
languages.setMonarchTokensProvider(languageId, def.language);
languages.setLanguageConfiguration(languageId, def.conf);
}
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
......@@ -3,7 +3,6 @@
class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
before_action do
push_frontend_feature_flag(:alert_list_status_filtering_enabled)
push_frontend_feature_flag(:alert_management_create_alert_issue)
push_frontend_feature_flag(:alert_assignee, project)
end
......
---
title: Organize alerts by status tabs
merge_request: 32582
author:
type: added
---
title: Add anchor for creating a branch
merge_request: 32745
author:
type: other
---
title: Update deprecated slot syntax in ./app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
merge_request: 32010
author: Gilang Gumilar
type: other
---
title: Update deprecated slot syntax in ./app/assets/javascripts/environments/components/environments_app.vue
merge_request: 32011
author: Gilang Gumilar
type: other
---
stage: Secure
group: Dynamic Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference, howto
---
......
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# SAST Analyzers **(ULTIMATE)**
SAST relies on underlying third party tools that are wrapped into what we call
......
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference, howto
---
......
......@@ -12,8 +12,8 @@ to familiarize yourself with the concept, the terminology,
and to learn what you can do with them.
Every merge request starts by creating a branch. You can either
do it locally through the command line, via a Git CLI application,
or through the GitLab UI.
do it locally through the [command line](#new-merge-request-from-your-local-environment), via a Git CLI application,
or through the [GitLab UI](#new-merge-request-from-a-new-branch-created-through-the-ui).
This document describes the several ways to create a merge request.
......@@ -100,7 +100,7 @@ button to open the [**New Merge Request** page](#new-merge-request-page).
A new merge request will be started using the current branch as the source,
and the default branch in the current project as the target.
## New merge request from you local environment
## New merge request from your local environment
Assuming you have your repository cloned into your computer and you'd
like to start working on changes to files, start by creating and
......
......@@ -8,6 +8,7 @@ import {
GlDropdownItem,
GlIcon,
GlTab,
GlBadge,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -33,10 +34,19 @@ describe('AlertManagementList', () => {
const findLoader = () => wrapper.find(GlLoadingIcon);
const findStatusDropdown = () => wrapper.find(GlDropdown);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const alertsCount = {
acknowledged: 6,
all: 16,
open: 14,
resolved: 2,
triggered: 10,
};
function mountComponent({
props = {
alertManagementEnabled: false,
......@@ -44,7 +54,6 @@ describe('AlertManagementList', () => {
},
data = {},
loading = false,
alertListStatusFilteringEnabled = false,
stubs = {},
} = {}) {
wrapper = mount(AlertManagementList, {
......@@ -54,11 +63,6 @@ describe('AlertManagementList', () => {
emptyAlertSvgPath: 'illustration/path',
...props,
},
provide: {
glFeatures: {
alertListStatusFilteringEnabled,
},
},
data() {
return data;
},
......@@ -93,42 +97,25 @@ describe('AlertManagementList', () => {
});
describe('Status Filter Tabs', () => {
describe('alertListStatusFilteringEnabled feature flag enabled', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts },
loading: false,
alertListStatusFilteringEnabled: true,
stubs: {
GlTab: true,
},
});
});
it('should display filter tabs for all statuses', () => {
const tabs = findStatusFilterTabs().wrappers;
tabs.forEach((tab, i) => {
expect(tab.text()).toContain(ALERTS_STATUS_TABS[i].title);
});
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount },
loading: false,
stubs: {
GlTab: true,
},
});
});
describe('alertListStatusFilteringEnabled feature flag disabled', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts },
loading: false,
alertListStatusFilteringEnabled: false,
stubs: {
GlTab: true,
},
});
});
it('should display filter tabs with alerts count badge for each status', () => {
const tabs = findStatusFilterTabs().wrappers;
const badges = findStatusFilterBadge();
it('should NOT display tabs', () => {
expect(findStatusFilterTabs()).not.toExist();
tabs.forEach((tab, i) => {
const status = ALERTS_STATUS_TABS[i].status.toLowerCase();
expect(tab.text()).toContain(ALERTS_STATUS_TABS[i].title);
expect(badges.at(i).text()).toContain(alertsCount[status]);
});
});
});
......@@ -137,7 +124,7 @@ describe('AlertManagementList', () => {
it('loading state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: null },
data: { alerts: null, alertsCount: null },
loading: true,
});
expect(findAlertsTable().exists()).toBe(true);
......@@ -152,7 +139,7 @@ describe('AlertManagementList', () => {
it('error state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: null, errored: true },
data: { alerts: null, alertsCount: null, errored: true },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
......@@ -169,7 +156,7 @@ describe('AlertManagementList', () => {
it('empty state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: [], errored: false },
data: { alerts: [], alertsCount: { all: 0 }, errored: false },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
......@@ -186,7 +173,7 @@ describe('AlertManagementList', () => {
it('has data state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
expect(findLoader().exists()).toBe(false);
......@@ -202,7 +189,7 @@ describe('AlertManagementList', () => {
it('displays status dropdown', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
expect(findStatusDropdown().exists()).toBe(true);
......@@ -211,7 +198,7 @@ describe('AlertManagementList', () => {
it('shows correct severity icons', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
......@@ -228,7 +215,7 @@ describe('AlertManagementList', () => {
it('renders severity text', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
......@@ -242,7 +229,7 @@ describe('AlertManagementList', () => {
it('navigates to the detail page when alert row is clicked', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
......@@ -266,6 +253,7 @@ describe('AlertManagementList', () => {
severity: 'high',
},
],
alertsCount,
errored: false,
},
loading: false,
......@@ -286,6 +274,7 @@ describe('AlertManagementList', () => {
severity: 'high',
},
],
alertsCount,
errored: false,
},
loading: false,
......@@ -312,7 +301,7 @@ describe('AlertManagementList', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
});
......
......@@ -35,8 +35,6 @@ import {
setRenderTreeList,
setShowWhitespace,
setRenderIt,
requestFullDiff,
receiveFullDiffSucess,
receiveFullDiffError,
fetchFullDiff,
toggleFullDiff,
......@@ -1136,34 +1134,8 @@ describe('DiffsStoreActions', () => {
});
});
describe('requestFullDiff', () => {
it('commits REQUEST_FULL_DIFF', done => {
testAction(
requestFullDiff,
'file',
{},
[{ type: types.REQUEST_FULL_DIFF, payload: 'file' }],
[],
done,
);
});
});
describe('receiveFullDiffSucess', () => {
it('commits REQUEST_FULL_DIFF', done => {
testAction(
receiveFullDiffSucess,
{ filePath: 'test' },
{},
[{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }],
[],
done,
);
});
});
describe('receiveFullDiffError', () => {
it('commits REQUEST_FULL_DIFF', done => {
it('updates state with the file that did not load', done => {
testAction(
receiveFullDiffError,
'file',
......@@ -1191,7 +1163,7 @@ describe('DiffsStoreActions', () => {
mock.onGet(`${gl.TEST_HOST}/context`).replyOnce(200, ['test']);
});
it('dispatches receiveFullDiffSucess', done => {
it('commits the success and dispatches an action to expand the new lines', done => {
const file = {
context_lines_path: `${gl.TEST_HOST}/context`,
file_path: 'test',
......@@ -1201,11 +1173,8 @@ describe('DiffsStoreActions', () => {
fetchFullDiff,
file,
null,
[],
[
{ type: 'receiveFullDiffSucess', payload: { filePath: 'test' } },
{ type: 'setExpandedDiffLines', payload: { file, data: ['test'] } },
],
[{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }],
[{ type: 'setExpandedDiffLines', payload: { file, data: ['test'] } }],
done,
);
});
......@@ -1243,11 +1212,8 @@ describe('DiffsStoreActions', () => {
toggleFullDiff,
'test',
state,
[],
[
{ type: 'requestFullDiff', payload: 'test' },
{ type: 'fetchFullDiff', payload: state.diffFiles[0] },
],
[{ type: types.REQUEST_FULL_DIFF, payload: 'test' }],
[{ type: 'fetchFullDiff', payload: state.diffFiles[0] }],
done,
);
});
......
......@@ -53,7 +53,7 @@ describe('Environment', () => {
describe('without environments', () => {
beforeEach(() => {
mockRequest(200, { environments: [] });
return createWrapper(true);
return createWrapper();
});
it('should render the empty state', () => {
......@@ -118,7 +118,7 @@ describe('Environment', () => {
describe('unsuccessful request', () => {
beforeEach(() => {
mockRequest(500, {});
return createWrapper(true);
return createWrapper();
});
it('should render empty state', () => {
......
export const getKey = name => `$_gl_jest_${name}`;
export const getBinding = (el, name) => el[getKey(name)];
export const createMockDirective = () => ({
bind(el, { name, value, arg, modifiers }) {
el[getKey(name)] = {
value,
arg,
modifiers,
};
},
unbind(el, { name }) {
delete el[getKey(name)];
},
});
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import { SIDE_RIGHT, SIDE_LEFT } from '~/ide/constants';
const TEST_TABS = [
{
title: 'Lorem',
icon: 'angle-up',
views: [{ name: 'lorem-1' }, { name: 'lorem-2' }],
},
{
title: 'Ipsum',
icon: 'angle-down',
views: [{ name: 'ipsum-1' }, { name: 'ipsum-2' }],
},
];
const TEST_CURRENT_INDEX = 1;
const TEST_CURRENT_VIEW = TEST_TABS[TEST_CURRENT_INDEX].views[1].name;
const TEST_OPEN_VIEW = TEST_TABS[TEST_CURRENT_INDEX].views[0];
describe('~/ide/components/ide_sidebar_nav', () => {
let wrapper;
const createComponent = (props = {}) => {
if (wrapper) {
throw new Error('wrapper already exists');
}
wrapper = shallowMount(IdeSidebarNav, {
propsData: {
tabs: TEST_TABS,
currentView: TEST_CURRENT_VIEW,
isOpen: false,
...props,
},
directives: {
tooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findButtons = () => wrapper.findAll('li button');
const findButtonsData = () =>
findButtons().wrappers.map(button => {
return {
title: button.attributes('title'),
ariaLabel: button.attributes('aria-label'),
classes: button.classes(),
qaSelector: button.attributes('data-qa-selector'),
icon: button.find(GlIcon).props('name'),
tooltip: getBinding(button.element, 'tooltip').value,
};
});
const clickTab = () =>
findButtons()
.at(TEST_CURRENT_INDEX)
.trigger('click');
describe.each`
isOpen | side | otherSide | classes | classesObj | emitEvent | emitArg
${false} | ${SIDE_LEFT} | ${SIDE_RIGHT} | ${[]} | ${{}} | ${'open'} | ${[TEST_OPEN_VIEW]}
${false} | ${SIDE_RIGHT} | ${SIDE_LEFT} | ${['is-right']} | ${{}} | ${'open'} | ${[TEST_OPEN_VIEW]}
${true} | ${SIDE_RIGHT} | ${SIDE_LEFT} | ${['is-right']} | ${{ [TEST_CURRENT_INDEX]: ['active'] }} | ${'close'} | ${[]}
`(
'with side = $side, isOpen = $isOpen',
({ isOpen, side, otherSide, classes, classesObj, emitEvent, emitArg }) => {
let bsTooltipHide;
beforeEach(() => {
createComponent({ isOpen, side });
bsTooltipHide = jest.fn();
wrapper.vm.$root.$on('bv::hide::tooltip', bsTooltipHide);
});
it('renders buttons', () => {
expect(findButtonsData()).toEqual(
TEST_TABS.map((tab, index) => ({
title: tab.title,
ariaLabel: tab.title,
classes: ['ide-sidebar-link', ...classes, ...(classesObj[index] || [])],
qaSelector: `${tab.title.toLowerCase()}_tab_button`,
icon: tab.icon,
tooltip: {
container: 'body',
placement: otherSide,
},
})),
);
});
it('when tab clicked, emits event', () => {
expect(wrapper.emitted()).toEqual({});
clickTab();
expect(wrapper.emitted()).toEqual({
[emitEvent]: [emitArg],
});
});
it('when tab clicked, hides tooltip', () => {
expect(bsTooltipHide).not.toHaveBeenCalled();
clickTab();
expect(bsTooltipHide).toHaveBeenCalled();
});
},
);
});
......@@ -2,6 +2,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import { createStore } from '~/ide/stores';
import paneModule from '~/ide/stores/modules/pane';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import Vuex from 'vuex';
const localVue = createLocalVue();
......@@ -24,19 +25,15 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
width,
...props,
},
slots: {
'header-icon': '<div class=".header-icon-slot">SLOT ICON</div>',
header: '<div class=".header-slot"/>',
footer: '<div class=".footer-slot"/>',
},
});
};
const findTabButton = () => wrapper.find(`[data-qa-selector="${fakeComponentName}_tab_button"]`);
const findSidebarNav = () => wrapper.find(IdeSidebarNav);
beforeEach(() => {
store = createStore();
store.registerModule('leftPane', paneModule());
jest.spyOn(store, 'dispatch').mockImplementation();
});
afterEach(() => {
......@@ -75,92 +72,60 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
${'left'}
${'right'}
`('when side=$side', ({ side }) => {
it('correctly renders side specific attributes', () => {
beforeEach(() => {
createComponent({ extensionTabs, side });
const button = findTabButton();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.classes()).toContain('multi-file-commit-panel');
expect(wrapper.classes()).toContain(`ide-${side}-sidebar`);
expect(wrapper.find('.multi-file-commit-panel-inner')).not.toBe(null);
expect(wrapper.find(`.ide-${side}-sidebar-${fakeComponentName}`)).not.toBe(null);
expect(button.attributes('data-placement')).toEqual(side === 'left' ? 'right' : 'left');
if (side === 'right') {
// this class is only needed on the right side; there is no 'is-left'
expect(button.classes()).toContain('is-right');
} else {
expect(button.classes()).not.toContain('is-right');
}
});
});
});
describe('when default side', () => {
let button;
beforeEach(() => {
createComponent({ extensionTabs });
button = findTabButton();
it('correctly renders side specific attributes', () => {
expect(wrapper.classes()).toContain('multi-file-commit-panel');
expect(wrapper.classes()).toContain(`ide-${side}-sidebar`);
expect(wrapper.find('.multi-file-commit-panel-inner')).not.toBe(null);
expect(wrapper.find(`.ide-${side}-sidebar-${fakeComponentName}`)).not.toBe(null);
expect(findSidebarNav().props('side')).toBe(side);
});
it('correctly renders tab-specific classes', () => {
store.state.rightPane.currentView = fakeComponentName;
return wrapper.vm.$nextTick().then(() => {
expect(button.classes()).toContain('button-class-1');
expect(button.classes()).toContain('button-class-2');
});
it('nothing is dispatched', () => {
expect(store.dispatch).not.toHaveBeenCalled();
});
it('can show an open pane tab with an active view', () => {
store.state.rightPane.isOpen = true;
store.state.rightPane.currentView = fakeComponentName;
it('when sidebar emits open, dispatch open', () => {
const view = 'lorem-view';
return wrapper.vm.$nextTick().then(() => {
expect(button.classes()).toEqual(expect.arrayContaining(['ide-sidebar-link', 'active']));
expect(button.attributes('data-original-title')).toEqual(fakeComponentName);
expect(wrapper.find('.js-tab-view').exists()).toBe(true);
});
});
it('does not show a pane which is not open', () => {
store.state.rightPane.isOpen = false;
store.state.rightPane.currentView = fakeComponentName;
findSidebarNav().vm.$emit('open', view);
return wrapper.vm.$nextTick().then(() => {
expect(button.classes()).not.toEqual(
expect.arrayContaining(['ide-sidebar-link', 'active']),
);
expect(wrapper.find('.js-tab-view').exists()).toBe(false);
});
expect(store.dispatch).toHaveBeenCalledWith(`${side}Pane/open`, view);
});
describe('when button is clicked', () => {
it('opens view', () => {
button.trigger('click');
expect(store.state.rightPane.isOpen).toBeTruthy();
});
it('toggles open view if tab is currently active', () => {
button.trigger('click');
expect(store.state.rightPane.isOpen).toBeTruthy();
it('when sidebar emits close, dispatch toggleOpen', () => {
findSidebarNav().vm.$emit('close');
button.trigger('click');
expect(store.state.rightPane.isOpen).toBeFalsy();
});
expect(store.dispatch).toHaveBeenCalledWith(`${side}Pane/toggleOpen`);
});
});
it('shows header-icon', () => {
expect(wrapper.find('.header-icon-slot')).not.toBeNull();
describe.each`
isOpen
${true}
${false}
`('when isOpen=$isOpen', ({ isOpen }) => {
beforeEach(() => {
store.state.rightPane.isOpen = isOpen;
store.state.rightPane.currentView = fakeComponentName;
createComponent({ extensionTabs });
});
it('shows header', () => {
expect(wrapper.find('.header-slot')).not.toBeNull();
it(`tab view is shown=${isOpen}`, () => {
expect(wrapper.find('.js-tab-view').exists()).toBe(isOpen);
});
it('shows footer', () => {
expect(wrapper.find('.footer-slot')).not.toBeNull();
it('renders sidebar nav', () => {
expect(findSidebarNav().props()).toEqual({
tabs: extensionTabs,
side: 'right',
currentView: fakeComponentName,
isOpen,
});
});
});
});
......
......@@ -7,20 +7,6 @@ describe('IDE pane module getters', () => {
[TEST_VIEW]: true,
};
describe('isActiveView', () => {
it('returns true if given view matches currentView', () => {
const result = getters.isActiveView({ currentView: 'A' })('A');
expect(result).toBe(true);
});
it('returns false if given view does not match currentView', () => {
const result = getters.isActiveView({ currentView: 'A' })('B');
expect(result).toBe(false);
});
});
describe('isAliveView', () => {
it('returns true if given view is in keepAliveViews', () => {
const result = getters.isAliveView({ keepAliveViews: TEST_KEEP_ALIVE_VIEWS }, {})(TEST_VIEW);
......@@ -29,25 +15,25 @@ describe('IDE pane module getters', () => {
});
it('returns true if given view is active view and open', () => {
const result = getters.isAliveView(
{ ...state(), isOpen: true },
{ isActiveView: () => true },
)(TEST_VIEW);
const result = getters.isAliveView({ ...state(), isOpen: true, currentView: TEST_VIEW })(
TEST_VIEW,
);
expect(result).toBe(true);
});
it('returns false if given view is active view and closed', () => {
const result = getters.isAliveView(state(), { isActiveView: () => true })(TEST_VIEW);
const result = getters.isAliveView({ ...state(), currentView: TEST_VIEW })(TEST_VIEW);
expect(result).toBe(false);
});
it('returns false if given view is not activeView', () => {
const result = getters.isAliveView(
{ ...state(), isOpen: true },
{ isActiveView: () => false },
)(TEST_VIEW);
const result = getters.isAliveView({
...state(),
isOpen: true,
currentView: `${TEST_VIEW}_other`,
})(TEST_VIEW);
expect(result).toBe(false);
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册