Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-21 15:07:30 +00:00
parent d4c968c95c
commit f1a1bd96b7
37 changed files with 393 additions and 156 deletions

View file

@ -146,7 +146,7 @@ review-stop:
.allure-report-base:
image:
name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.3.2
name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.3.4
entrypoint: [""]
stage: post-qa
variables:

View file

@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.12.5 (2021-06-21)
### Fixed (3 changes)
- [Fix failing spec](gitlab-org/gitlab@7d1a9b0155195eb082f5b33ba1310deed742a7a4) ([merge request](gitlab-org/gitlab!64488))
- [Advanced Search Settings page does not load if the ES url is unreachable](gitlab-org/gitlab@80b262f0e79f02a89724ed4e3988e686f53c959c) ([merge request](gitlab-org/gitlab!64488)) **GitLab Enterprise Edition**
- [Fix Password expired error on git fetch via SSH for LDAP user](gitlab-org/gitlab@19a7d7a6d3cd43f1c7559c729532ad3b9dafb75c) ([merge request](gitlab-org/gitlab!64488))
## 13.12.4 (2021-06-14)
### Fixed (3 changes)

View file

@ -1,5 +1,5 @@
<script>
import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { GlLabel, GlTooltip, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
@ -16,6 +16,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default {
components: {
GlTooltip,
GlLabel,
GlLoadingIcon,
GlIcon,
@ -55,7 +56,7 @@ export default {
};
},
computed: {
...mapState(['isShowingLabels', 'issuableType']),
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
@ -99,6 +100,9 @@ export default {
}
return false;
},
shouldRenderEpicCountables() {
return this.isEpicBoard && this.item.hasIssues;
},
showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel);
},
@ -115,6 +119,17 @@ export default {
}
return __('Blocked issue');
},
totalEpicsCount() {
return this.item.descendantCounts.openedEpics + this.item.descendantCounts.closedEpics;
},
totalIssuesCount() {
return this.item.descendantCounts.openedIssues + this.item.descendantCounts.closedIssues;
},
totalWeight() {
return (
this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues
);
},
},
methods: {
...mapActions(['performSearch', 'setError']),
@ -227,6 +242,59 @@ export default {
{{ itemId }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
<span v-if="shouldRenderEpicCountables" data-testid="epic-countables">
<gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
<p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
{{ __('Epics') }} &#8226;
<span class="gl-font-weight-normal"
>{{
sprintf(__('%{openedEpics} open, %{closedEpics} closed'), {
openedEpics: item.descendantCounts.openedEpics,
closedEpics: item.descendantCounts.closedEpics,
})
}}
</span>
</p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Issues') }} &#8226;
<span class="gl-font-weight-normal"
>{{
sprintf(__('%{openedIssues} open, %{closedIssues} closed'), {
openedIssues: item.descendantCounts.openedIssues,
closedIssues: item.descendantCounts.closedIssues,
})
}}
</span>
</p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Weight') }} &#8226;
<span class="gl-font-weight-normal" data-testid="epic-countables-total-weight"
>{{
sprintf(__('%{closedWeight} complete, %{openWeight} incomplete'), {
openWeight: item.descendantWeightSum.openedIssues,
closedWeight: item.descendantWeightSum.closedIssues,
})
}}
</span>
</p>
</gl-tooltip>
<span ref="countBadge" class="issue-count-badge board-card-info">
<span v-if="allowSubEpics" class="gl-mr-3">
<gl-icon name="epic" />
{{ totalEpicsCount }}
</span>
<span class="gl-mr-3" data-testid="epic-countables-counts-issues">
<gl-icon name="issues" />
{{ totalIssuesCount }}
</span>
<span class="gl-mr-3" data-testid="epic-countables-weight-issues">
<gl-icon name="weight" />
{{ totalWeight }}
</span>
</span>
</span>
<span v-if="!isEpicBoard">
<issue-due-date
v-if="item.dueDate"
:date="item.dueDate"
@ -239,6 +307,7 @@ export default {
@click="filterByWeight(item.weight)"
/>
</span>
</span>
</div>
<div class="board-card-assignee gl-display-flex">
<user-avatar-link

View file

@ -1,4 +1,4 @@
/* global Flash */
/* global createFlash */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
@ -71,5 +71,9 @@ export function fetchCommitMergeRequests() {
$container.html($content);
})
.catch(() => Flash(s__('Commits|An error occurred while fetching merge requests data.')));
.catch(() =>
createFlash({
message: s__('Commits|An error occurred while fetching merge requests data.'),
}),
);
}

View file

@ -1,7 +1,6 @@
<script>
/* global Flash */
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import createFlash from '~/flash';
import { getParameterByName } from '~/lib/utils/common_utils';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@ -116,7 +115,7 @@ export default {
this.isLoading = false;
window.scrollTo({ top: 0, behavior: 'smooth' });
Flash(COMMON_STR.FAILURE);
createFlash({ message: COMMON_STR.FAILURE });
});
},
fetchAllGroups() {
@ -202,7 +201,7 @@ export default {
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
}
Flash(message);
createFlash({ message });
this.targetGroup.isBeingRemoved = false;
});
},

