Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-24 21:11:16 +00:00
parent 22dc7bdafc
commit 1631d8a2e0
45 changed files with 386 additions and 324 deletions

View file

@ -298,7 +298,6 @@
rules:
- <<: *if-not-canonical-namespace
when: never
- <<: *if-master-refs
- changes: *ci-build-images-patterns
- changes: *code-qa-patterns

View file

@ -26,6 +26,9 @@ export default {
toggleFileByFile() {
this.setFileByFile({ fileByFile: !this.viewDiffsFileByFile });
},
toggleWhitespace(updatedSetting) {
this.setShowWhitespace({ showWhitespace: updatedSetting, pushState: true });
},
},
};
</script>
@ -80,26 +83,21 @@ export default {
</gl-button>
</gl-button-group>
</div>
<div class="gl-mt-3 gl-px-3">
<label class="gl-mb-0">
<input
id="show-whitespace"
type="checkbox"
:checked="showWhitespace"
@change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
/>
{{ __('Show whitespace changes') }}
</label>
</div>
<div class="gl-mt-3 gl-px-3">
<gl-form-checkbox
data-testid="file-by-file"
class="gl-mb-0"
:checked="viewDiffsFileByFile"
@input="toggleFileByFile"
>
{{ $options.i18n.fileByFile }}
</gl-form-checkbox>
</div>
<gl-form-checkbox
data-testid="show-whitespace"
class="gl-mt-3 gl-pl-3"
:checked="showWhitespace"
@input="toggleWhitespace"
>
{{ $options.i18n.whitespace }}
</gl-form-checkbox>
<gl-form-checkbox
data-testid="file-by-file"
class="gl-pl-3 gl-mb-0"
:checked="viewDiffsFileByFile"
@input="toggleFileByFile"
>
{{ $options.i18n.fileByFile }}
</gl-form-checkbox>
</gl-dropdown>
</template>

View file

@ -21,5 +21,6 @@ export const DIFF_FILE = {
};
export const SETTINGS_DROPDOWN = {
whitespace: __('Show whitespace changes'),
fileByFile: __('Show one file at a time'),
};

View file

@ -6,6 +6,9 @@ import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import { __ } from './locale';
// TODO: Update all references of "issuable_vue_app:change" https://gitlab.com/gitlab-org/gitlab/-/issues/322760
export const EVENT_ISSUABLE_VUE_APP_CHANGE = 'issuable_vue_app:change';
export default class Issue {
constructor() {
if ($('.js-alert-moved-from-service-desk-warning').length) {
@ -23,9 +26,13 @@ export default class Issue {
}
// Listen to state changes in the Vue app
document.addEventListener('issuable_vue_app:change', (event) => {
this.issuableVueAppChangeHandler = (event) =>
this.updateTopState(event.detail.isClosed, event.detail.data);
});
document.addEventListener(EVENT_ISSUABLE_VUE_APP_CHANGE, this.issuableVueAppChangeHandler);
}
dispose() {
document.removeEventListener(EVENT_ISSUABLE_VUE_APP_CHANGE, this.issuableVueAppChangeHandler);
}
/**

View file

@ -1,6 +1,9 @@
import Diff from '~/diff';
import GpgBadges from '~/gpg_badges';
import initChangesDropdown from '~/init_changes_dropdown';
import initCompareSelector from '~/projects/compare';
initCompareSelector();
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new

View file

@ -82,6 +82,7 @@ export default {
:loading="isLoading"
data-testid="pipelines-manual-actions-dropdown"
right
lazy
icon="play"
>
<gl-dropdown-item

View file

@ -31,6 +31,8 @@ export default {
:text="$options.translations.artifacts"
:aria-label="$options.translations.artifacts"
icon="download"
right
lazy
text-sr-only
>
<gl-dropdown-item

View file

@ -69,7 +69,7 @@ export default {
this.loading = true;
if (reset) {
this.selectedRevision = emptyDropdownText;
this.selectedRevision = this.getDefaultBranch();
}
return axios

View file

@ -223,6 +223,7 @@ export default {
type="text"
class="js-add-issuable-form-input add-issuable-form-input"
data-qa-selector="add_issue_field"
autocomplete="off"
@input="onInput"
@focus="onFocus"
@blur="onBlur"

View file

@ -21,11 +21,14 @@ export default {
'allowLabelRemove',
'allowScopedLabels',
'labelsFilterBasePath',
'labelsFilterParam',
]),
},
methods: {
labelFilterUrl(label) {
return `${this.labelsFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
return `${this.labelsFilterBasePath}?${this.labelsFilterParam}[]=${encodeURIComponent(
label.title,
)}`;
},
scopedLabel(label) {
return this.allowScopedLabels && isScopedLabel(label);

View file

@ -81,6 +81,11 @@ export default {
required: false,
default: '',
},
labelsFilterParam: {
type: String,
required: false,
default: 'label_name',
},
dropdownButtonText: {
type: String,
required: false,
@ -156,6 +161,7 @@ export default {
labelsFetchPath: this.labelsFetchPath,
labelsManagePath: this.labelsManagePath,
labelsFilterBasePath: this.labelsFilterBasePath,
labelsFilterParam: this.labelsFilterParam,
labelsListTitle: this.labelsListTitle,
labelsCreateTitle: this.labelsCreateTitle,
footerCreateLabelTitle: this.footerCreateLabelTitle,

View file

@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController
before_action :validate_refs!
before_action do
push_frontend_feature_flag(:compare_repo_dropdown)
push_frontend_feature_flag(:compare_repo_dropdown, source_project, default_enabled: :yaml)
end
feature_category :source_code_management

View file

@ -28,4 +28,20 @@ module CompareHelper
.new(current_user: current_user, source_project: source_project, project_feature: :repository)
.execute(include_routes: true)
end
def project_compare_selector_data(project, merge_request, params)
{
project_compare_index_path: project_compare_index_path(project),
refs_project_path: refs_project_path(project),
params_from: params[:from],
params_to: params[:to],
project_merge_request_path: merge_request.present? ? project_merge_request_path(project, merge_request) : '',
create_mr_path: create_mr_button? ? create_mr_path : ''
}.tap do |data|
if Feature.enabled?(:compare_repo_dropdown, project, default_enabled: :yaml)
data[:project_to] = { id: project.id, name: project.full_path }.to_json
data[:projects_from] = target_projects(project).map { |project| { id: project.id, name: project.full_path } }.to_json
end
end
end
end

View file

@ -1,28 +0,0 @@
= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-prepend
.input-group-text
= s_("CompareBranches|Source")
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag")
= sprite_icon('chevron-down', css_class: 'float-right')
= render 'shared/ref_dropdown'
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-prepend
.input-group-text
= s_("CompareBranches|Target")
= hidden_field_tag :from, params[:from]
= button_tag type: 'button', title: params[:from], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag")
= sprite_icon('chevron-down', css_class: 'float-right')
= render 'shared/ref_dropdown'
&nbsp;
= button_tag s_("CompareBranches|Compare"), class: "btn gl-button btn-success commits-compare-btn"
- if @merge_request.present?
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'gl-ml-3 btn'
- elsif create_mr_button?
= link_to _("Create merge request"), create_mr_path, class: 'gl-ml-3 btn gl-button'

View file

@ -13,17 +13,4 @@
= html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
.prepend-top-20
- if Feature.enabled?(:compare_repo_dropdown)
#js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project),
refs_project_path: refs_project_path(@project),
params_from: params[:from], params_to: params[:to],
project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '',
create_mr_path: create_mr_button? ? create_mr_path : '',
project_to: { id: @project.id, name: @project.full_path }.to_json,
projects_from: target_projects(@project).map { |project| { id:project.id, name: project.full_path } }.to_json } }
- else
#js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project),
refs_project_path: refs_project_path(@project),
params_from: params[:from], params_to: params[:to],
project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '',
create_mr_path: create_mr_button? ? create_mr_path : '' } }
#js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) }

View file

@ -2,7 +2,8 @@
- page_title "#{params[:from]}...#{params[:to]}"
.sub-header-block.no-bottom-space
= render "form"
.js-signature-container{ data: { 'signatures-path' => signatures_namespace_project_compare_index_path } }
#js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) }
- if @commits.present?
= render "projects/commits/commit_list"

View file

@ -0,0 +1,5 @@
---
title: Restyle the repository compare show page
merge_request: 53523
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Reduce elements in Pipeline page dropdowns with lazy
merge_request: 54674
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Improve Linked Issues Usability
merge_request: 50879
author: Andrew Minion
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add branch_name to dast_profiles table
merge_request: 54891
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Fix bold text mismatch in MR ⚙ menu
merge_request: 54531
author:
type: fixed

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class AddBranchNameToDastProfile < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
add_column :dast_profiles, :branch_name, :text
end
add_text_limit :dast_profiles, :branch_name, 255
end
def down
with_lock_retries do
remove_column :dast_profiles, :branch_name
end
end
end

View file

@ -0,0 +1 @@
1266bf92f23a42d96778bf546534882f03d2388f22640e4cfaa2a9a1aad19093

View file

@ -11634,7 +11634,9 @@ CREATE TABLE dast_profiles (
updated_at timestamp with time zone NOT NULL,
name text NOT NULL,
description text NOT NULL,
branch_name text,
CONSTRAINT check_5fcf73bf61 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_6c9d775949 CHECK ((char_length(branch_name) <= 255)),
CONSTRAINT check_c34e505c24 CHECK ((char_length(description) <= 255))
);

View file

@ -287,6 +287,7 @@ See [database guidelines](database/index.md).
## Domain-specific guides
- [CI/CD development documentation](cicd/index.md)
- [AppSec development documentation](appsec/index.md)
## Other Development guides

View file

@ -0,0 +1,32 @@
---
stage: Secure, Protect
group: all
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/#assignments
type: index, dev, reference
---
# Application Security development documentation
Development guides that are specific to the stages that work on Application Security features are listed here.
Please go to [Application Security](../../user/application_security/index.md) if you are looking for documentation on how to use those features.
## Namespaces
Application Security code in the Rails monolith is organized into the following namespaces, which generally follows
the feature categories in the [Secure](https://about.gitlab.com/stages-devops-lifecycle/secure/) and [Protect](https://about.gitlab.com/stages-devops-lifecycle/protect/) stages.
- `AppSec`: shared code.
- `AppSec::ContainerScanning`: Container Scanning code.
- `AppSec::Dast`: DAST code.
- `AppSec::DependencyScanning`: Dependency Scanning code.
- `AppSec::Fuzzing::Api`: API Fuzzing code.
- `AppSec::Fuzzing::Coverage`: Coverage Fuzzing code.
- `AppSec::Fuzzing`: Shared fuzzing code.
- `AppSec::LicenseCompliance`: License Compliance code.
- `AppSec::Sast`: SAST code.
- `AppSec::SecretDetection`: Secret Detection code.
- `AppSec::VulnMgmt`: Vulnerability Management code.
Most AppSec code does not conform to these namespace guidelines. When developing, make an effort
to move existing code into the appropriate namespace whenever possible.

View file

@ -69,3 +69,4 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Miscellaneous
- [Maintenance operations](maintenance_operations.md)
- [Update multiple database objects](setting_multiple_values.md)

View file

@ -4,24 +4,22 @@ group: Database
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/#assignments
---
# Setting Multiple Values
# Update multiple database objects
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32921) in GitLab 13.5.
There's often a need to update multiple objects with new values for one
or more columns. One method of doing this is using `Relation#update_all`:
You can update multiple database objects with new values for one or more columns.
One method is to use `Relation#update_all`:
```ruby
user.issues.open.update_all(due_date: 7.days.from_now) # (1)
user.issues.update_all('relative_position = relative_position + 1') # (2)
```
But what do you do if you cannot express the update as either a static value (1)
or as a calculation (2)?
Thankfully we can use `UPDATE FROM` to express the need to update multiple rows
with distinct values in a single query. One can either use a temporary table, or
a Common Table Expression (CTE), and then use that as the source of the updates:
If you cannot express the update as either a static value (1) or as a calculation (2),
use `UPDATE FROM` to express the need to update multiple rows with distinct values
in a single query. Create a temporary table, or a Common Table Expression (CTE),
and use it as the source of the updates:
```sql
with updates(obj_id, new_title, new_weight) as (
@ -34,23 +32,22 @@ update issues
where id = obj_id
```
The bad news: there is no way to express this in ActiveRecord or even dropping
down to ARel. The `UpdateManager` does not support `update from`, so this
is not expressible.
The good news: we supply an abstraction to help you generate these kinds of
updates, called `Gitlab::Database::BulkUpdate`. This constructs queries such as the
above, and uses binding parameters to avoid SQL injection.
You can't express this in ActiveRecord, or by dropping down to [Arel](https://api.rubyonrails.org/v6.1.0/classes/Arel.html),
because the `UpdateManager` does not support `update from`. However, we supply
an abstraction to help you generate these kinds of updates: `Gitlab::Database::BulkUpdate`.
This abstraction constructs queries like the previous example, and uses
binding parameters to avoid SQL injection.
## Usage
To use this, we need:
To use `Gitlab::Database::BulkUpdate`, we need:
- the list of columns to update
- a mapping from object/ID to the new values to set for that object
- a way to determine the table for each object
- The list of columns to update.
- A mapping from the object (or ID) to the new values to set for that object.
- A way to determine the table for each object.
For example, we can express the query above as:
For example, we can express the example query in a way that determines the
table by calling `object.class.table_name`:
```ruby
issue_a = Issue.find(..)
@ -63,10 +60,7 @@ issue_b = Issue.find(..)
})
```
Here the table can be determined automatically, from calling
`object.class.table_name`, so we don't need to provide anything.
We can even pass heterogeneous sets of objects, if the updates all make sense
You can even pass heterogeneous sets of objects, if the updates all make sense
for them:
```ruby
@ -82,8 +76,8 @@ merge_request = MergeRequest.find(..)
})
```
If your objects do not return the correct model class (perhaps because they are
part of a union), then we need to specify this explicitly in a block:
If your objects do not return the correct model class, such as if they are part
of a union, then specify the model class explicitly in a block:
```ruby
bazzes = params
@ -103,7 +97,10 @@ end
## Caveats
Note that this is a **very low level** tool, and operates on the raw column
values. Enumerations and state fields must be translated into their underlying
representations, for example, and nested associations are not supported. No
validations or hooks are called.
This tool is **very low level**, and operates directly on the raw column
values. You should consider these issues if you implement it:
- Enumerations and state fields must be translated into their underlying
representations.
- Nested associations are not supported.
- No validations or hooks are called.

View file

@ -440,12 +440,11 @@ components, we need to include the store and provide the correct state:
//component_spec.js
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { createStore } from './store';
import Component from './component.vue'
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('component', () => {
let store;
@ -455,7 +454,6 @@ describe('component', () => {
store = createStore();
wrapper = mount(Component, {
localVue,
store,
});
};
@ -483,6 +481,11 @@ describe('component', () => {
});
```
Some test files may still use the
[deprecated `createLocalVue` function](https://gitlab.com/gitlab-org/gitlab/-/issues/220482)
from `@vue/test-utils` and `localVue.use(Vuex)`. This is unnecessary, and should be
avoided or removed when possible.
### Two way data binding
When storing form data in Vuex, it is sometimes necessary to update the value stored. The store

View file

@ -605,9 +605,10 @@ When adding a foreign-key constraint to an existing column in a non-empty table,
we have to employ `add_concurrent_foreign_key` and `add_concurrent_index`
instead of `add_reference`.
For an empty table (such as a fresh one), it is recommended to use
`add_reference` in a single-transaction migration, combining it with other
operations that don't require `disable_ddl_transaction!`.
If you have a new or empty table that doesn't reference a
[high-traffic table](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/rubocop-migrations.yml#L3),
we recommend that you use `add_reference` in a single-transaction migration. You can
combine it with other operations that don't require `disable_ddl_transaction!`.
You can read more about adding [foreign key constraints to an existing column](database/add_foreign_key_to_existing_column.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -187,27 +187,31 @@ Feature.disable(:local_file_reviews)
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/13950) in GitLab 11.5.
In a merge request, you can leave comments in any part of the file being changed.
In the Merge Request Diff UI, click the **{comment}** **comment** icon in the gutter
to expand the diff lines and leave a comment, just as you would for a changed line.
In the Merge Request Diff UI, you can:
![Comment on any diff file line](img/comment-on-any-diff-line.png)
- **Comment on a single line**: Click the **{comment}** **comment** icon in the
gutter to expand the diff lines and display a comment box.
- [**Comment on multiple lines**](#commenting-on-multiple-lines).
### Commenting on multiple lines
> - [Introduced](https://gitlab.com/gitlab-org/ux-research/-/issues/870) in GitLab 13.2.
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49875) click-and-drag features in GitLab 13.8.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/299121) in GitLab 13.9.
GitLab provides a way to select which lines of code a comment refers to. After starting a comment
a dropdown selector is shown to select the first line that this comment refers to.
The last line is the line that the comment icon was initially clicked on.
When commenting on a diff, you can select which lines of code your comment refers
to by either:
New comments default to single line comments by having the first and last lines
the same. Selecting a different starting line turns this into a multiline comment.
![Comment on any diff file line](img/comment-on-any-diff-line_v13_10.png)
![Multiline comment selection highlighted](img/multiline-comment-highlighted.png)
- Clicking and dragging the **{comment}** **comment** icon in the gutter to highlight
lines in the diff. GitLab expands the diff lines and displays a comment box.
- After starting a comment by clicking the **{comment}** **comment** icon in the
gutter, select the first line number your comment refers to in the **Commenting on lines**
select box. New comments default to single-line comments, unless you select
a different starting line.
Once a multiline comment is saved the lines of code pertaining to that comment are listed directly
above it.
Multiline comments display the comment's line numbers above the body of the comment:
![Multiline comment selection displayed above comment](img/multiline-comment-saved.png)

View file

@ -7559,15 +7559,6 @@ msgstr ""
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr ""
msgid "CompareBranches|Compare"
msgstr ""
msgid "CompareBranches|Source"
msgstr ""
msgid "CompareBranches|Target"
msgstr ""
msgid "CompareBranches|There isn't anything to compare."
msgstr ""
@ -26722,9 +26713,6 @@ msgstr ""
msgid "Select branch"
msgstr ""
msgid "Select branch/tag"
msgstr ""
msgid "Select due date"
msgstr ""

View file

@ -168,7 +168,7 @@
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
"@gitlab/eslint-plugin": "8.1.0",
"@gitlab/stylelint-config": "^2.2.0",
"@gitlab/stylelint-config": "2.2.0",
"@testing-library/dom": "^7.16.2",
"@vue/test-utils": "1.1.2",
"acorn": "^6.3.0",

View file

@ -21,13 +21,13 @@ RSpec.describe 'Merge request > User toggles whitespace changes', :js do
describe 'clicking "Hide whitespace changes" button' do
it 'toggles the "Hide whitespace changes" button' do
find('#show-whitespace').click
find('[data-testid="show-whitespace"]').click
visit diffs_project_merge_request_path(project, merge_request)
find('.js-show-diff-settings').click
expect(find('#show-whitespace')).not_to be_checked
expect(find('[data-testid="show-whitespace"]')).not_to be_checked
end
end
end

View file

@ -24,7 +24,7 @@ RSpec.describe 'Merge Request button' do
project.add_developer(user)
end
it 'shows Create merge request button' do
it 'shows Create merge request button', :js do
href = project_new_merge_request_path(
project,
merge_request: {
@ -83,7 +83,7 @@ RSpec.describe 'Merge Request button' do
end
context 'on own fork of project' do
it 'shows Create merge request button' do
it 'shows Create merge request button', :js do
href = project_new_merge_request_path(
forked_project,
merge_request: {
@ -120,7 +120,7 @@ RSpec.describe 'Merge Request button' do
let(:fork_url) { project_compare_path(forked_project, from: 'master', to: 'feature') }
end
it 'shows the correct merge request button when viewing across forks' do
it 'shows the correct merge request button when viewing across forks', :js do
sign_in(user)
project.add_developer(user)

View file

@ -1,79 +1,66 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SettingsDropdown from '~/diffs/components/settings_dropdown.vue';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
import diffModule from '~/diffs/store/modules';
const localVue = createLocalVue();
localVue.use(Vuex);
import createDiffsStore from '../create_diffs_store';
describe('Diff settings dropdown component', () => {
let wrapper;
let vm;
let actions;
let store;
function createComponent(extendStore = () => {}) {
const store = new Vuex.Store({
modules: {
diffs: {
namespaced: true,
actions,
state: diffModule().state,
getters: diffModule().getters,
},
},
});
store = createDiffsStore();
extendStore(store);
wrapper = mount(SettingsDropdown, {
localVue,
store,
});
wrapper = extendedWrapper(
mount(SettingsDropdown, {
store,
}),
);
vm = wrapper.vm;
}
function getFileByFileCheckbox(vueWrapper) {
return vueWrapper.find('[data-testid="file-by-file"]');
return vueWrapper.findByTestId('file-by-file');
}
function setup({ storeUpdater } = {}) {
createComponent(storeUpdater);
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
}
beforeEach(() => {
actions = {
setInlineDiffViewType: jest.fn(),
setParallelDiffViewType: jest.fn(),
setRenderTreeList: jest.fn(),
setShowWhitespace: jest.fn(),
setFileByFile: jest.fn(),
};
setup();
});
afterEach(() => {
store.dispatch.mockRestore();
wrapper.destroy();
});
describe('tree view buttons', () => {
it('list view button dispatches setRenderTreeList with false', () => {
createComponent();
wrapper.find('.js-list-view').trigger('click');
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false);
expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', false);
});
it('tree view button dispatches setRenderTreeList with true', () => {
createComponent();
wrapper.find('.js-tree-view').trigger('click');
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true);
expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', true);
});
it('sets list button as selected when renderTreeList is false', () => {
createComponent((store) => {
Object.assign(store.state.diffs, {
renderTreeList: false,
});
setup({
storeUpdater: (origStore) =>
Object.assign(origStore.state.diffs, { renderTreeList: false }),
});
expect(wrapper.find('.js-list-view').classes('selected')).toBe(true);
@ -81,10 +68,8 @@ describe('Diff settings dropdown component', () => {
});
it('sets tree button as selected when renderTreeList is true', () => {
createComponent((store) => {
Object.assign(store.state.diffs, {
renderTreeList: true,
});
setup({
storeUpdater: (origStore) => Object.assign(origStore.state.diffs, { renderTreeList: true }),
});
expect(wrapper.find('.js-list-view').classes('selected')).toBe(false);
@ -94,10 +79,9 @@ describe('Diff settings dropdown component', () => {
describe('compare changes', () => {
it('sets inline button as selected', () => {
createComponent((store) => {
Object.assign(store.state.diffs, {
diffViewType: INLINE_DIFF_VIEW_TYPE,
});
setup({
storeUpdater: (origStore) =>
Object.assign(origStore.state.diffs, { diffViewType: INLINE_DIFF_VIEW_TYPE }),
});
expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(true);
@ -105,10 +89,9 @@ describe('Diff settings dropdown component', () => {
});
it('sets parallel button as selected', () => {
createComponent((store) => {
Object.assign(store.state.diffs, {
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
});
setup({
storeUpdater: (origStore) =>
Object.assign(origStore.state.diffs, { diffViewType: PARALLEL_DIFF_VIEW_TYPE }),
});
expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(false);
@ -116,53 +99,49 @@ describe('Diff settings dropdown component', () => {
});
it('calls setInlineDiffViewType when clicking inline button', () => {
createComponent();
wrapper.find('.js-inline-diff-button').trigger('click');
expect(actions.setInlineDiffViewType).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith('diffs/setInlineDiffViewType', expect.anything());
});
it('calls setParallelDiffViewType when clicking parallel button', () => {
createComponent();
wrapper.find('.js-parallel-diff-button').trigger('click');
expect(actions.setParallelDiffViewType).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(
'diffs/setParallelDiffViewType',
expect.anything(),
);
});
});
describe('whitespace toggle', () => {
it('does not set as checked when showWhitespace is false', () => {
createComponent((store) => {
Object.assign(store.state.diffs, {
showWhitespace: false,
});
setup({
storeUpdater: (origStore) =>
Object.assign(origStore.state.diffs, { showWhitespace: false }),
});
expect(wrapper.find('#show-whitespace').element.checked).toBe(false);
expect(wrapper.findByTestId('show-whitespace').element.checked).toBe(false);
});
it('sets as checked when showWhitespace is true', () => {
createComponent((store) => {
Object.assign(store.state.diffs, {
showWhitespace: true,
});
setup({
storeUpdater: (origStore) => Object.assign(origStore.state.diffs, { showWhitespace: true }),
});
expect(wrapper.find('#show-whitespace').element.checked).toBe(true);
expect(wrapper.findByTestId('show-whitespace').element.checked).toBe(true);
});
it('calls setShowWhitespace on change', () => {
createComponent();
it('calls setShowWhitespace on change', async () => {
const checkbox = wrapper.findByTestId('show-whitespace');
const { checked } = checkbox.element;
const checkbox = wrapper.find('#show-whitespace');
checkbox.trigger('click');
checkbox.element.checked = true;
checkbox.trigger('change');
await vm.$nextTick();
expect(actions.setShowWhitespace).toHaveBeenCalledWith(expect.anything(), {
showWhitespace: true,
expect(store.dispatch).toHaveBeenCalledWith('diffs/setShowWhitespace', {
showWhitespace: !checked,
pushState: true,
});
});
@ -179,15 +158,12 @@ describe('Diff settings dropdown component', () => {
${false} | ${false}
`(
'sets the checkbox to { checked: $checked } if the fileByFile setting is $fileByFile',
async ({ fileByFile, checked }) => {
createComponent((store) => {
Object.assign(store.state.diffs, {
viewDiffsFileByFile: fileByFile,
});
({ fileByFile, checked }) => {
setup({
storeUpdater: (origStore) =>
Object.assign(origStore.state.diffs, { viewDiffsFileByFile: fileByFile }),
});
await vm.$nextTick();
expect(getFileByFileCheckbox(wrapper).element.checked).toBe(checked);
},
);
@ -199,19 +175,16 @@ describe('Diff settings dropdown component', () => {
`(
'when the file by file setting starts as $start, toggling the checkbox should call setFileByFile with $setting',
async ({ start, setting }) => {
createComponent((store) => {
Object.assign(store.state.diffs, {
viewDiffsFileByFile: start,
});
setup({
storeUpdater: (origStore) =>
Object.assign(origStore.state.diffs, { viewDiffsFileByFile: start }),
});
await vm.$nextTick();
getFileByFileCheckbox(wrapper).trigger('click');
await vm.$nextTick();
expect(actions.setFileByFile).toHaveBeenLastCalledWith(expect.anything(), {
expect(store.dispatch).toHaveBeenCalledWith('diffs/setFileByFile', {
fileByFile: setting,
});
},

View file

@ -1,91 +1,94 @@
import { getByText } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Issue from '~/issue';
import Issue, { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issue';
import axios from '~/lib/utils/axios_utils';
import '~/lib/utils/text_utility';
describe('Issue', () => {
let $boxClosed;
let $boxOpen;
let testContext;
let mock;
beforeEach(() => {
testContext = {};
beforeAll(() => {
preloadFixtures('issues/closed-issue.html');
preloadFixtures('issues/open-issue.html');
});
preloadFixtures('issues/closed-issue.html');
preloadFixtures('issues/open-issue.html');
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
function expectVisibility($element, shouldBeVisible) {
if (shouldBeVisible) {
expect($element).not.toHaveClass('hidden');
} else {
expect($element).toHaveClass('hidden');
}
}
testContext = {};
testContext.issue = new Issue();
});
function expectIssueState(isIssueOpen) {
expectVisibility($boxClosed, !isIssueOpen);
expectVisibility($boxOpen, isIssueOpen);
}
afterEach(() => {
mock.restore();
testContext.issue.dispose();
});
function findElements() {
$boxClosed = $('div.status-box-issue-closed');
const getIssueCounter = () => document.querySelector('.issue_counter');
const getOpenStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Open/), {
selector: '.status-box-open',
});
const getClosedStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Closed/), {
selector: '.status-box-issue-closed',
});
expect($boxClosed).toExist();
expect($boxClosed).toHaveText('Closed');
$boxOpen = $('div.status-box-open');
expect($boxOpen).toExist();
expect($boxOpen).toHaveText('Open');
}
[true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, () => {
const action = isIssueInitiallyOpen ? 'close' : 'reopen';
let mock;
function setup() {
testContext.issue = new Issue();
expectIssueState(isIssueInitiallyOpen);
testContext.$projectIssuesCounter = $('.issue_counter').first();
testContext.$projectIssuesCounter.text('1,001');
describe.each`
desc | isIssueInitiallyOpen | expectedCounterText
${'with an initially open issue'} | ${true} | ${'1,000'}
${'with an initially closed issue'} | ${false} | ${'1,002'}
`('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
loadFixtures('issues/open-issue.html');
} else {
loadFixtures('issues/closed-issue.html');
}
testContext.issueCounter = getIssueCounter();
testContext.statusBoxClosed = getClosedStatusBox();
testContext.statusBoxOpen = getOpenStatusBox();
testContext.issueCounter.textContent = '1,001';
});
it(`has the proper visible status box when ${isIssueInitiallyOpen ? 'open' : 'closed'}`, () => {
if (isIssueInitiallyOpen) {
expect(testContext.statusBoxClosed).toHaveClass('hidden');
expect(testContext.statusBoxOpen).not.toHaveClass('hidden');
} else {
expect(testContext.statusBoxClosed).not.toHaveClass('hidden');
expect(testContext.statusBoxOpen).toHaveClass('hidden');
}
});
describe('when vue app triggers change', () => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
loadFixtures('issues/open-issue.html');
} else {
loadFixtures('issues/closed-issue.html');
}
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
jest.spyOn(axios, 'get');
findElements(isIssueInitiallyOpen);
});
afterEach(() => {
mock.restore();
$('div.flash-alert').remove();
});
it(`${action}s the issue on dispatch of issuable_vue_app:change event`, () => {
setup();
document.dispatchEvent(
new CustomEvent('issuable_vue_app:change', {
new CustomEvent(EVENT_ISSUABLE_VUE_APP_CHANGE, {
detail: {
data: { id: 1 },
isClosed: isIssueInitiallyOpen,
},
}),
);
});
expectIssueState(!isIssueInitiallyOpen);
it('displays correct status box', () => {
if (isIssueInitiallyOpen) {
expect(testContext.statusBoxClosed).not.toHaveClass('hidden');
expect(testContext.statusBoxOpen).toHaveClass('hidden');
} else {
expect(testContext.statusBoxClosed).toHaveClass('hidden');
expect(testContext.statusBoxOpen).not.toHaveClass('hidden');
}
});
it('updates issueCounter text', () => {
expect(testContext.issueCounter).toBeVisible();
expect(testContext.issueCounter).toHaveText(expectedCounterText);
});
});
});

View file

@ -1,5 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
@ -63,10 +63,6 @@ describe('Pipelines Actions dropdown', () => {
});
describe('on click', () => {
beforeEach(() => {
createComponent({ actions: mockActions }, mount);
});
it('makes a request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(200);

View file

@ -1,24 +1,27 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
const createComponent = () => {
wrapper = mount(PipelineArtifacts, {
wrapper = shallowMount(PipelineArtifacts, {
propsData: {
artifacts: [
{
name: 'artifact',
name: 'job my-artifact',
path: '/download/path',
},
{
name: 'artifact two',
name: 'job-2 my-artifact-2',
path: '/download/path-two',
},
],
},
stubs: {
GlSprintf,
},
});
};
@ -39,8 +42,8 @@ describe('Pipelines Artifacts dropdown', () => {
});
it('should render a link with the provided path', () => {
expect(findFirstGlDropdownItem().find('a').attributes('href')).toEqual('/download/path');
expect(findFirstGlDropdownItem().attributes('href')).toBe('/download/path');
expect(findFirstGlDropdownItem().text()).toContain('artifact');
expect(findFirstGlDropdownItem().text()).toBe('Download job my-artifact artifact');
});
});

View file

@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import PipelinesTableRowComponent from '~/pipelines/components/pipelines_list/pipelines_table_row.vue';
import eventHub from '~/pipelines/event_hub';
@ -181,10 +182,16 @@ describe('Pipelines Table Row', () => {
expect(wrapper.find('.js-pipelines-retry-button').attributes('title')).toMatch('Retry');
expect(wrapper.find('.js-pipelines-cancel-button').exists()).toBe(true);
expect(wrapper.find('.js-pipelines-cancel-button').attributes('title')).toMatch('Cancel');
});
const actionsMenu = wrapper.find('[data-testid="pipelines-manual-actions-dropdown"]');
it('should render the manual actions', async () => {
const manualActions = wrapper.find('[data-testid="pipelines-manual-actions-dropdown"]');
expect(actionsMenu.text()).toContain(scheduledJobAction.name);
// Click on the dropdown and wait for `lazy` dropdown items
manualActions.find('.dropdown-toggle').trigger('click');
await waitForPromises();
expect(manualActions.text()).toContain(scheduledJobAction.name);
});
it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {

View file

@ -11,32 +11,31 @@ import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (initialState = mockConfig, slots = {}) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', initialState);
return shallowMount(DropdownValue, {
localVue,
store,
slots,
});
};
describe('DropdownValue', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
const createComponent = (initialState = {}, slots = {}) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', { ...mockConfig, ...initialState });
wrapper = shallowMount(DropdownValue, {
localVue,
store,
slots,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns a label filter URL based on provided label param', () => {
createComponent();
expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
@ -44,6 +43,10 @@ describe('DropdownValue', () => {
});
describe('scopedLabel', () => {
beforeEach(() => {
createComponent();
});
it('returns `true` when provided label param is a scoped label', () => {
expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true);
});
@ -56,28 +59,29 @@ describe('DropdownValue', () => {
describe('template', () => {
it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => {
createComponent();
expect(wrapper.attributes('class')).toContain('has-labels');
});
it('renders element containing `None` when `selectedLabels` is empty', () => {
const wrapperNoLabels = createComponent(
createComponent(
{
...mockConfig,
selectedLabels: [],
},
{
default: 'None',
},
);
const noneEl = wrapperNoLabels.find('span.text-secondary');
const noneEl = wrapper.find('span.text-secondary');
expect(noneEl.exists()).toBe(true);
expect(noneEl.text()).toBe('None');
wrapperNoLabels.destroy();
});
it('renders labels when `selectedLabels` is not empty', () => {
createComponent();
expect(wrapper.findAll(GlLabel).length).toBe(2);
});
});

View file

@ -47,6 +47,7 @@ export const mockConfig = {
labelsFetchPath: '/gitlab-org/my-project/-/labels.json',
labelsManagePath: '/gitlab-org/my-project/-/labels',
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
};
export const mockSuggestedColors = {

View file

@ -889,7 +889,7 @@
resolved "https://registry.yarnpkg.com/@gitlab/favicon-overlay/-/favicon-overlay-2.0.0.tgz#2f32d0b6a4d5b8ac44e2927083d9ab478a78c984"
integrity sha512-GNcORxXJ98LVGzOT9dDYKfbheqH6lNgPDD72lyXRnQIH7CjgGyos8i17aSBPq1f4s3zF3PyedFiAR4YEZbva2Q==
"@gitlab/stylelint-config@^2.2.0":
"@gitlab/stylelint-config@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/stylelint-config/-/stylelint-config-2.2.0.tgz#f0139c8bd29525b51ee9f16d26b66283bd2be5bb"
integrity sha512-yLBwRu/geN7nGzoOtF6VV2Fbjhcu2w3PwVnJ5/6wX3MILLO7Wh8zzurIjjSnls8124WUoD7n51Catrjl0hyqDw==