Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-19 03:08:03 +00:00
parent ca98ae2df5
commit 5b20366d04
29 changed files with 314 additions and 192 deletions

View File

@ -1 +1 @@
14.3.1
14.4.0

View File

@ -1,7 +1,8 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { formType } from '../constants';
@ -170,17 +171,7 @@ export default {
}
},
methods: {
...mapActions(['setError', 'unsetError']),
boardCreateResponse(data) {
return data.createBoard.board.webPath;
},
boardUpdateResponse(data) {
const path = data.updateBoard.board.webPath;
const param = getParameterByName('group_by')
? `?group_by=${getParameterByName('group_by')}`
: '';
return `${path}${param}`;
},
...mapActions(['setError', 'unsetError', 'setBoard']),
cancel() {
this.$emit('cancel');
},
@ -191,10 +182,10 @@ export default {
});
if (!this.board.id) {
return this.boardCreateResponse(response.data);
return response.data.createBoard.board;
}
return this.boardUpdateResponse(response.data);
return response.data.updateBoard.board;
},
async deleteBoard() {
await this.$apollo.mutate({
@ -218,8 +209,14 @@ export default {
}
} else {
try {
const url = await this.createOrUpdateBoard();
visitUrl(url);
const board = await this.createOrUpdateBoard();
this.setBoard(board);
this.cancel();
const param = getParameterByName('group_by')
? `?group_by=${getParameterByName('group_by')}`
: '';
updateHistory({ url: `${this.boardBaseUrl}/${getIdFromGraphQLId(board.id)}${param}` });
} catch {
this.setError({ message: this.$options.i18n.saveErrorMessage });
} finally {

View File

@ -45,9 +45,6 @@ export default {
},
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: {
boardId: {
default: '',
},
weightFeatureAvailable: {
default: false,
},
@ -78,7 +75,7 @@ export default {
},
},
computed: {
...mapState(['activeId', 'filterParams']),
...mapState(['activeId', 'filterParams', 'boardId']),
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);

View File

@ -19,8 +19,6 @@ import { s__ } from '~/locale';
import eventHub from '../eventhub';
import groupBoardsQuery from '../graphql/group_boards.query.graphql';
import projectBoardsQuery from '../graphql/project_boards.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupRecentBoardsQuery from '../graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '../graphql/project_recent_boards.query.graphql';
@ -69,48 +67,15 @@ export default {
maxPosition: 0,
filterTerm: '',
currentPage: '',
board: {},
};
},
apollo: {
board: {
query() {
return this.currentBoardQuery;
},
variables() {
return {
fullPath: this.fullPath,
boardId: this.fullBoardId,
};
},
update(data) {
const board = data.workspace?.board;
this.setBoardConfig(board);
return {
...board,
labels: board?.labels?.nodes,
};
},
error() {
this.setError({ message: this.$options.i18n.errorFetchingBoard });
},
},
},
computed: {
...mapState(['boardType', 'fullBoardId']),
...mapState(['boardType', 'board', 'isBoardLoading']),
...mapGetters(['isGroupBoard', 'isProjectBoard']),
parentType() {
return this.boardType;
},
currentBoardQueryCE() {
return this.isGroupBoard ? groupBoardQuery : projectBoardQuery;
},
currentBoardQuery() {
return this.currentBoardQueryCE;
},
isBoardLoading() {
return this.$apollo.queries.board.loading;
},
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},

View File

@ -1,8 +1,9 @@
#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
mutation createBoard($input: CreateBoardInput!) {
createBoard(input: $input) {
board {
id
webPath
...BoardScopeFragment
}
errors
}

View File

@ -1,8 +1,9 @@
#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
mutation UpdateBoard($input: UpdateBoardInput!) {
updateBoard(input: $input) {
board {
id
webPath
...BoardScopeFragment
}
errors
}

View File

@ -54,7 +54,6 @@ function mountBoardApp(el) {
apolloProvider,
provide: {
disabled: parseBoolean(el.dataset.disabled),
boardId,
groupId: Number(groupId),
rootPath,
fullPath,

View File

@ -31,10 +31,12 @@ import {
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import eventHub from '../eventhub';
import { gqlClient } from '../graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
@ -49,6 +51,8 @@ import * as types from './mutation_types';
export default {
fetchBoard: ({ commit, dispatch }, { fullPath, fullBoardId, boardType }) => {
commit(types.REQUEST_CURRENT_BOARD);
const variables = {
fullPath,
boardId: fullBoardId,
@ -60,9 +64,13 @@ export default {
variables,
})
.then(({ data }) => {
const board = data.workspace?.board;
commit(types.RECEIVE_BOARD_SUCCESS, board);
dispatch('setBoardConfig', board);
if (data.workspace?.errors) {
commit(types.RECEIVE_BOARD_FAILURE);
} else {
const board = data.workspace?.board;
commit(types.RECEIVE_BOARD_SUCCESS, board);
dispatch('setBoardConfig', board);
}
})
.catch(() => commit(types.RECEIVE_BOARD_FAILURE));
},
@ -87,6 +95,13 @@ export default {
commit(types.SET_BOARD_CONFIG, config);
},
setBoard: async ({ commit, dispatch }, board) => {
commit(types.RECEIVE_BOARD_SUCCESS, board);
await dispatch('setBoardConfig', board);
dispatch('performSearch', { resetLists: true });
eventHub.$emit('updateTokens');
},
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
},
@ -107,16 +122,16 @@ export default {
);
},
performSearch({ dispatch }) {
performSearch({ dispatch }, { resetLists = false } = {}) {
dispatch(
'setFilters',
convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
);
dispatch('fetchLists');
dispatch('fetchLists', { resetLists });
dispatch('resetIssues');
},
fetchLists: ({ commit, state, dispatch }) => {
fetchLists: ({ commit, state, dispatch }, { resetLists = false } = {}) => {
const { boardType, filterParams, fullPath, fullBoardId, issuableType } = state;
const variables = {
@ -133,6 +148,7 @@ export default {
.query({
query: listsQuery[issuableType].query,
variables,
...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
})
.then(({ data }) => {
const { lists, hideBacklogList } = data[boardType].board;

View File

@ -1,3 +1,4 @@
export const REQUEST_CURRENT_BOARD = 'REQUEST_CURRENT_BOARD';
export const RECEIVE_BOARD_SUCCESS = 'RECEIVE_BOARD_SUCCESS';
export const RECEIVE_BOARD_FAILURE = 'RECEIVE_BOARD_FAILURE';
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';

View File

@ -1,5 +1,6 @@
import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
@ -33,15 +34,23 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
};
export default {
[mutationTypes.REQUEST_CURRENT_BOARD]: (state) => {
state.isBoardLoading = true;
},
[mutationTypes.RECEIVE_BOARD_SUCCESS]: (state, board) => {
state.board = {
...board,
labels: board?.labels?.nodes || [],
};
state.fullBoardId = board.id;
state.boardId = getIdFromGraphQLId(board.id);
state.isBoardLoading = false;
},
[mutationTypes.RECEIVE_BOARD_FAILURE]: (state) => {
state.error = s__('Boards|An error occurred while fetching the board. Please reload the page.');
state.isBoardLoading = false;
},
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {

View File

@ -2,6 +2,7 @@ import { inactiveId, ListType } from '~/boards/constants';
export default () => ({
board: {},
isBoardLoading: false,
boardType: null,
issuableType: null,
fullPath: null,

View File

@ -75,25 +75,25 @@ export default {
<gl-button-group class="gl-ml-3">
<gl-button
v-gl-tooltip.hover
:title="__('Jump to previous unresolved thread')"
:aria-label="__('Jump to previous unresolved thread')"
:title="__('Go to previous unresolved thread')"
:aria-label="__('Go to previous unresolved thread')"
class="discussion-previous-btn gl-rounded-base! gl-px-2!"
data-track-action="click_button"
data-track-label="mr_previous_unresolved_thread"
data-track-property="click_previous_unresolved_thread_top"
icon="angle-up"
icon="chevron-lg-up"
category="tertiary"
@click="jumpToPreviousDiscussion"
/>
<gl-button
v-gl-tooltip.hover
:title="__('Jump to next unresolved thread')"
:aria-label="__('Jump to next unresolved thread')"
:title="__('Go to next unresolved thread')"
:aria-label="__('Go to next unresolved thread')"
class="discussion-next-btn gl-rounded-base! gl-px-2!"
data-track-action="click_button"
data-track-label="mr_next_unresolved_thread"
data-track-property="click_next_unresolved_thread_top"
icon="angle-down"
icon="chevron-lg-down"
category="tertiary"
@click="jumpToNextDiscussion"
/>

View File

@ -1,12 +1,19 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { first } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, n__ } from '~/locale';
import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE,
FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE,
} from '../../constants';
import getPackagePipelinesQuery from '../../graphql/queries/get_package_pipelines.query.graphql';
import PackageHistoryLoader from './package_history_loader.vue';
export default {
name: 'PackageHistory',
@ -25,6 +32,7 @@ export default {
GlLink,
GlSprintf,
HistoryItem,
PackageHistoryLoader,
TimeAgoTooltip,
},
props: {
@ -37,15 +45,27 @@ export default {
required: true,
},
},
apollo: {
pipelines: {
query: getPackagePipelinesQuery,
variables() {
return this.queryVariables;
},
update(data) {
return data.package?.pipelines?.nodes || [];
},
error() {
createFlash({ message: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE });
},
},
},
data() {
return {
pipelines: [],
showDescription: false,
};
},
computed: {
pipelines() {
return this.packageEntity?.pipelines?.nodes || [];
},
firstPipeline() {
return first(this.pipelines);
},
@ -65,6 +85,15 @@ export default {
this.archivedLines,
);
},
isLoading() {
return this.$apollo.queries.pipelines.loading;
},
queryVariables() {
return {
id: this.packageEntity.id,
first: GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE,
};
},
},
methods: {
truncate(value) {
@ -80,7 +109,8 @@ export default {
<template>
<div class="issuable-discussion">
<h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
<package-history-loader v-if="isLoading" />
<ul v-else class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
<history-item icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>

View File

@ -0,0 +1,24 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
export default {
components: {
GlSkeletonLoader,
},
loader: {
width: 580,
height: 80,
},
};
</script>
<template>
<div class="gl-ml-5 gl-md-max-w-70p">
<gl-skeleton-loader :width="$options.loader.width" :height="$options.loader.height">
<rect x="49" y="9" width="531" height="16" rx="4" />
<circle cx="16" cy="16" r="16" />
<rect x="49" y="57" width="302" height="16" rx="4" />
<circle cx="16" cy="64" r="16" />
</gl-skeleton-loader>
</div>
</template>

View File

@ -72,6 +72,9 @@ export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
'PackageRegistry|Failed to load the package data',
);
export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while fetching the package history.',
);
export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
export const PACKAGE_REGISTRY_TITLE = __('Package Registry');
@ -149,3 +152,5 @@ export const CONAN_HELP_PATH = helpPagePath('user/packages/conan_repository/inde
export const NUGET_HELP_PATH = helpPagePath('user/packages/nuget_repository/index');
export const PYPI_HELP_PATH = helpPagePath('user/packages/pypi_repository/index');
export const COMPOSER_HELP_PATH = helpPagePath('user/packages/composer_repository/index');
export const GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE = 10;

View File

@ -27,25 +27,6 @@ query getPackageDetails($id: PackagesPackageID!) {
name
}
}
pipelines(first: 10) {
nodes {
ref
id
sha
createdAt
commitPath
path
user {
id
name
}
project {
id
name
webUrl
}
}
}
packageFiles(first: 100) {
nodes {
id

View File

@ -0,0 +1,24 @@
query getPackagePipelines($id: PackagesPackageID!, $first: Int) {
package(id: $id) {
id
pipelines(first: $first) {
nodes {
ref
id
sha
createdAt
commitPath
path
user {
id
name
}
project {
id
name
webUrl
}
}
}
}
}

View File

@ -0,0 +1,8 @@
---
name: remove_mergeable_state_check
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86612
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362555
milestone: '15.1'
type: development
group: group::code review
default_enabled: false

View File

@ -313,6 +313,10 @@ This creates two jobs:
The `covfuzz-ci.yml` is the same as that in the [original synchronous example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example#running-go-fuzz-from-ci).
## FIPS-enabled binary
[Starting in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/352549) the coverage fuzzing binary is compiled with `golang-fips` on Linux x86 and uses OpenSSL as the cryptographic backend. For more details, see [FIPS compliance at GitLab with Go](../../../development/fips_compliance.md#go).
## Offline environment
To use coverage fuzzing in an offline environment:

View File

@ -17516,6 +17516,9 @@ msgstr ""
msgid "Go to next page"
msgstr ""
msgid "Go to next unresolved thread"
msgstr ""
msgid "Go to page %{page}"
msgstr ""
@ -17528,6 +17531,9 @@ msgstr ""
msgid "Go to previous page"
msgstr ""
msgid "Go to previous unresolved thread"
msgstr ""
msgid "Go to primary site"
msgstr ""
@ -22065,12 +22071,6 @@ msgstr ""
msgid "July"
msgstr ""
msgid "Jump to next unresolved thread"
msgstr ""
msgid "Jump to previous unresolved thread"
msgstr ""
msgid "Jun"
msgstr ""
@ -27154,6 +27154,9 @@ msgstr ""
msgid "PackageRegistry|Something went wrong while deleting the package."
msgstr ""
msgid "PackageRegistry|Something went wrong while fetching the package history."
msgstr ""
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""

View File

@ -20,8 +20,6 @@ describe('Board Column Component', () => {
};
const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
const boardId = '1';
const listMock = {
...listObj,
listType,
@ -39,9 +37,6 @@ describe('Board Column Component', () => {
disabled: false,
list: listMock,
},
provide: {
boardId,
},
});
};

View File

@ -1,4 +1,6 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -8,7 +10,6 @@ import { formType } from '~/boards/constants';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
import { createStore } from '~/boards/stores';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@ -16,6 +17,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
Vue.use(Vuex);
const currentBoard = {
id: 'gid://gitlab/Board/1',
name: 'test',
@ -46,11 +49,18 @@ describe('BoardForm', () => {
const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message');
const findInput = () => wrapper.find('#board-new-name');
const store = createStore({
const setBoardMock = jest.fn();
const setErrorMock = jest.fn();
const store = new Vuex.Store({
getters: {
isGroupBoard: () => true,
isProjectBoard: () => false,
},
actions: {
setBoard: setBoardMock,
setError: setErrorMock,
},
});
const createComponent = (props, data) => {
@ -168,7 +178,7 @@ describe('BoardForm', () => {
expect(mutate).not.toHaveBeenCalled();
});
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
it('calls a correct GraphQL mutation and sets board in state', async () => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
@ -184,13 +194,12 @@ describe('BoardForm', () => {
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('test-path');
expect(setBoardMock).toHaveBeenCalledTimes(1);
});
it('shows a GlAlert if GraphQL mutation fails', async () => {
it('sets error in state if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.new });
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
fillForm();
@ -199,8 +208,8 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalled();
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
expect(wrapper.vm.setError).toHaveBeenCalled();
expect(setBoardMock).not.toHaveBeenCalled();
expect(setErrorMock).toHaveBeenCalled();
});
});
});
@ -256,7 +265,8 @@ describe('BoardForm', () => {
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('test-path');
expect(setBoardMock).toHaveBeenCalledTimes(1);
expect(global.window.location.href).not.toContain('?group_by=epic');
});
it('calls GraphQL mutation with correct parameters when issues are grouped by epic', async () => {
@ -282,13 +292,13 @@ describe('BoardForm', () => {
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('test-path?group_by=epic');
expect(setBoardMock).toHaveBeenCalledTimes(1);
expect(global.window.location.href).toContain('?group_by=epic');
});
it('shows a GlAlert if GraphQL mutation fails', async () => {
it('sets error in state if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.edit });
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findInput().trigger('keyup.enter', { metaKey: true });
@ -297,8 +307,8 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalled();
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
expect(wrapper.vm.setError).toHaveBeenCalled();
expect(setBoardMock).not.toHaveBeenCalled();
expect(setErrorMock).toHaveBeenCalled();
});
});

View File

@ -2,11 +2,10 @@ import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import { BoardType } from '~/boards/constants';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
@ -15,8 +14,7 @@ import defaultStore from '~/boards/stores';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
mockGroupBoardResponse,
mockProjectBoardResponse,
mockBoard,
mockGroupAllBoardsResponse,
mockProjectAllBoardsResponse,
mockGroupRecentBoardsResponse,
@ -49,6 +47,7 @@ describe('BoardsSelector', () => {
},
state: {
boardType: isGroupBoard ? 'group' : 'project',
board: mockBoard,
},
});
};
@ -65,9 +64,6 @@ describe('BoardsSelector', () => {
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
const projectBoardsQueryHandlerSuccess = jest
.fn()
.mockResolvedValue(mockProjectAllBoardsResponse);
@ -92,8 +88,6 @@ describe('BoardsSelector', () => {
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
} = {}) => {
fakeApollo = createMockApollo([
[projectBoardQuery, projectBoardQueryHandlerSuccess],
[groupBoardQuery, groupBoardQueryHandlerSuccess],
[projectBoardsQuery, projectBoardsQueryHandler],
[groupBoardsQuery, groupBoardsQueryHandlerSuccess],
[projectRecentBoardsQuery, projectRecentBoardsQueryHandler],
@ -133,12 +127,13 @@ describe('BoardsSelector', () => {
describe('loading', () => {
// we are testing loading state, so don't resolve responses until after the tests
afterEach(async () => {
await nextTick();
await waitForPromises();
});
it('shows loading spinner', () => {
it('shows loading spinner', async () => {
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
await nextTick();
expect(getLoadingIcon().exists()).toBe(true);
expect(getDropdownHeaders()).toHaveLength(0);
@ -251,23 +246,4 @@ describe('BoardsSelector', () => {
expect(notCalledHandler).not.toHaveBeenCalled();
});
});
describe('fetching current board', () => {
it.each`
boardType | queryHandler | notCalledHandler
${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => {
createStore({
isProjectBoard: boardType === BoardType.project,
isGroupBoard: boardType === BoardType.group,
});
createComponent();
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
});
});
});

View File

@ -144,30 +144,6 @@ export const mockProjectRecentBoardsResponse = {
},
};
export const mockGroupBoardResponse = {
data: {
workspace: {
board: {
id: 'gid://gitlab/Board/1',
name: 'Development',
},
__typename: 'Group',
},
},
};
export const mockProjectBoardResponse = {
data: {
workspace: {
board: {
id: 'gid://gitlab/Board/2',
name: 'Development',
},
__typename: 'Project',
},
},
};
export const mockAssigneesList = [
{
id: 2,

View File

@ -84,6 +84,9 @@ describe('fetchBoard', () => {
action: actions.fetchBoard,
payload,
expectedMutations: [
{
type: types.REQUEST_CURRENT_BOARD,
},
{
type: types.RECEIVE_BOARD_SUCCESS,
payload: mockBoard,
@ -100,6 +103,9 @@ describe('fetchBoard', () => {
action: actions.fetchBoard,
payload,
expectedMutations: [
{
type: types.REQUEST_CURRENT_BOARD,
},
{
type: types.RECEIVE_BOARD_FAILURE,
},
@ -133,6 +139,20 @@ describe('setBoardConfig', () => {
});
});
describe('setBoard', () => {
it('dispatches setBoardConfig', () => {
return testAction({
action: actions.setBoard,
payload: mockBoard,
expectedMutations: [{ type: types.RECEIVE_BOARD_SUCCESS, payload: mockBoard }],
expectedActions: [
{ type: 'setBoardConfig', payload: mockBoard },
{ type: 'performSearch', payload: { resetLists: true } },
],
});
});
});
describe('setFilters', () => {
it.each([
[
@ -172,7 +192,11 @@ describe('performSearch', () => {
{},
{},
[],
[{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }],
[
{ type: 'setFilters', payload: {} },
{ type: 'fetchLists', payload: { resetLists: false } },
{ type: 'resetIssues' },
],
);
});
});

View File

@ -34,6 +34,14 @@ describe('Board Store Mutations', () => {
state = defaultState();
});
describe('REQUEST_CURRENT_BOARD', () => {
it('Should set isBoardLoading state to true', () => {
mutations[types.REQUEST_CURRENT_BOARD](state);
expect(state.isBoardLoading).toBe(true);
});
});
describe('RECEIVE_BOARD_SUCCESS', () => {
it('Should set board to state', () => {
mutations[types.RECEIVE_BOARD_SUCCESS](state, mockBoard);

View File

@ -1,17 +1,29 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { stubComponent } from 'helpers/stub_component';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
packageData,
packagePipelines,
packagePipelinesQuery,
} from 'jest/packages_and_registries/package_registry/mock_data';
import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import { FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE } from '~/packages_and_registries/package_registry/constants';
import component from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageHistoryLoader from '~/packages_and_registries/package_registry/components/details/package_history_loader.vue';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import waitForPromises from 'helpers/wait_for_promises';
import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql';
jest.mock('~/flash');
describe('Package History', () => {
let wrapper;
let apolloProvider;
const defaultProps = {
projectName: 'baz project',
packageEntity: { ...packageData() },
@ -22,8 +34,17 @@ describe('Package History', () => {
const createPipelines = (amount) =>
[...Array(amount)].map((x, index) => packagePipelines({ id: index + 1 })[0]);
const mountComponent = (props) => {
const mountComponent = (
props,
resolver = jest.fn().mockResolvedValue(packagePipelinesQuery()),
) => {
Vue.use(VueApollo);
const requestHandlers = [[getPackagePipelines, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(component, {
apolloProvider,
propsData: { ...defaultProps, ...props },
stubs: {
HistoryItem: stubComponent(HistoryItem, {
@ -38,23 +59,39 @@ describe('Package History', () => {
wrapper.destroy();
});
const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader);
const findHistoryElement = (testId) => wrapper.findByTestId(testId);
const findElementLink = (container) => container.findComponent(GlLink);
const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip);
const findTitle = () => wrapper.findByTestId('title');
const findTimeline = () => wrapper.findByTestId('timeline');
it('has the correct title', () => {
it('renders the loading container when loading', () => {
mountComponent();
expect(findPackageHistoryLoader().exists()).toBe(true);
});
it('does not render the loading container once resolved', async () => {
mountComponent();
await waitForPromises();
expect(findPackageHistoryLoader().exists()).toBe(false);
});
it('has the correct title', async () => {
mountComponent();
await waitForPromises();
const title = findTitle();
expect(title.exists()).toBe(true);
expect(title.text()).toBe('History');
});
it('has a timeline container', () => {
it('has a timeline container', async () => {
mountComponent();
await waitForPromises();
const title = findTimeline();
@ -64,6 +101,18 @@ describe('Package History', () => {
);
});
it('calls createFlash function if load fails', async () => {
mountComponent({}, jest.fn().mockRejectedValue());
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE,
}),
);
});
describe.each`
name | amount | icon | text | timeAgoTooltip | link
${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'@gitlab-org/package-15 version 1.0.0 was first created'} | ${packageData().createdAt} | ${null}
@ -78,11 +127,18 @@ describe('Package History', () => {
({ name, icon, text, timeAgoTooltip, link, amount }) => {
let element;
beforeEach(() => {
const packageEntity = { ...packageData(), pipelines: { nodes: createPipelines(amount) } };
mountComponent({
packageEntity,
});
beforeEach(async () => {
const packageEntity = { ...packageData() };
const pipelinesResolver = jest
.fn()
.mockResolvedValue(packagePipelinesQuery(createPipelines(amount)));
mountComponent(
{
packageEntity,
},
pipelinesResolver,
);
await waitForPromises();
element = findHistoryElement(name);
});

View File

@ -202,10 +202,6 @@ export const packageDetailsQuery = (extendPackage) => ({
nodes: packageTags(),
__typename: 'PackageTagConnection',
},
pipelines: {
nodes: packagePipelines(),
__typename: 'PipelineConnection',
},
packageFiles: {
nodes: packageFiles(),
__typename: 'PackageFileConnection',
@ -223,6 +219,19 @@ export const packageDetailsQuery = (extendPackage) => ({
},
});
export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({
data: {
package: {
id: 'gid://gitlab/Packages::Package/111',
pipelines: {
nodes: pipelines,
__typename: 'PipelineConnection',
},
__typename: 'PackageDetailsType',
},
},
});
export const emptyPackageDetailsQuery = () => ({
data: {
package: {

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Emails::ConfirmService do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
subject(:service) { described_class.new(user) }
@ -11,7 +11,9 @@ RSpec.describe Emails::ConfirmService do
it 'enqueues a background job to send confirmation email again' do
email = user.emails.create!(email: 'new@email.com')
expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers')
travel_to(10.minutes.from_now) do
expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers')
end
end
end
end