View file

@ -3,7 +3,7 @@ import $ from 'jquery';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import flash from '~/flash';
import createFlash from '~/flash';
import initChangesDropdown from '~/init_changes_dropdown';
import initNotes from '~/init_notes';
import axios from '~/lib/utils/axios_utils';
@ -39,7 +39,7 @@ if (filesContainer.length) {
new Diff();
})
.catch(() => {
flash({ message: __('An error occurred while retrieving diff files') });
createFlash({ message: __('An error occurred while retrieving diff files') });
});
} else {
new Diff();

View file

@ -170,7 +170,7 @@ export default {
})
.catch(() => {
this.loading = false;
createFlash(ERROR_MESSAGE);
createFlash({ message: ERROR_MESSAGE });
});
},
formData() {

View file

@ -8,7 +8,7 @@ class Admin::RunnersController < Admin::ApplicationController
push_frontend_feature_flag(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
end
feature_category :continuous_integration
feature_category :runner
NUMBER_OF_RUNNERS_PER_PAGE = 30

View file

@ -7,7 +7,7 @@ class Groups::RunnersController < Groups::ApplicationController
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
feature_category :continuous_integration
feature_category :runner
def show
end

View file

@ -6,7 +6,7 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
feature_category :continuous_integration
feature_category :runner
def index
redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings')

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CascadeDeleteFreezePeriods < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
OLD_PROJECT_FK = 'fk_rails_2e02bbd1a6'
NEW_PROJECT_FK = 'fk_2e02bbd1a6'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :ci_freeze_periods, :projects, column: :project_id, on_delete: :cascade, name: NEW_PROJECT_FK
remove_foreign_key_if_exists :ci_freeze_periods, :projects, column: :project_id, name: OLD_PROJECT_FK
end
def down
add_concurrent_foreign_key :ci_freeze_periods, :projects, column: :project_id, on_delete: nil, name: OLD_PROJECT_FK
remove_foreign_key_if_exists :ci_freeze_periods, :projects, column: :project_id, name: NEW_PROJECT_FK
end
end

View file

@ -0,0 +1 @@
3f73aa7d2cff11d00b330d88e76daaa058f82b7012da3c244f246da6e538921c

View file

@ -25627,6 +25627,9 @@ ALTER TABLE ONLY geo_event_log
ALTER TABLE ONLY deployments
ADD CONSTRAINT fk_289bba3222 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_freeze_periods
ADD CONSTRAINT fk_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY notes
ADD CONSTRAINT fk_2e82291620 FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE SET NULL;
@ -26575,9 +26578,6 @@ ALTER TABLE ONLY onboarding_progresses
ALTER TABLE ONLY protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_2d2aba21ef FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_freeze_periods
ADD CONSTRAINT fk_rails_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id);
ALTER TABLE ONLY issuable_severities
ADD CONSTRAINT fk_rails_2fbb74ad6d FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;

View file

@ -90,7 +90,9 @@ export default {
try {
await this.contentEditor.setSerializedContent(this.content);
} catch (e) {
createFlash(__('There was an error loading content in the editor'), e);
createFlash({
message: __('There was an error loading content in the editor'), error: e
});
}
},
methods: {

View file

@ -106,7 +106,7 @@ In this file, we write the actions that call mutations for handling a list of us
.then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_USERS_ERROR, error)
createFlash('There was an error')
createFlash({ message: 'There was an error' })
});
}

View file

@ -31,7 +31,7 @@ install GitLab:
| [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, there are some trade-offs that you need to be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/>- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations).<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. |
| [Docker](https://docs.gitlab.com/omnibus/docker/) | The GitLab packages, Dockerized. | Use this method if you're familiar with Docker. |
| [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Useful for unsupported systems like \*BSD.|
| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#documentation) | The GitLab Environment toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Since GET is in beta and not yet recommended for production use, use this method if you want to test deploying GitLab in scalable environment. |
| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#documentation) | The GitLab Environment toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Customers are very welcome to trial and evaluate GET today, however be aware of [key limitations](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#missing-features-to-be-aware-of) of the current iteration. For production environments further manual setup will be required based on your specific requirements. |
## Install GitLab on cloud providers

View file

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference, howto
---
# Threads **(FREE)**
# Comments and threads **(FREE)**
GitLab encourages communication through comments, threads, and
[code suggestions](../project/merge_requests/reviews/suggestions.md).

View file

@ -112,6 +112,12 @@ You can filter by the following:
- Author
- Label
### View count of issues and weight in an epic
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331330) in GitLab 14.1.
Epics on the **Epic Boards** show a summary of their issues and weight. Hovering over the total counts will show the number of open and closed issues, as well as the completed and incomplete weight.
### Move epics and lists
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.

View file

@ -147,7 +147,7 @@ To use a custom issue template with Service Desk, in your project:
1. Go to **Settings > General > Service Desk**.
1. From the dropdown **Template to append to all Service Desk issues**, select your template.
### Using custom email display name
### Using a custom email display name
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7529) in GitLab 12.8.
@ -160,22 +160,29 @@ To edit the custom email display name:
1. Enter a new name in **Email display name**.
1. Select **Save Changes**.
### Using custom email address **(FREE SELF)**
### Using a custom email address **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2201) in GitLab Premium 13.0.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284656) in GitLab 13.8.
If the `service_desk_email` is configured, then you can create Service Desk
issues by sending emails to the Service Desk email address. The default
address has the following format:
`project_contact+%{key}@example.com`.
It is possible to customize the email address used by Service Desk. To do this, you must configure
both a [custom mailbox](#configuring-a-custom-mailbox) and a
[custom suffix](#configuring-a-custom-email-address-suffix).
The `%{key}` part is used to find the project where the issue should be created. The
`%{key}` part combines the path to the project and configurable project name suffix:
`<project_full_path>-<project_name_suffix>`.
#### Configuring a custom mailbox
You can set the project name suffix in your project's Service Desk settings.
It can contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`).
NOTE:
On GitLab.com a custom mailbox is already configured with `contact-project+%{key}@incoming.gitlab.com` as the email address, so you only have to configure the
[custom suffix](#configuring-a-custom-email-address-suffix) in project settings.
Using the `service_desk_email` configuration, you can customize the mailbox
used by Service Desk. This allows you to have a separate email address for
Service Desk by also configuring a [custom suffix](#configuring-a-custom-email-address-suffix)
in project settings.
The `address` must include the `+%{key}` placeholder within the 'user'
portion of the address, before the `@`. This is used to identify the project
where the issue should be created.
NOTE:
The `service_desk_email` and `incoming_email` configurations should
@ -183,7 +190,7 @@ always use separate mailboxes. This is important, because emails picked from
`service_desk_email` mailbox are processed by a different worker and it would
not recognize `incoming_email` emails.
To configure a custom email address for Service Desk with IMAP, add the following snippets to your configuration file:
To configure a custom mailbox for Service Desk with IMAP, add the following snippets to your configuration file in full:
- Example for installations from source:
@ -207,36 +214,22 @@ To configure a custom email address for Service Desk with IMAP, add the followin
```ruby
gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
gitlab_rails['service_desk_email_email'] = "project_support@gmail.com"
gitlab_rails['service_desk_email_password'] = "[REDACTED]"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_idle_timeout'] = 60
gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
gitlab_rails['service_desk_email_host'] = "imap.gmail.com"
gitlab_rails['service_desk_email_port'] = 993
gitlab_rails['service_desk_email_ssl'] = true
gitlab_rails['service_desk_email_start_tls'] = false
```
In this case, suppose the `mygroup/myproject` project Service Desk settings has the project name
suffix set to `support`, and a user sends an email to `project_contact+mygroup-myproject-support@example.com`.
As a result, a new Service Desk issue is created from this email in the `mygroup/myproject` project.
The configuration options are the same as for configuring
[incoming email](../../administration/incoming_email.md#set-it-up).
#### Microsoft Graph
##### Microsoft Graph
> Introduced in [GitLab 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/214900)
@ -247,17 +240,11 @@ Graph API instead of IMAP. Follow the [documentation in the incoming e-mail sect
```ruby
gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@example.onmicrosoft.com"
gitlab_rails['service_desk_email_email'] = "project_contact@example.onmicrosoft.com"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
gitlab_rails['service_desk_inbox_method'] = 'microsoft_graph'
gitlab_rails['service_desk_inbox_options'] = {
'tenant_id': '<YOUR-TENANT-ID>',
'client_id': '<YOUR-CLIENT-ID>',
@ -268,6 +255,22 @@ Graph API instead of IMAP. Follow the [documentation in the incoming e-mail sect
The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details.
#### Configuring a custom email address suffix
You can set a custom suffix in your project's Service Desk settings once you have configured a [custom mailbox](#configuring-a-custom-mailbox).
It can contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`).
When configured, the custom suffix creates a new Service Desk email address, consisting of the
`service_desk_email_address` setting and a key of the format: `<project_full_path>-<custom_suffix>`
For example, suppose the `mygroup/myproject` project Service Desk settings has the following configured:
- Project name suffix is set to `support`.
- Service Desk email address is configured to `contact+%{key}@example.com`.
The Service Desk email address for this project is: `contact+mygroup-myproject-support@example.com`.
The [incoming email](../../administration/incoming_email.md) address still works.
## Using Service Desk
You can use Service Desk to [create an issue](#as-an-end-user-issue-creator) or [respond to one](#as-a-responder-to-the-issue).

View file

@ -436,6 +436,9 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests"
msgstr[0] ""
msgstr[1] ""
msgid "%{closedWeight} complete, %{openWeight} incomplete"
msgstr ""
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr ""
@ -6352,6 +6355,9 @@ msgstr ""
msgid "Checkout|Failed to load states. Please try again."
msgstr ""
msgid "Checkout|Failed to load the payment form. Please try again."
msgstr ""
msgid "Checkout|Failed to register credit card. Please try again."
msgstr ""
@ -31434,9 +31440,6 @@ msgstr ""
msgid "SuperSonics|Cloud license"
msgstr ""
msgid "SuperSonics|Enter activation code"
msgstr ""
msgid "SuperSonics|Expires on"
msgstr ""

View file

@ -72,8 +72,14 @@ module QA
testcase = example.metadata[:testcase]
example.tms('Testcase', testcase) if testcase
issue = example.metadata.dig(:quarantine, :issue)
example.issue('Issue', issue) if issue
quarantine_issue = example.metadata.dig(:quarantine, :issue)
example.issue('Quarantine issue', quarantine_issue) if quarantine_issue
spec_file = example.file_path.split('/').last
example.issue(
'Failure issues',
"https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}"
)
example.add_link(name: "Job(#{Env.ci_job_name})", url: Env.ci_job_url) if Env.running_in_ci?
end

View file

@ -14,20 +14,20 @@ module QA
let!(:api_client) { Runtime::API::Client.new(user: user) }
let!(:personal_access_token) { api_client.personal_access_token }
let!(:sandbox) do
let(:sandbox) do
Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = admin_api_client
end
end
let!(:source_group) do
let(:source_group) do
Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = api_client
group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
end
end
let!(:subgroup) do
let(:subgroup) do
Resource::Group.fabricate_via_api! do |group|
group.api_client = api_client
group.sandbox = source_group
@ -63,6 +63,10 @@ module QA
before do
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
# create groups explicitly before connecting gitlab instance
source_group
subgroup
Flow::Login.sign_in(as: user)
Page::Main::Menu.perform(&:go_to_create_group)
Page::Group::New.perform do |group|
@ -73,6 +77,7 @@ module QA
# Non blocking issues:
# https://gitlab.com/gitlab-org/gitlab/-/issues/331252
# https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back
it(
'imports group with subgroups and labels',
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785',
@ -96,9 +101,9 @@ module QA
Page::Group::BulkImport.perform do |import_page|
import_page.import_group(source_group.path, sandbox.path)
aggregate_failures do
expect(import_page).to have_imported_group(source_group.path, wait: 180)
aggregate_failures do
expect { imported_group.reload! }.to eventually_eq(source_group).within(duration: 10)
expect { imported_group.labels }.to eventually_include(*source_group.labels).within(duration: 10)

View file

@ -39,9 +39,9 @@ module QA
end
context 'when using attachments in comments', :object_storage do
let(:gif_file_name) { 'banana_sample.gif' }
let(:png_file_name) { 'testfile.png' }
let(:file_to_attach) do
File.absolute_path(File.join('qa', 'fixtures', 'designs', gif_file_name))
File.absolute_path(File.join('qa', 'fixtures', 'designs', png_file_name))
end
before do
@ -50,9 +50,9 @@ module QA
it 'comments on an issue with an attachment', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1742' do
Page::Project::Issue::Show.perform do |show|
show.comment('See attached banana for scale', attachment: file_to_attach)
show.comment('See attached image for scale', attachment: file_to_attach)
expect(show.noteable_note_item.find("img[src$='#{gif_file_name}']")).to be_visible
expect(show.noteable_note_item.find("img[src$='#{png_file_name}']")).to be_visible
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :ci_pending_build, class: 'Ci::PendingBuild' do
build factory: :ci_build
project
protected { build.protected }
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :ci_running_build, class: 'Ci::RunningBuild' do
build factory: :ci_build
project
runner factory: :ci_runner
runner_type { runner.runner_type }
end
end

View file

@ -1,7 +1,7 @@
import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants';
@ -35,8 +35,14 @@ describe('Board card component', () => {
let store;
const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
const findEpicCountables = () => wrapper.findByTestId('epic-countables');
const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const createStore = () => {
const createStore = ({ isEpicBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
state: {
@ -45,16 +51,14 @@ describe('Board card component', () => {
},
getters: {
isGroupBoard: () => true,
isEpicBoard: () => false,
isEpicBoard: () => isEpicBoard,
isProjectBoard: () => false,
},
});
};
const createWrapper = (props = {}) => {
createStore();
wrapper = mount(BoardCardInner, {
wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
list,
@ -88,6 +92,7 @@ describe('Board card component', () => {
weight: 1,
};
createStore();
createWrapper({ item: issue, list });
});
@ -414,7 +419,90 @@ describe('Board card component', () => {
},
});
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('is an epic board', () => {
const descendantCounts = {
closedEpics: 0,
closedIssues: 0,
openedEpics: 0,
openedIssues: 0,
};
const descendantWeightSum = {
closedIssues: 0,
openedIssues: 0,
};
beforeEach(() => {
createStore({ isEpicBoard: true });
});
it('should render if the item has issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: true,
},
});
expect(findEpicCountables().exists()).toBe(true);
});
it('should not render if the item does not have issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: false,
},
});
expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
});
it('shows render item countBadge and weights correctly', () => {
createWrapper({
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
},
descendantWeightSum: {
...descendantWeightSum,
openedIssues: 2,
},
hasIssues: true,
},
});
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
expect(findEpicCountablesBadgeWeight().text()).toBe('2');
});
it('renders the tooltip with the correct data', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
},
hasIssues: true,
},
});
const tooltip = findEpicCountablesTotalTooltip();
expect(tooltip).toBeDefined();
expect(findEpicCountablesTotalWeight().text()).toBe('10 complete, 5 incomplete');
});
});
});

View file

@ -1,33 +1,17 @@
import { GlButton } from '@gitlab/ui';
import { Extension } from '@tiptap/core';
import { shallowMount } from '@vue/test-utils';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_button', () => {
let wrapper;
let tiptapEditor;
let toggleFooSpy;
const CONTENT_TYPE = 'bold';
const ICON_NAME = 'bold';
const LABEL = 'Bold';
const buildEditor = () => {
toggleFooSpy = jest.fn();
tiptapEditor = createContentEditor({
extensions: [
{
tiptapExtension: Extension.create({
addCommands() {
return {
toggleFoo: () => toggleFooSpy,
};
},
}),
},
],
renderMarkdown: () => true,
}).tiptapEditor;
tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'isActive');
};
@ -78,20 +62,28 @@ describe('content_editor/components/toolbar_button', () => {
describe('when button is clicked', () => {
it('executes the content type command when executeCommand = true', async () => {
buildWrapper({ editorCommand: 'toggleFoo' });
const editorCommand = 'toggleFoo';
const mockCommands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
buildWrapper({ editorCommand });
await findButton().trigger('click');
expect(toggleFooSpy).toHaveBeenCalled();
expect(mockCommands[editorCommand]).toHaveBeenCalled();
expect(mockCommands.focus).toHaveBeenCalled();
expect(mockCommands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute).toHaveLength(1);
});
it('does not executes the content type command when executeCommand = false', async () => {
const editorCommand = 'toggleFoo';
const mockCommands = mockChainedCommands(tiptapEditor, [editorCommand, 'run']);
buildWrapper();
await findButton().trigger('click');
expect(toggleFooSpy).not.toHaveBeenCalled();
expect(mockCommands[editorCommand]).not.toHaveBeenCalled();
expect(wrapper.emitted().execute).toHaveLength(1);
});
});

View file

@ -2,21 +2,16 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
import { createTestContentEditorExtension, createTestEditor } from '../test_utils';
import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_headings_dropdown', () => {
let wrapper;
let tiptapEditor;
let commandMocks;
const buildEditor = () => {
const testExtension = createTestContentEditorExtension({
commands: TEXT_STYLE_DROPDOWN_ITEMS.map((item) => item.editorCommand),
});
commandMocks = testExtension.commandMocks;
tiptapEditor = createTestEditor({
extensions: [testExtension.tiptapExtension],
extensions: [Heading],
});
jest.spyOn(tiptapEditor, 'isActive');
@ -104,9 +99,12 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
const { editorCommand, commandParams } = textStyle;
const commands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
expect(commandMocks[editorCommand]).toHaveBeenCalledWith(commandParams || {});
expect(commands[editorCommand]).toHaveBeenCalledWith(commandParams || {});
expect(commands.focus).toHaveBeenCalled();
expect(commands.run).toHaveBeenCalled();
});
});

View file

@ -1,26 +1,23 @@
import { BulletList } from '@tiptap/extension-bullet-list';
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { Document } from '@tiptap/extension-document';
import { Heading } from '@tiptap/extension-heading';
import { ListItem } from '@tiptap/extension-list-item';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
import {
KEYBOARD_SHORTCUT_TRACKING_ACTION,
INPUT_RULE_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
import { tiptapExtension as BulletList } from '~/content_editor/extensions/bullet_list';
import { tiptapExtension as CodeBlockLowlight } from '~/content_editor/extensions/code_block_highlight';
import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
import { tiptapExtension as ListItem } from '~/content_editor/extensions/list_item';
import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
import { createTestEditor } from '../test_utils';
describe('content_editor/services/track_input_rules_and_shortcuts', () => {
let trackingSpy;
let editor;
let trackedExtensions;
const HEADING_TEXT = 'Heading text';
const extensions = [Document, Paragraph, Text, Heading, CodeBlockLowlight, BulletList, ListItem];
const extensions = [Heading, CodeBlockLowlight, BulletList, ListItem];
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
@ -29,7 +26,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('given the heading extension is instrumented', () => {
beforeEach(() => {
trackedExtensions = extensions.map(trackInputRulesAndShortcuts);
editor = new Editor({
editor = createTestEditor({
extensions: extensions.map(trackInputRulesAndShortcuts),
});
});

View file

@ -15,7 +15,7 @@ import { Editor } from '@tiptap/vue-2';
* include in the editor
* @returns An instance of a Tiptaps Editor class
*/
export const createTestEditor = ({ extensions = [] }) => {
export const createTestEditor = ({ extensions = [] } = {}) => {
return new Editor({
extensions: [Document, Text, Paragraph, ...extensions],
});

View file

@ -1,9 +1,9 @@
import '~/flash';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@ -27,6 +27,7 @@ import {
const $toast = {
show: jest.fn(),
};
jest.mock('~/flash');
describe('AppComponent', () => {
let wrapper;
@ -123,12 +124,12 @@ describe('AppComponent', () => {
mock.onGet('/dashboard/groups.json').reply(400);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
jest.spyOn(window, 'Flash').mockImplementation(() => {});
return vm.fetchGroups({}).then(() => {
expect(vm.isLoading).toBe(false);
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.');
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred. Please try again.',
});
});
});
});
@ -324,15 +325,13 @@ describe('AppComponent', () => {
const message = 'An error occurred. Please try again.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 });
jest.spyOn(vm.store, 'removeGroup');
jest.spyOn(window, 'Flash').mockImplementation(() => {});
vm.leaveGroup();
expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
expect(window.Flash).toHaveBeenCalledWith(message);
expect(createFlash).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false);
});
});
@ -341,15 +340,13 @@ describe('AppComponent', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 });
jest.spyOn(vm.store, 'removeGroup');
jest.spyOn(window, 'Flash').mockImplementation(() => {});
vm.leaveGroup(childGroupItem, groupItem);
expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
expect(window.Flash).toHaveBeenCalledWith(message);
expect(createFlash).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false);
});
});

View file

@ -190,7 +190,9 @@ describe('UploadBlobModal', () => {
});
it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.');
expect(createFlash).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
});
});
afterEach(() => {

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('cascade_delete_freeze_periods')
RSpec.describe CascadeDeleteFreezePeriods do
let(:namespace) { table(:namespaces).create!(name: 'deploy_freeze', path: 'deploy_freeze') }
let(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
let(:freeze_periods) { table(:ci_freeze_periods) }
describe "#up" do
it 'allows for a project to be deleted' do
freeze_periods.create!(id: 1, project_id: project.id, freeze_start: '5 * * * *', freeze_end: '6 * * * *', cron_timezone: 'UTC')
migrate!
project.delete
expect(freeze_periods.where(project_id: project.id).count).to be_zero
end
end
end

View file

@ -384,7 +384,7 @@ RSpec.describe Ci::Build do
context 'when there is a queuing entry already present' do
before do
::Ci::PendingBuild.create!(build: build, project: build.project)
create(:ci_pending_build, build: build, project: build.project)
end
it 'does not raise an error' do
@ -396,7 +396,7 @@ RSpec.describe Ci::Build do
context 'when both failure scenario happen at the same time' do
before do
::Ci::Build.find(build.id).update_column(:lock_version, 100)
::Ci::PendingBuild.create!(build: build, project: build.project)
create(:ci_pending_build, build: build, project: build.project)
end
it 'raises stale object error exception' do
@ -478,7 +478,7 @@ RSpec.describe Ci::Build do
let(:build) { create(:ci_build, :pending) }
before do
::Ci::PendingBuild.create!(build: build, project: build.project)
create(:ci_pending_build, build: build, project: build.project)
::Ci::Build.find(build.id).update_column(:lock_version, 100)
end

View file

@ -20,7 +20,7 @@ RSpec.describe Ci::PendingBuild do
context 'when another queuing entry exists for given build' do
before do
described_class.create!(build: build, project: project, protected: false)
create(:ci_pending_build, build: build, project: project)
end
it 'returns a build id as a result' do

View file

@ -21,10 +21,7 @@ RSpec.describe Ci::RunningBuild do
context 'when another queuing entry exists for given build' do
before do
described_class.create!(build: build,
project: project,
runner: runner,
runner_type: runner.runner_type)
create(:ci_running_build, build: build, project: project, runner: runner)
end
it 'returns a build id as a result' do

View file

@ -45,7 +45,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when duplicate entry exists' do
before do
::Ci::PendingBuild.create!(build: build, project: project)
create(:ci_pending_build, build: build, project: build.project)
end
it 'does nothing and returns build id' do
@ -66,7 +66,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when pending build exists' do
before do
Ci::PendingBuild.create!(build: build, project: project)
create(:ci_pending_build, build: build, project: build.project)
end
it 'removes pending build in a transaction' do
@ -146,9 +146,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when duplicate entry exists' do
before do
::Ci::RunningBuild.create!(
build: build, project: project, runner: runner, runner_type: runner.runner_type
)
create(:ci_running_build, build: build, project: project, runner: runner)
end
it 'does nothing and returns build id' do
@ -169,9 +167,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when shared runner build tracking entry exists' do
before do
Ci::RunningBuild.create!(
build: build, project: project, runner: runner, runner_type: runner.runner_type
)
create(:ci_running_build, build: build, project: project, runner: runner)
end
it 'removes shared runner build' do