Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
06eadec552
commit
f66bb12f38
|
@ -1 +1 @@
|
|||
d5024702496569b6de051046e5e295c44de94be5
|
||||
f085d841e2f5571b260910a454215bc1a7687bf0
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<script>
|
||||
import Draggable from 'vuedraggable';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { sortBy } from 'lodash';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
|
||||
import BoardColumnNew from './board_column_new.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import defaultSortableConfig from '~/sortable/sortable_config';
|
||||
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -36,6 +39,25 @@ export default {
|
|||
? sortBy([...Object.values(this.boardLists)], 'position')
|
||||
: this.lists;
|
||||
},
|
||||
canDragColumns() {
|
||||
return this.glFeatures.graphqlBoardLists && this.canAdminList;
|
||||
},
|
||||
boardColumnWrapper() {
|
||||
return this.canDragColumns ? Draggable : 'div';
|
||||
},
|
||||
draggableOptions() {
|
||||
const options = {
|
||||
...defaultSortableConfig,
|
||||
disabled: this.disabled,
|
||||
draggable: '.is-draggable',
|
||||
fallbackOnBody: false,
|
||||
group: 'boards-list',
|
||||
tag: 'div',
|
||||
value: this.lists,
|
||||
};
|
||||
|
||||
return this.canDragColumns ? options : {};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.glFeatures.graphqlBoardLists) {
|
||||
|
@ -43,7 +65,26 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['showPromotionList']),
|
||||
...mapActions(['moveList', 'showPromotionList']),
|
||||
handleDragOnStart() {
|
||||
sortableStart();
|
||||
},
|
||||
|
||||
handleDragOnEnd(params) {
|
||||
sortableEnd();
|
||||
|
||||
const { item, newIndex, oldIndex, to } = params;
|
||||
|
||||
const listId = item.dataset.id;
|
||||
const replacedListId = to.children[newIndex].dataset.id;
|
||||
|
||||
this.moveList({
|
||||
listId,
|
||||
replacedListId,
|
||||
newIndex,
|
||||
adjustmentValue: newIndex < oldIndex ? 1 : -1,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -53,16 +94,28 @@ export default {
|
|||
<gl-alert v-if="error" variant="danger" :dismissible="false">
|
||||
{{ error }}
|
||||
</gl-alert>
|
||||
<div v-if="!isSwimlanesOn" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap">
|
||||
<component
|
||||
:is="boardColumnWrapper"
|
||||
v-if="!isSwimlanesOn"
|
||||
ref="list"
|
||||
v-bind="draggableOptions"
|
||||
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
|
||||
data-qa-selector="boards_list"
|
||||
@start="handleDragOnStart"
|
||||
@end="handleDragOnEnd"
|
||||
>
|
||||
<board-column
|
||||
v-for="list in boardListsToUse"
|
||||
:key="list.id"
|
||||
ref="board"
|
||||
:can-admin-list="canAdminList"
|
||||
:class="{
|
||||
'is-draggable': !list.preset,
|
||||
}"
|
||||
:list="list"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<template v-else>
|
||||
<epics-swimlanes
|
||||
|
|
|
@ -180,6 +180,10 @@ export default {
|
|||
{ state, commit, dispatch },
|
||||
{ listId, replacedListId, newIndex, adjustmentValue },
|
||||
) => {
|
||||
if (listId === replacedListId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { boardLists } = state;
|
||||
const backupList = { ...boardLists };
|
||||
const movedList = boardLists[listId];
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -13,12 +12,6 @@ export default {
|
|||
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
||||
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
|
||||
},
|
||||
mounted() {
|
||||
this.patchAriaLabel();
|
||||
},
|
||||
updated() {
|
||||
this.patchAriaLabel();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', [
|
||||
'setInlineDiffViewType',
|
||||
|
@ -26,17 +19,18 @@ export default {
|
|||
'setRenderTreeList',
|
||||
'setShowWhitespace',
|
||||
]),
|
||||
patchAriaLabel() {
|
||||
this.$el
|
||||
.querySelector('.js-show-diff-settings')
|
||||
.setAttribute('aria-label', __('Diff view settings'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
|
||||
<gl-dropdown
|
||||
icon="settings"
|
||||
:text="__('Diff view settings')"
|
||||
:text-sr-only="true"
|
||||
toggle-class="js-show-diff-settings"
|
||||
right
|
||||
>
|
||||
<div class="gl-px-3">
|
||||
<span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
|
||||
<gl-button-group class="gl-display-flex">
|
||||
|
|
|
@ -235,10 +235,6 @@ h3.popover-header {
|
|||
@extend .border-0;
|
||||
}
|
||||
|
||||
&.card-without-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.bg-light {
|
||||
@extend .border-0;
|
||||
}
|
||||
|
|
|
@ -234,6 +234,8 @@ ul.content-list {
|
|||
}
|
||||
}
|
||||
|
||||
ul.content-list.issuable-list > li,
|
||||
ul.content-list.todos-list > li,
|
||||
.card > .content-list > li {
|
||||
padding: $gl-padding-top $gl-padding;
|
||||
}
|
||||
|
|
|
@ -42,10 +42,6 @@ class Projects::BranchesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def recent
|
||||
@branches = @repository.recent_branches
|
||||
end
|
||||
|
||||
def diverging_commit_counts
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
|
|
|
@ -18,7 +18,9 @@ module Ci
|
|||
return Ci::Pipeline.none
|
||||
end
|
||||
|
||||
items = pipelines.no_child
|
||||
items = pipelines
|
||||
items = items.no_child unless params[:iids].present?
|
||||
items = by_iids(items)
|
||||
items = by_scope(items)
|
||||
items = by_status(items)
|
||||
items = by_ref(items)
|
||||
|
@ -52,6 +54,14 @@ module Ci
|
|||
project.repository.tag_names
|
||||
end
|
||||
|
||||
def by_iids(items)
|
||||
if params[:iids].present?
|
||||
items.for_iid(params[:iids])
|
||||
else
|
||||
items
|
||||
end
|
||||
end
|
||||
|
||||
def by_scope(items)
|
||||
case params[:scope]
|
||||
when 'running'
|
||||
|
|
|
@ -12,7 +12,9 @@ module Resolvers
|
|||
|
||||
def resolve(iid:)
|
||||
BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args|
|
||||
args[:key].all_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) }
|
||||
finder = ::Ci::PipelinesFinder.new(project, context[:current_user], iids: iids)
|
||||
|
||||
finder.execute.each { |pipeline| loader.call(pipeline.iid.to_s, pipeline) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -85,9 +85,8 @@
|
|||
- if @todos.any?
|
||||
.js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
|
||||
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
|
||||
.card.card-without-border.card-without-margin
|
||||
%ul.content-list.todos-list
|
||||
= render @todos
|
||||
%ul.content-list.todos-list
|
||||
= render @todos
|
||||
= paginate @todos, theme: "gitlab"
|
||||
.js-nothing-here-container.todos-all-done.hidden.svg-content
|
||||
= image_tag 'illustrations/todos_all_done.svg'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
- if @issues.to_a.any?
|
||||
.card.card-without-border
|
||||
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
|
||||
= render partial: 'projects/issues/issue', collection: @issues
|
||||
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
|
||||
= render partial: 'projects/issues/issue', collection: @issues
|
||||
= paginate @issues, theme: "gitlab"
|
||||
- else
|
||||
= render 'shared/empty_states/issues'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
- if @merge_requests.to_a.any?
|
||||
.card.card-without-border
|
||||
%ul.content-list.mr-list.issuable-list
|
||||
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
|
||||
%ul.content-list.mr-list.issuable-list
|
||||
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
|
||||
|
||||
= paginate @merge_requests, theme: "gitlab"
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
.dropdown-title.gl-display-flex
|
||||
%span.gl-ml-auto
|
||||
= _("Select type")
|
||||
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ "aria-label" => _('Close') }
|
||||
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') }
|
||||
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
|
||||
.dropdown-content
|
||||
%ul
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Adds type="button" to the close button for the issue type selector to prevent
|
||||
accidental form submission
|
||||
merge_request: 48249
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove Bootstrap 4's Cards components from Issuables and Todos
|
||||
merge_request: 48004
|
||||
author: Takuya Noguchi
|
||||
type: performance
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
MESSAGE = <<~MESSAGE
|
||||
## Pipeline Changes
|
||||
|
||||
This Merge Request contains changes to the pipeline configuration for the GitLab project.
|
||||
|
||||
Please consider the effect of the changes in this Merge Request on the following:
|
||||
- Effects on different [pipeline types](https://docs.gitlab.com/ee/development/pipelines.html#pipelines-for-merge-requests)
|
||||
- Effects on non-canonical projects (`gitlab-foss`, `security`, etc)
|
||||
- Effects on [pipeline performance](https://about.gitlab.com/handbook/engineering/quality/performance-indicators/#average-merge-request-pipeline-duration-for-gitlab)
|
||||
- Effects on fork pipelines
|
||||
|
||||
Please consider communicating these changes to the broader team following the [communication guideline for pipeline changes](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity-team/#pipeline-changes)
|
||||
MESSAGE
|
||||
|
||||
if helper.has_ci_changes?
|
||||
markdown(MESSAGE)
|
||||
end
|
|
@ -439,9 +439,11 @@ parameter, indicating a starting or ending point of our pagination. They should
|
|||
followed with `first` or `last` parameter respectively to indicate _how many_ items
|
||||
we want to fetch after or before a given endpoint.
|
||||
|
||||
For example, here we're fetching 10 designs after a cursor:
|
||||
For example, here we're fetching 10 designs after a cursor (let us call this `projectQuery`):
|
||||
|
||||
```javascript
|
||||
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||
|
||||
query {
|
||||
project(fullPath: "root/my-project") {
|
||||
id
|
||||
|
@ -453,6 +455,9 @@ query {
|
|||
id
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -460,21 +465,31 @@ query {
|
|||
}
|
||||
```
|
||||
|
||||
Note that we are using the [`pageInfo.fragment.graphql`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/graphql_shared/fragments/pageInfo.fragment.graphql) to populate the `pageInfo` information.
|
||||
|
||||
#### Using `fetchMore` method in components
|
||||
|
||||
This approach makes sense to use with user-handled pagination (e.g. when the scrolls to fetch more data or explicitly clicks a "Next Page"-button).
|
||||
When we need to fetch all the data initially, it is recommended to use [a (non-smart) query, instead](#using-a-recursive-query-in-components).
|
||||
|
||||
When making an initial fetch, we usually want to start a pagination from the beginning.
|
||||
In this case, we can either:
|
||||
|
||||
- Skip passing a cursor.
|
||||
- Pass `null` explicitly to `after`.
|
||||
|
||||
After data is fetched, we should save a `pageInfo` object. Let's assume we're storing
|
||||
it to Vue component `data`:
|
||||
After data is fetched, we can use the `update`-hook as an opportunity [to customize
|
||||
the data that is set in the Vue component property](https://apollo.vuejs.org/api/smart-query.html#options), getting a hold of the `pageInfo` object among other data.
|
||||
|
||||
In the `result`-hook, we can inspect the `pageInfo` object to see if we need to fetch
|
||||
the next page. Note that we also keep a `requestCount` to ensure that the application
|
||||
does not keep requesting the next page, indefinitely:
|
||||
|
||||
```javascript
|
||||
data() {
|
||||
return {
|
||||
pageInfo: null,
|
||||
requestCount: 0,
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
|
@ -482,13 +497,29 @@ apollo: {
|
|||
query: projectQuery,
|
||||
variables() {
|
||||
return {
|
||||
// rest of design variables
|
||||
...
|
||||
// ... The rest of the design variables
|
||||
first: 10,
|
||||
};
|
||||
},
|
||||
result(res) {
|
||||
this.pageInfo = res.data?.project?.issue?.designCollection?.designs?.pageInfo;
|
||||
update(data) {
|
||||
const { id = null, issue = {} } = data.project || {};
|
||||
const { edges = [], pageInfo } = issue.designCollection?.designs || {};
|
||||
|
||||
return {
|
||||
id,
|
||||
edges,
|
||||
pageInfo,
|
||||
};
|
||||
},
|
||||
result() {
|
||||
const { pageInfo } = this.designs;
|
||||
|
||||
// Increment the request count with each new result
|
||||
this.requestCount += 1;
|
||||
// Only fetch next page if we have more requests and there is a next page to fetch
|
||||
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.hasNextPage) {
|
||||
this.fetchNextPage(pageInfo.endCursor);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -497,34 +528,102 @@ apollo: {
|
|||
When we want to move to the next page, we use an Apollo `fetchMore` method, passing a
|
||||
new cursor (and, optionally, new variables) there. In the `updateQuery` hook, we have
|
||||
to return a result we want to see in the Apollo cache after fetching the next page.
|
||||
[`Immer`s `produce`](#immutability-and-cache-updates)-function can help us with the immutability here:
|
||||
|
||||
```javascript
|
||||
fetchNextPage() {
|
||||
// as a first step, we're checking if we have more pages to move forward
|
||||
if (this.pageInfo?.hasNextPage) {
|
||||
this.$apollo.queries.designs.fetchMore({
|
||||
variables: {
|
||||
// rest of design variables
|
||||
...
|
||||
first: 10,
|
||||
after: this.pageInfo?.endCursor,
|
||||
},
|
||||
updateQuery(previousResult, { fetchMoreResult }) {
|
||||
// here we can implement the logic of adding new designs to fetched one (for example, if we use infinite scroll)
|
||||
// or replacing old result with the new one if we use numbered pages
|
||||
fetchNextPage(endCursor) {
|
||||
this.$apollo.queries.designs.fetchMore({
|
||||
variables: {
|
||||
// ... The rest of the design variables
|
||||
first: 10,
|
||||
after: endCursor,
|
||||
},
|
||||
updateQuery(previousResult, { fetchMoreResult }) {
|
||||
// Here we can implement the logic of adding new designs to existing ones
|
||||
// (for example, if we use infinite scroll) or replacing old result
|
||||
// with the new one if we use numbered pages
|
||||
|
||||
const newDesigns = fetchMoreResult.project.issue.designCollection.designs;
|
||||
previousResult.project.issue.designCollection.designs.push(...newDesigns)
|
||||
const { designs: previousDesigns } = previousResult.project.issue.designCollection;
|
||||
const { designs: newDesigns } = fetchMoreResult.project.issue.designCollection
|
||||
|
||||
return previousResult;
|
||||
},
|
||||
});
|
||||
}
|
||||
return produce(previousResult, draftData => {
|
||||
// `produce` gives us a working copy, `draftData`, that we can modify
|
||||
// as we please and from it will produce the next immutable result for us
|
||||
draftData.project.issue.designCollection.designs = [...previousDesigns, ...newDesigns];
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Please note we don't have to save `pageInfo` one more time; `fetchMore` triggers a query
|
||||
`result` hook as well.
|
||||
#### Using a recursive query in components
|
||||
|
||||
When it is necessary to fetch all paginated data initially an Apollo query can do the trick for us.
|
||||
If we need to fetch the next page based on user interactions, it is recommend to use a [`smartQuery`](https://apollo.vuejs.org/api/smart-query.html) along with the [`fetchMore`-hook](#using-fetchmore-method-in-components).
|
||||
|
||||
When the query resolves we can update the component data and inspect the `pageInfo` object
|
||||
to see if we need to fetch the next page, i.e. call the method recursively.
|
||||
|
||||
Note that we also keep a `requestCount` to ensure that the application does not keep
|
||||
requesting the next page, indefinitely.
|
||||
|
||||
```javascript
|
||||
data() {
|
||||
return {
|
||||
requestCount: 0,
|
||||
isLoading: false,
|
||||
designs: {
|
||||
edges: [],
|
||||
pageInfo: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchDesigns();
|
||||
},
|
||||
methods: {
|
||||
handleError(error) {
|
||||
this.isLoading = false;
|
||||
// Do something with `error`
|
||||
},
|
||||
fetchDesigns(endCursor) {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: projectQuery,
|
||||
variables() {
|
||||
return {
|
||||
// ... The rest of the design variables
|
||||
first: 10,
|
||||
endCursor,
|
||||
};
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const { id = null, issue = {} } = data.project || {};
|
||||
const { edges = [], pageInfo } = issue.designCollection?.designs || {};
|
||||
|
||||
// Update data
|
||||
this.designs = {
|
||||
id,
|
||||
edges: [...this.designs.edges, ...edges];
|
||||
pageInfo: pageInfo;
|
||||
};
|
||||
|
||||
// Increment the request count with each new result
|
||||
this.requestCount += 1;
|
||||
// Only fetch next page if we have more requests and there is a next page to fetch
|
||||
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.hasNextPage) {
|
||||
this.fetchDesigns(pageInfo.endCursor);
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
}
|
||||
})
|
||||
.catch(this.handleError);
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
### Managing performance
|
||||
|
||||
|
@ -666,34 +765,51 @@ it('calls mutation on submitting form ', () => {
|
|||
|
||||
To test the logic of Apollo cache updates, we might want to mock an Apollo Client in our unit tests. We use [`mock-apollo-client`](https://www.npmjs.com/package/mock-apollo-client) library to mock Apollo client and [`createMockApollo` helper](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/frontend/helpers/mock_apollo_helper.js) we created on top of it.
|
||||
|
||||
To separate tests with mocked client from 'usual' unit tests, it's recommended to create an additional component factory. This way we only create Apollo Client instance when it's necessary:
|
||||
To separate tests with mocked client from 'usual' unit tests, it's recommended to create an additional factory and pass the created `mockApollo` as an option to the `createComponent`-factory. This way we only create Apollo Client instance when it's necessary.
|
||||
|
||||
```javascript
|
||||
function createComponent() {...}
|
||||
|
||||
function createComponentWithApollo() {...}
|
||||
```
|
||||
|
||||
Then we need to inject `VueApollo` to Vue local instance (`localVue.use()` can also be called within `createComponentWithApollo()`)
|
||||
We need to inject `VueApollo` to the Vue local instance and, likewise, it is recommended to call `localVue.use()` within `createMockApolloProvider()` to only load it when it is necessary.
|
||||
|
||||
```javascript
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
function createMockApolloProvider() {
|
||||
localVue.use(VueApollo);
|
||||
|
||||
return createMockApollo(requestHandlers);
|
||||
}
|
||||
|
||||
function createComponent(options = {}) {
|
||||
const { mockApollo } = options;
|
||||
...
|
||||
return shallowMount(..., {
|
||||
localVue,
|
||||
apolloProvider: mockApollo,
|
||||
...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
After this, on the global `describe`, we should create a variable for `fakeApollo`:
|
||||
After this, you can control whether you need a variable for `mockApollo` and assign it in the appropriate `describe`-scope:
|
||||
|
||||
```javascript
|
||||
describe('Some component with Apollo mock', () => {
|
||||
describe('Some component', () => {
|
||||
let wrapper;
|
||||
let fakeApollo
|
||||
})
|
||||
|
||||
describe('with Apollo mock', () => {
|
||||
let mockApollo;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApollo = createMockApolloProvider();
|
||||
wrapper = createComponent({ mockApollo });
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Within component factory, we need to define an array of _handlers_ for every query or mutation:
|
||||
Within `createMockApolloProvider`-factory, we need to define an array of _handlers_ for every query or mutation:
|
||||
|
||||
```javascript
|
||||
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
|
||||
|
@ -702,13 +818,16 @@ import moveDesignMutation from '~/design_management/graphql/mutations/move_desig
|
|||
|
||||
describe('Some component with Apollo mock', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
let mockApollo;
|
||||
|
||||
function createMockApolloProvider() {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
function createComponentWithApollo() {
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
];
|
||||
...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
@ -718,23 +837,38 @@ After this, we need to create a mock Apollo Client instance using a helper:
|
|||
```javascript
|
||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||
|
||||
describe('Some component with Apollo mock', () => {
|
||||
describe('Some component', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
function createComponentWithApollo() {
|
||||
function createMockApolloProvider() {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
wrapper = shallowMount(Index, {
|
||||
return createMockApollo(requestHandlers);
|
||||
}
|
||||
|
||||
function createComponent(options = {}) {
|
||||
const { mockApollo } = options;
|
||||
|
||||
return shallowMount(Index, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
describe('with Apollo mock', () => {
|
||||
let mockApollo;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApollo = createMockApolloProvider();
|
||||
wrapper = createComponent({ mockApollo });
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
When mocking resolved values, ensure the structure of the response is the same
|
||||
|
@ -744,13 +878,15 @@ When testing queries, please keep in mind they are promises, so they need to be
|
|||
|
||||
```javascript
|
||||
it('renders a loading state', () => {
|
||||
createComponentWithApollo();
|
||||
const mockApollo = createMockApolloProvider();
|
||||
const wrapper = createComponent({ mockApollo });
|
||||
|
||||
expect(wrapper.find(LoadingSpinner).exists()).toBe(true)
|
||||
});
|
||||
|
||||
it('renders designs list', async () => {
|
||||
createComponentWithApollo();
|
||||
const mockApollo = createMockApolloProvider();
|
||||
const wrapper = createComponent({ mockApollo });
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
@ -762,7 +898,7 @@ it('renders designs list', async () => {
|
|||
If we need to test a query error, we need to mock a rejected value as request handler:
|
||||
|
||||
```javascript
|
||||
function createComponentWithApollo() {
|
||||
function createMockApolloProvider() {
|
||||
...
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockRejectedValue(new Error('GraphQL error')],
|
||||
|
@ -772,7 +908,7 @@ function createComponentWithApollo() {
|
|||
...
|
||||
|
||||
it('renders error if query fails', async () => {
|
||||
createComponent()
|
||||
const wrapper = createComponent();
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
@ -786,9 +922,11 @@ Request handlers can also be passed to component factory as a parameter.
|
|||
Mutations could be tested the same way with a few additional `nextTick`s to get the updated result:
|
||||
|
||||
```javascript
|
||||
function createComponentWithApollo({
|
||||
function createMockApolloProvider({
|
||||
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
|
||||
}) {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
moveDesignHandler = moveHandler;
|
||||
|
||||
const requestHandlers = [
|
||||
|
@ -797,15 +935,21 @@ function createComponentWithApollo({
|
|||
[moveDesignMutation, moveDesignHandler],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
wrapper = shallowMount(Index, {
|
||||
return createMockApollo(requestHandlers);
|
||||
}
|
||||
|
||||
function createComponent(options = {}) {
|
||||
const { mockApollo } = options;
|
||||
|
||||
return shallowMount(Index, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
}
|
||||
...
|
||||
it('calls a mutation with correct parameters and reorders designs', async () => {
|
||||
createComponentWithApollo({});
|
||||
const mockApollo = createMockApolloProvider({});
|
||||
const wrapper = createComponent({ mockApollo });
|
||||
|
||||
wrapper.find(VueDraggable).vm.$emit('change', {
|
||||
moved: {
|
||||
|
@ -833,7 +977,7 @@ If your application contains `@client` queries, most probably you will have an A
|
|||
```javascript
|
||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||
...
|
||||
mockApollo = createMockApollo(requestHandlers, resolvers);
|
||||
const mockApollo = createMockApollo(requestHandlers, resolvers);
|
||||
```
|
||||
|
||||
Sometimes we want to test a `result` hook of the local query. In order to have it triggered, we need to populate a cache with correct data to be fetched with this query:
|
||||
|
@ -850,12 +994,14 @@ query fetchLocalUser {
|
|||
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
|
||||
|
||||
function createMockApolloProvider() {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
];
|
||||
|
||||
mockApollo = createMockApollo(requestHandlers, {});
|
||||
const mockApollo = createMockApollo(requestHandlers, {});
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: fetchLocalUserQuery,
|
||||
data: {
|
||||
|
@ -885,9 +1031,10 @@ Sometimes it is necessary to control what the local resolver returns and inspect
|
|||
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
|
||||
|
||||
function createMockApolloProvider(options = {}) {
|
||||
Vue.use(VueApollo);
|
||||
const { fetchLocalUserSpy } = options;
|
||||
|
||||
mockApollo = createMockApollo([], {
|
||||
const mockApollo = createMockApollo([], {
|
||||
Query: {
|
||||
fetchLocalUser: fetchLocalUserSpy,
|
||||
},
|
||||
|
|
|
@ -485,17 +485,19 @@ request, be sure to start the `dont-interrupt-me` job before pushing.
|
|||
|
||||
1. All jobs must only pull caches by default.
|
||||
1. All jobs must be able to pass with an empty cache. In other words, caches are only there to speed up jobs.
|
||||
1. We currently have 6 different caches defined in
|
||||
1. We currently have several different caches defined in
|
||||
[`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml),
|
||||
with fixed keys:
|
||||
- `.rails-cache`.
|
||||
- `.static-analysis-cache`.
|
||||
- `.coverage-cache`
|
||||
- `.qa-cache`
|
||||
- `.yarn-cache`.
|
||||
- `.assets-compile-cache` (the key includes `${NODE_ENV}` so it's actually two different caches).
|
||||
1. Only 6 specific jobs, running in 2-hourly scheduled pipelines, are pushing (i.e. updating) to the caches:
|
||||
- `update-rails-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
|
||||
- `update-static-analysis-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
|
||||
- `update-coverage-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
|
||||
- `update-qa-cache`, defined in [`.gitlab/ci/qa.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/qa.gitlab-ci.yml).
|
||||
- `update-assets-compile-production-cache`, defined in [`.gitlab/ci/frontend.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/frontend.gitlab-ci.yml).
|
||||
- `update-assets-compile-test-cache`, defined in [`.gitlab/ci/frontend.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/frontend.gitlab-ci.yml).
|
||||
|
@ -519,7 +521,7 @@ variable:
|
|||
|
||||
```shell
|
||||
echo "Downloading archived master..."
|
||||
wget -O /tmp/gitlab.tar.gz https://storage.googleapis.com/gitlab-ci-git-repo-cache/project-278964/gitlab-master.tar.gz
|
||||
wget -O /tmp/gitlab.tar.gz https://storage.googleapis.com/gitlab-ci-git-repo-cache/project-278964/gitlab-master-shallow.tar.gz
|
||||
|
||||
if [ ! -f /tmp/gitlab.tar.gz ]; then
|
||||
echo "Repository cache not available, cloning a new directory..."
|
||||
|
|
|
@ -268,6 +268,10 @@ module Gitlab
|
|||
def has_database_scoped_labels?(current_mr_labels)
|
||||
current_mr_labels.any? { |label| label.start_with?('database::') }
|
||||
end
|
||||
|
||||
def has_ci_changes?
|
||||
changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,7 @@ class GitlabDanger
|
|||
product_analytics
|
||||
utility_css
|
||||
pajamas
|
||||
pipeline
|
||||
].freeze
|
||||
|
||||
CI_ONLY_RULES ||= %w[
|
||||
|
|
|
@ -45,7 +45,7 @@ module QA
|
|||
# Note: This test doesn't have the :orchestrated tag because it runs in the Test::Integration::Praefect
|
||||
# scenario with other tests that aren't considered orchestrated.
|
||||
# It also runs on staging using nfs-file07 as non-cluster storage and nfs-file22 as cluster/praefect storage
|
||||
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974' do
|
||||
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/284645', type: :investigating } do
|
||||
let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } }
|
||||
let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } }
|
||||
let(:project) do
|
||||
|
|
|
@ -72,7 +72,7 @@ RSpec.describe Ci::PipelinesFinder do
|
|||
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
|
||||
end
|
||||
|
||||
it 'filters out child pipelines and show only the parents' do
|
||||
it 'filters out child pipelines and shows only the parents by default' do
|
||||
is_expected.to eq([parent_pipeline])
|
||||
end
|
||||
end
|
||||
|
@ -195,6 +195,21 @@ RSpec.describe Ci::PipelinesFinder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when iids filter is specified' do
|
||||
let(:params) { { iids: [pipeline1.iid, pipeline3.iid] } }
|
||||
let!(:pipeline1) { create(:ci_pipeline, project: project) }
|
||||
let!(:pipeline2) { create(:ci_pipeline, project: project) }
|
||||
let!(:pipeline3) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
|
||||
|
||||
it 'returns matches pipelines' do
|
||||
is_expected.to match_array([pipeline1, pipeline3])
|
||||
end
|
||||
|
||||
it 'does not fitler out child pipelines' do
|
||||
is_expected.to include(pipeline3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sha is specified' do
|
||||
let!(:pipeline) { create(:ci_pipeline, project: project, sha: '97de212e80737a608d939f648d959671fb0a0142') }
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import Draggable from 'vuedraggable';
|
||||
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
|
||||
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
|
||||
import getters from 'ee_else_ce/boards/stores/getters';
|
||||
|
@ -10,6 +11,11 @@ import BoardContent from '~/boards/components/board_content.vue';
|
|||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const actions = {
|
||||
moveList: jest.fn(),
|
||||
showPromotionList: jest.fn(),
|
||||
};
|
||||
|
||||
describe('BoardContent', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -21,12 +27,13 @@ describe('BoardContent', () => {
|
|||
|
||||
const createStore = (state = defaultState) => {
|
||||
return new Vuex.Store({
|
||||
actions,
|
||||
getters,
|
||||
state,
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = state => {
|
||||
const createComponent = ({ state, props = {}, graphqlBoardListsEnabled = false } = {}) => {
|
||||
const store = createStore({
|
||||
...defaultState,
|
||||
...state,
|
||||
|
@ -37,25 +44,61 @@ describe('BoardContent', () => {
|
|||
lists: mockListsWithModel,
|
||||
canAdminList: true,
|
||||
disabled: false,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled },
|
||||
},
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders a BoardColumn component per list', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
|
||||
});
|
||||
|
||||
it('does not display EpicsSwimlanes component', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('graphqlBoardLists feature flag enabled', () => {
|
||||
describe('can admin list', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } });
|
||||
});
|
||||
|
||||
it('renders draggable component', () => {
|
||||
expect(wrapper.find(Draggable).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can not admin list', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } });
|
||||
});
|
||||
|
||||
it('renders draggable component', () => {
|
||||
expect(wrapper.find(Draggable).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('graphqlBoardLists feature flag disabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ graphqlBoardListsEnabled: false });
|
||||
});
|
||||
|
||||
it('does not render draggable component', () => {
|
||||
expect(wrapper.find(Draggable).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -290,6 +290,33 @@ describe('moveList', () => {
|
|||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not commit MOVE_LIST or dispatch updateList if listId and replacedListId are the same', () => {
|
||||
const initialBoardListsState = {
|
||||
'gid://gitlab/List/1': mockListsWithModel[0],
|
||||
'gid://gitlab/List/2': mockListsWithModel[1],
|
||||
};
|
||||
|
||||
const state = {
|
||||
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
|
||||
boardType: 'group',
|
||||
disabled: false,
|
||||
boardLists: initialBoardListsState,
|
||||
};
|
||||
|
||||
testAction(
|
||||
actions.moveList,
|
||||
{
|
||||
listId: 'gid://gitlab/List/1',
|
||||
replacedListId: 'gid://gitlab/List/1',
|
||||
newIndex: 1,
|
||||
adjustmentValue: 1,
|
||||
},
|
||||
state,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateList', () => {
|
||||
|
|
|
@ -18,6 +18,10 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
|||
resolve(described_class, obj: project, args: args, ctx: { current_user: current_user })
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
end
|
||||
|
||||
it 'resolves pipeline for the passed iid' do
|
||||
result = batch_sync do
|
||||
resolve_pipeline(project, { iid: '1234' })
|
||||
|
@ -26,6 +30,21 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
|||
expect(result).to eq(pipeline)
|
||||
end
|
||||
|
||||
it 'keeps the queries under the threshold' do
|
||||
create(:ci_pipeline, project: project, iid: '1235')
|
||||
|
||||
control = ActiveRecord::QueryRecorder.new do
|
||||
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
|
||||
end
|
||||
|
||||
expect do
|
||||
batch_sync do
|
||||
resolve_pipeline(project, { iid: '1234' })
|
||||
resolve_pipeline(project, { iid: '1235' })
|
||||
end
|
||||
end.not_to exceed_query_limit(control)
|
||||
end
|
||||
|
||||
it 'does not resolve a pipeline outside the project' do
|
||||
result = batch_sync do
|
||||
resolve_pipeline(other_pipeline.project, { iid: '1234' })
|
||||
|
|
|
@ -591,4 +591,30 @@ RSpec.describe Gitlab::Danger::Helper do
|
|||
expect(helper.prepare_labels_for_mr([])).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_ci_changes?' do
|
||||
context 'when .gitlab/ci is changed' do
|
||||
it 'returns true' do
|
||||
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab/ci/test.yml])
|
||||
|
||||
expect(helper.has_ci_changes?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when .gitlab-ci.yml is changed' do
|
||||
it 'returns true' do
|
||||
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab-ci.yml])
|
||||
|
||||
expect(helper.has_ci_changes?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when neither .gitlab/ci/ or .gitlab-ci.yml is changed' do
|
||||
it 'returns false' do
|
||||
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb nested/.gitlab-ci.yml])
|
||||
|
||||
expect(helper.has_ci_changes?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe GitlabDanger do
|
||||
let(:gitlab_danger_helper) { nil }
|
||||
|
@ -9,7 +9,7 @@ RSpec.describe GitlabDanger do
|
|||
|
||||
describe '.local_warning_message' do
|
||||
it 'returns an informational message with rules that can run' do
|
||||
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, product_analytics, utility_css, pajamas')
|
||||
expect(described_class.local_warning_message).to eq("==> Only the following Danger rules can be run locally: #{described_class::LOCAL_RULES.join(', ')}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,12 +5,12 @@ require 'spec_helper'
|
|||
RSpec.describe 'getting pipeline information nested in a project' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:project) { create(:project, :repository, :public) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:current_user) { create(:user) }
|
||||
let!(:project) { create(:project, :repository, :public) }
|
||||
let!(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:current_user) { create(:user) }
|
||||
let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] }
|
||||
|
||||
let(:query) do
|
||||
let!(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
|
@ -35,4 +35,45 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
|
||||
expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE')
|
||||
end
|
||||
|
||||
context 'batching' do
|
||||
let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
|
||||
let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
|
||||
let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
|
||||
|
||||
it 'executes the finder once' do
|
||||
mock = double(Ci::PipelinesFinder)
|
||||
opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
|
||||
|
||||
expect(Ci::PipelinesFinder).to receive(:new).once.with(project, current_user, opts).and_return(mock)
|
||||
expect(mock).to receive(:execute).once.and_return(Ci::Pipeline.none)
|
||||
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
it 'keeps the queries under the threshold' do
|
||||
control = ActiveRecord::QueryRecorder.new do
|
||||
single_pipeline_query = build_query_to_find_pipeline_shas(pipeline)
|
||||
|
||||
post_graphql(single_pipeline_query, current_user: current_user)
|
||||
end
|
||||
|
||||
aggregate_failures do
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end.not_to exceed_query_limit(control)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_query_to_find_pipeline_shas(*pipelines)
|
||||
pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
|
||||
"pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
|
||||
end.join(' ')
|
||||
|
||||
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,13 +50,13 @@ RSpec.describe Tooling::TestMapGenerator do
|
|||
let(:expected_mapping) do
|
||||
{
|
||||
'lib/gitlab/current_settings.rb' => [
|
||||
'./spec/factories_spec.rb'
|
||||
'spec/factories_spec.rb'
|
||||
],
|
||||
'lib/feature.rb' => [
|
||||
'./spec/factories_spec.rb'
|
||||
'spec/factories_spec.rb'
|
||||
],
|
||||
'lib/gitlab/marginalia.rb' => [
|
||||
'./spec/factories_spec.rb'
|
||||
'spec/factories_spec.rb'
|
||||
]
|
||||
}
|
||||
end
|
||||
|
@ -80,16 +80,16 @@ RSpec.describe Tooling::TestMapGenerator do
|
|||
let(:expected_mapping) do
|
||||
{
|
||||
'lib/gitlab/current_settings.rb' => [
|
||||
'./spec/factories_spec.rb',
|
||||
'./spec/models/project_spec.rb'
|
||||
'spec/factories_spec.rb',
|
||||
'spec/models/project_spec.rb'
|
||||
],
|
||||
'lib/feature.rb' => [
|
||||
'./spec/factories_spec.rb',
|
||||
'./spec/models/project_spec.rb'
|
||||
'spec/factories_spec.rb',
|
||||
'spec/models/project_spec.rb'
|
||||
],
|
||||
'lib/gitlab/marginalia.rb' => [
|
||||
'./spec/factories_spec.rb',
|
||||
'./spec/models/project_spec.rb'
|
||||
'spec/factories_spec.rb',
|
||||
'spec/models/project_spec.rb'
|
||||
]
|
||||
}
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ module Tooling
|
|||
example_groups.each do |example_id, files|
|
||||
files.each do |file|
|
||||
spec_file = strip_example_uid(example_id)
|
||||
@mapping[file] << spec_file
|
||||
@mapping[file] << spec_file.delete_prefix('./')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue