Commit 8a7efa45 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 53b1f4ea
...@@ -409,5 +409,4 @@ RSpec/RepeatedExample: ...@@ -409,5 +409,4 @@ RSpec/RepeatedExample:
- 'spec/rubocop/cop/migration/update_large_table_spec.rb' - 'spec/rubocop/cop/migration/update_large_table_spec.rb'
- 'spec/services/notification_service_spec.rb' - 'spec/services/notification_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb' - 'spec/services/web_hook_service_spec.rb'
- 'ee/spec/services/boards/lists/update_service_spec.rb'
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb' - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
...@@ -16,6 +16,14 @@ import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_ ...@@ -16,6 +16,14 @@ import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_
import { ListType } from '../constants'; import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
/**
* Please don't edit this file, have a look at:
* ./board_column.vue
* https://gitlab.com/gitlab-org/gitlab/-/issues/212300
*
* This file here will be deleted soon
* @deprecated
*/
export default Vue.extend({ export default Vue.extend({
components: { components: {
BoardBlankState, BoardBlankState,
...@@ -54,6 +62,13 @@ export default Vue.extend({ ...@@ -54,6 +62,13 @@ export default Vue.extend({
type: String, type: String,
required: true, required: true,
}, },
// Does not do anything but is used
// to support the API of the new board_column.vue
canAdminList: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
......
This diff is collapsed.
...@@ -3,7 +3,6 @@ import Vue from 'vue'; ...@@ -3,7 +3,6 @@ import Vue from 'vue';
import 'ee_else_ce/boards/models/issue'; import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list'; import 'ee_else_ce/boards/models/list';
import Board from 'ee_else_ce/boards/components/board';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar'; import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
...@@ -65,7 +64,15 @@ export default () => { ...@@ -65,7 +64,15 @@ export default () => {
issueBoardsApp = new Vue({ issueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
Board, Board: () =>
window?.gon?.features?.sfcIssueBoards
? import('ee_else_ce/boards/components/board_column.vue')
: /**
* Please have a look at, we are moving to the SFC soon:
* https://gitlab.com/gitlab-org/gitlab/-/issues/212300
* @deprecated
*/
import('ee_else_ce/boards/components/board'),
BoardSidebar, BoardSidebar,
BoardAddIssuesModal, BoardAddIssuesModal,
BoardSettingsSidebar: () => BoardSettingsSidebar: () =>
......
...@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table'; ...@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf'; import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale'; import { n__, __ } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
...@@ -41,7 +42,6 @@ export default function dropzoneInput(form) { ...@@ -41,7 +42,6 @@ export default function dropzoneInput(form) {
let addFileToForm; let addFileToForm;
let updateAttachingMessage; let updateAttachingMessage;
let isImage; let isImage;
let getFilename;
let uploadFile; let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>'); formTextarea.wrap('<div class="div-dropzone"></div>');
...@@ -235,17 +235,6 @@ export default function dropzoneInput(form) { ...@@ -235,17 +235,6 @@ export default function dropzoneInput(form) {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`); $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
}; };
getFilename = e => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
value = e.clipboardData.getData('text/plain');
}
value = value.split('\r');
return value[0];
};
const showSpinner = () => $uploadingProgressContainer.removeClass('hide'); const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
......
...@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => { ...@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => {
form.querySelector('.js-filename').textContent = fileInput.value.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape form.querySelector('.js-filename').textContent = fileInput.value.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
}); });
}; };
export const getFilename = ({ clipboardData }) => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
} else if (clipboardData && clipboardData.getData) {
value = clipboardData.getData('text/plain');
}
value = value.split('\r');
return value[0];
};
...@@ -48,7 +48,7 @@ export default { ...@@ -48,7 +48,7 @@ export default {
}, },
}, },
mounted() { mounted() {
if (!this.hasTruncatedDiffLines) { if (this.isTextFile && !this.hasTruncatedDiffLines) {
this.fetchDiff(); this.fetchDiff();
} }
}, },
......
...@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController ...@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true) push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end end
private private
......
...@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true) push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end end
private private
......
...@@ -20,7 +20,7 @@ module ContainerExpirationPoliciesHelper ...@@ -20,7 +20,7 @@ module ContainerExpirationPoliciesHelper
def older_than_options def older_than_options
ContainerExpirationPolicy.older_than_options.map do |key, val| ContainerExpirationPolicy.older_than_options.map do |key, val|
{ key: key.to_s, label: val }.tap do |base| { key: key.to_s, label: val }.tap do |base|
base[:default] = true if key.to_s == '30d' base[:default] = true if key.to_s == '90d'
end end
end end
end end
......
...@@ -32,7 +32,7 @@ module Ci ...@@ -32,7 +32,7 @@ module Ci
end end
def status def status
@status ||= statuses.latest.slow_composite_status @status ||= statuses.latest.slow_composite_status(project: project)
end end
def detailed_status(current_user) def detailed_status(current_user)
......
...@@ -968,7 +968,7 @@ module Ci ...@@ -968,7 +968,7 @@ module Ci
def latest_builds_status def latest_builds_status
return 'failed' unless yaml_errors.blank? return 'failed' unless yaml_errors.blank?
statuses.latest.slow_composite_status || 'skipped' statuses.latest.slow_composite_status(project: project) || 'skipped'
end end
def keep_around_commits def keep_around_commits
......
...@@ -138,7 +138,7 @@ module Ci ...@@ -138,7 +138,7 @@ module Ci
end end
def latest_stage_status def latest_stage_status
statuses.latest.slow_composite_status || 'skipped' statuses.latest.slow_composite_status(project: project) || 'skipped'
end end
end end
end end
...@@ -178,12 +178,12 @@ class CommitStatus < ApplicationRecord ...@@ -178,12 +178,12 @@ class CommitStatus < ApplicationRecord
select(:name) select(:name)
end end
def self.status_for_prior_stages(index) def self.status_for_prior_stages(index, project:)
before_stage(index).latest.slow_composite_status || 'success' before_stage(index).latest.slow_composite_status(project: project) || 'success'
end end
def self.status_for_names(names) def self.status_for_names(names, project:)
where(name: names).latest.slow_composite_status || 'success' where(name: names).latest.slow_composite_status(project: project) || 'success'
end end
def self.update_as_processed! def self.update_as_processed!
......
...@@ -65,8 +65,8 @@ module HasStatus ...@@ -65,8 +65,8 @@ module HasStatus
# This method performs expensive calculation of status: # This method performs expensive calculation of status:
# 1. By plucking all related objects, # 1. By plucking all related objects,
# 2. Or executes expensive SQL query # 2. Or executes expensive SQL query
def slow_composite_status def slow_composite_status(project:)
if Feature.enabled?(:ci_composite_status, default_enabled: false) if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
Gitlab::Ci::Status::Composite Gitlab::Ci::Status::Composite
.new(all, with_allow_failure: columns_hash.key?('allow_failure')) .new(all, with_allow_failure: columns_hash.key?('allow_failure'))
.status .status
......
...@@ -1689,7 +1689,7 @@ class User < ApplicationRecord ...@@ -1689,7 +1689,7 @@ class User < ApplicationRecord
def gitlab_employee? def gitlab_employee?
strong_memoize(:gitlab_employee) do strong_memoize(:gitlab_employee) do
if Gitlab.com? if Gitlab.com?
Mail::Address.new(email).domain == "gitlab.com" Mail::Address.new(email).domain == "gitlab.com" && confirmed?
else else
false false
end end
......
...@@ -89,11 +89,11 @@ module Ci ...@@ -89,11 +89,11 @@ module Ci
end end
def status_for_prior_stages(index) def status_for_prior_stages(index)
pipeline.processables.status_for_prior_stages(index) pipeline.processables.status_for_prior_stages(index, project: pipeline.project)
end end
def status_for_build_needs(needs) def status_for_build_needs(needs)
pipeline.processables.status_for_names(needs) pipeline.processables.status_for_names(needs, project: pipeline.project)
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
......
- board = local_assigns.fetch(:board, nil) - board = local_assigns.fetch(:board, nil)
- group = local_assigns.fetch(:group, false) - group = local_assigns.fetch(:group, false)
-# TODO: Move group_id and can_admin_list to the board store
See: https://gitlab.com/gitlab-org/gitlab/-/issues/213082
- group_id = @group&.id || "null"
- can_admin_list = can?(current_user, :admin_list, current_board_parent) == true
- @no_breadcrumb_container = true - @no_breadcrumb_container = true
- @no_container = true - @no_container = true
- @content_class = "issue-boards-content js-focus-mode-board" - @content_class = "issue-boards-content js-focus-mode-board"
...@@ -22,6 +26,8 @@ ...@@ -22,6 +26,8 @@
%board{ "v-cloak" => "true", %board{ "v-cloak" => "true",
"v-for" => "list in state.lists", "v-for" => "list in state.lists",
"ref" => "board", "ref" => "board",
":can-admin-list" => can_admin_list,
":group-id" => group_id,
":list" => "list", ":list" => "list",
":disabled" => "disabled", ":disabled" => "disabled",
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
......
-# Please have a look at app/assets/javascripts/boards/components/board_column.vue
This haml file is deprecated and will be deleted soon, please change the Vue app
https://gitlab.com/gitlab-org/gitlab/-/issues/212300
.board.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }', .board.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id", data: { qa_selector: "board_list" } } ":data-id" => "list.id", data: { qa_selector: "board_list" } }
.board-inner.d-flex.flex-column.position-relative.h-100.rounded .board-inner.d-flex.flex-column.position-relative.h-100.rounded
......
---
title: Upload a design by copy/pasting the file into the Design Tab
merge_request: 27776
author:
type: added
---
title: Enable container expiration policies by default for new projects
merge_request: 28480
author:
type: changed
---
title: Remove duplicate specs in update service spec
merge_request: 28650
author: Rajendra Kadam
type: added
# frozen_string_literal: true
class EnableContainerExpirationPoliciesByDefault < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
change_column_default :container_expiration_policies, :enabled, true
end
end
def down
with_lock_retries do
change_column_default :container_expiration_policies, :enabled, false
end
end
end
...@@ -1842,7 +1842,7 @@ CREATE TABLE public.container_expiration_policies ( ...@@ -1842,7 +1842,7 @@ CREATE TABLE public.container_expiration_policies (
cadence character varying(12) DEFAULT '7d'::character varying NOT NULL, cadence character varying(12) DEFAULT '7d'::character varying NOT NULL,
older_than character varying(12), older_than character varying(12),
keep_n integer, keep_n integer,
enabled boolean DEFAULT false NOT NULL enabled boolean DEFAULT true NOT NULL
); );
CREATE TABLE public.container_repositories ( CREATE TABLE public.container_repositories (
...@@ -12926,5 +12926,6 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -12926,5 +12926,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326145443 20200326145443
20200330074719 20200330074719
20200330132913 20200330132913
20200331220930
\. \.
...@@ -436,6 +436,24 @@ Keyset-based pagination is only supported for selected resources and ordering op ...@@ -436,6 +436,24 @@ Keyset-based pagination is only supported for selected resources and ordering op
| ------------------------- | -------------------------- | | ------------------------- | -------------------------- |
| [Projects](projects.md) | `order_by=id` only | | [Projects](projects.md) | `order_by=id` only |
## Path parameters
If an endpoint has path parameters, the documentation shows them with a preceding colon.
For example:
```plaintext
DELETE /projects/:id/share/:group_id
```
The `:id` path parameter needs to be replaced with the project id, and the `:group_id` needs to be replaced with the id of the group. The colons `:` should not be included.
The resulting cURL call for a project with id `5` and a group id of `17` is then:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/share/17
```
## Namespaced path encoding ## Namespaced path encoding
If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_PATH` is If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_PATH` is
......
...@@ -98,6 +98,8 @@ Complementary reads: ...@@ -98,6 +98,8 @@ Complementary reads:
- [Application limits](application_limits.md) - [Application limits](application_limits.md)
- [Redis guidelines](redis.md) - [Redis guidelines](redis.md)
- [Rails initializers](rails_initializers.md) - [Rails initializers](rails_initializers.md)
- [Code comments](code_comments.md)
- [Renaming features](renaming_features.md)
## Performance guides ## Performance guides
...@@ -150,9 +152,7 @@ Complementary reads: ...@@ -150,9 +152,7 @@ Complementary reads:
- [Verifying database capabilities](verifying_database_capabilities.md) - [Verifying database capabilities](verifying_database_capabilities.md)
- [Database Debugging and Troubleshooting](database_debugging.md) - [Database Debugging and Troubleshooting](database_debugging.md)
- [Query Count Limits](query_count_limits.md) - [Query Count Limits](query_count_limits.md)
- [Code comments](code_comments.md)
- [Creating enums](creating_enums.md) - [Creating enums](creating_enums.md)
- [Renaming features](renaming_features.md)
### Case studies ### Case studies
......
...@@ -121,7 +121,7 @@ For instance: ...@@ -121,7 +121,7 @@ For instance:
The [internal API](./internal_api.md) is documented for internal use. Please keep it up to date so we know what endpoints The [internal API](./internal_api.md) is documented for internal use. Please keep it up to date so we know what endpoints
different components are making use of. different components are making use of.
[Entity]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/entities.rb [Entity]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/entities
[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion [validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
[installing GitLab under a relative URL]: https://docs.gitlab.com/ee/install/relative_url.html [installing GitLab under a relative URL]: https://docs.gitlab.com/ee/install/relative_url.html
......
...@@ -79,6 +79,7 @@ the following preparations into account. ...@@ -79,6 +79,7 @@ the following preparations into account.
- Include either a rollback procedure or describe how to rollback changes. - Include either a rollback procedure or describe how to rollback changes.
- Add the output of the migration(s) to the MR description. - Add the output of the migration(s) to the MR description.
- Add tests for the migration in `spec/migrations` if necessary. See [Testing Rails migrations at GitLab](testing_guide/testing_migrations_guide.md) for more details. - Add tests for the migration in `spec/migrations` if necessary. See [Testing Rails migrations at GitLab](testing_guide/testing_migrations_guide.md) for more details.
- When [high-traffic](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/migration_helpers.rb#L12) tables are involved in the migration, use the [`with_lock_retries`](migration_style_guide.md#retry-mechanism-when-acquiring-database-locks) helper method. Review the relevant [examples in our documentation](migration_style_guide.md#examples) for use cases and solutions.
#### Preparation when adding or modifying queries #### Preparation when adding or modifying queries
......
...@@ -171,7 +171,7 @@ lock allow the database to process other statements. ...@@ -171,7 +171,7 @@ lock allow the database to process other statements.
### Examples ### Examples
Removing a column: **Removing a column:**
```ruby ```ruby
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
...@@ -189,7 +189,7 @@ def down ...@@ -189,7 +189,7 @@ def down
end end
``` ```
Removing a foreign key: **Removing a foreign key:**
```ruby ```ruby
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
...@@ -207,7 +207,7 @@ def down ...@@ -207,7 +207,7 @@ def down
end end
``` ```
Changing default value for a column: **Changing default value for a column:**
```ruby ```ruby
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
...@@ -225,6 +225,88 @@ def down ...@@ -225,6 +225,88 @@ def down
end end
``` ```
**Creating a new table with a foreign key:**
We can simply wrap the `create_table` method with `with_lock_retries`:
```ruby
def up
with_lock_retries do
create_table :issues do |t|
t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
t.string :title, limit: 255
end
end
end
def down
drop_table :issues
end
```
**Creating a new table when we have two foreign keys:**
For this, we'll need three migrations:
1. Creating the table without foreign keys (with the indices).
1. Add foreign key to the first table.
1. Add foreign key to the second table.
Creating the table:
```ruby
def up
create_table :imports do |t|
t.bigint :project_id, null: false
t.bigint :user_id, null: false
t.string :jid, limit: 255
end
add_index :imports, :project_id
add_index :imports, :user_id
end
def down
drop_table :imports
end
```
Adding foreign key to `projects`:
```ruby
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
add_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
end
end
def down
with_lock_retries do
remove_foreign_key :imports, column: :project_id
end
end
```
Adding foreign key to `users`:
```ruby
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
add_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
end
end
def down
with_lock_retries do
remove_foreign_key :imports, column: :user_id
end
end
```
### When to use the helper method ### When to use the helper method
The `with_lock_retries` helper method can be used when you normally use The `with_lock_retries` helper method can be used when you normally use
......
...@@ -247,7 +247,17 @@ create the actual RDS instance. ...@@ -247,7 +247,17 @@ create the actual RDS instance.
![RDS Subnet Group](img/rds_subnet_group.png) ![RDS Subnet Group](img/rds_subnet_group.png)
### Creating the database ### RDS Security Group
We need a security group for our database that will allow inbound traffic from the instances we'll deploy in our `gitlab-loadbalancer-sec-group` later on:
1. From the EC2 dashboard, select **Security Groups** from the left menu bar.
1. Click **Create security group**.
1. Give it a name (we'll use `gitlab-rds-sec-group`), a description, and select the `gitlab-vpc` from the **VPC** dropdown.
1. In the **Inbound rules** section, click **Add rule** and add a **PostgreSQL** rule, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier. The default PostgreSQL port is `5432`, which we'll also use when creating our database below.
1. When done, click **Create security group**.
### Create the database
Now, it's time to create the database: Now, it's time to create the database:
...@@ -266,7 +276,7 @@ Now, it's time to create the database: ...@@ -266,7 +276,7 @@ Now, it's time to create the database:
1. Select the VPC we created earlier (`gitlab-vpc`) from the **Virtual Private Cloud (VPC)** dropdown menu. 1. Select the VPC we created earlier (`gitlab-vpc`) from the **Virtual Private Cloud (VPC)** dropdown menu.
1. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier. 1. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier.
1. Set public accessibility to **No**. 1. Set public accessibility to **No**.
1. Under **VPC security group**, select **Create new** and enter a name. We'll use `gitlab-rds-sec-group`. 1. Under **VPC security group**, select **Choose existing** and select the `gitlab-rds-sec-group` we create above from the dropdown.
1. Leave the database port as the default `5432`. 1. Leave the database port as the default `5432`.
1. For **Database authentication**, select **Password authentication**. 1. For **Database authentication**, select **Password authentication**.
1. Expand the **Additional configuration** section and complete the following: 1. Expand the **Additional configuration** section and complete the following:
...@@ -327,17 +337,6 @@ persistence and is used for certain types of the GitLab application. ...@@ -327,17 +337,6 @@ persistence and is used for certain types of the GitLab application.
1. Leave the rest of the settings to their default values or edit to your liking. 1. Leave the rest of the settings to their default values or edit to your liking.
1. When done, click **Create**. 1. When done, click **Create**.
## RDS and Redis Security Group
Let's navigate to our EC2 security groups and add a small change for our EC2
instances to be able to connect to RDS. First, copy the security group name we
defined, namely `gitlab-security-group`, select the RDS security group and edit the
inbound rules. Choose the rule type to be PostgreSQL and paste the name under
source.
Similar to the above, jump to the `gitlab-security-group` group
and add a custom TCP rule for port `6379` accessible within itself.
## Setting up Bastion Hosts ## Setting up Bastion Hosts
Since our GitLab instances will be in private subnets, we need a way to connect to these instances via SSH to make configuration changes, perform upgrades, etc. One way of doing this is via a [bastion host](https://en.wikipedia.org/wiki/Bastion_host), sometimes also referred to as a jump box. Since our GitLab instances will be in private subnets, we need a way to connect to these instances via SSH to make configuration changes, perform upgrades, etc. One way of doing this is via a [bastion host](https://en.wikipedia.org/wiki/Bastion_host), sometimes also referred to as a jump box.
......
...@@ -969,6 +969,15 @@ If you want to switch back to Unicorn, follow these steps: ...@@ -969,6 +969,15 @@ If you want to switch back to Unicorn, follow these steps:
1. Edit the system `init.d` script to set the `USE_UNICORN=1` flag. If you have `/etc/default/gitlab`, then you should edit it instead. 1. Edit the system `init.d` script to set the `USE_UNICORN=1` flag. If you have `/etc/default/gitlab`, then you should edit it instead.
1. Restart GitLab. 1. Restart GitLab.
### Using Sidekiq instead of Sidekiq Cluster
As of GitLab 12.10, Source installations are using `bin/sidekiq-cluster` for managing Sidekiq processes.
Using Sidekiq directly will still be supported until 14.0. So if you're experiencing issues, please:
1. Edit the system `init.d` script to remove the `SIDEKIQ_WORKERS` flag. If you have `/etc/default/gitlab`, then you should edit it instead.
1. Restart GitLab.
1. [Create an issue](https://gitlab.com/gitlab-org/gitlab/issues/-/new) describing the problem.
## Troubleshooting ## Troubleshooting
### "You appear to have cloned an empty repository." ### "You appear to have cloned an empty repository."
......
...@@ -75,6 +75,19 @@ you can drag and drop designs onto the dedicated dropzone to upload them. ...@@ -75,6 +75,19 @@ you can drag and drop designs onto the dedicated dropzone to upload them.
![Drag and drop design uploads](img/design_drag_and_drop_uploads_v12_9.png) ![Drag and drop design uploads](img/design_drag_and_drop_uploads_v12_9.png)
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202634)
in GitLab 12.10, you can also copy images from your file system and
paste them directly on GitLab's Design page as a new design.
On macOS you can also take a screenshot and immediately copy it to
the clipboard by simultaneously clicking <kbd>Control</kbd> + <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>3</kbd>, and then paste it as a design.
Copy-and-pasting has some limitations:
- You can paste only one image at a time. When copy/pasting multiple files, only the first one will be uploaded.
- All images will be converted to `png` format under the hood, so when you want to copy/paste `gif` file, it will result in broken animation.
- Copy/pasting designs is not supported on Internet Explorer.
Designs with the same filename as an existing uploaded design will create a new version Designs with the same filename as an existing uploaded design will create a new version
of the design, and will replace the previous version. [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9, dropping a design on an existing uploaded design will also create a new version, of the design, and will replace the previous version. [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9, dropping a design on an existing uploaded design will also create a new version,
provided the filenames are the same. provided the filenames are the same.
......
...@@ -9,7 +9,13 @@ module Gitlab ...@@ -9,7 +9,13 @@ module Gitlab
private private
def create_labels(worker_class, queue) def create_labels(worker_class, queue)
labels = { queue: queue.to_s, urgency: "", external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" } labels = { queue: queue.to_s,
worker: worker_class.to_s,
urgency: "",
external_dependencies: FALSE_LABEL,
feature_category: "",
boundary: "" }
return labels unless worker_class && worker_class.include?(WorkerAttributes) return labels unless worker_class && worker_class.include?(WorkerAttributes)
labels[:urgency] = worker_class.get_urgency.to_s labels[:urgency] = worker_class.get_urgency.to_s
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
### Environment variables ### Environment variables
RAILS_ENV="production" RAILS_ENV="production"
USE_UNICORN="" USE_UNICORN=""
SIDEKIQ_WORKERS=1
# Script variable names should be lower-case not to conflict with # Script variable names should be lower-case not to conflict with
# internal /bin/sh variables such as PATH, EDITOR or SHELL. # internal /bin/sh variables such as PATH, EDITOR or SHELL.
...@@ -36,7 +37,6 @@ pid_path="$app_root/tmp/pids" ...@@ -36,7 +37,6 @@ pid_path="$app_root/tmp/pids"
socket_path="$app_root/tmp/sockets" socket_path="$app_root/tmp/sockets"
rails_socket="$socket_path/gitlab.socket" rails_socket="$socket_path/gitlab.socket"
web_server_pid_path="$pid_path/unicorn.pid" web_server_pid_path="$pid_path/unicorn.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
mail_room_enabled=false mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid" mail_room_pid_path="$pid_path/mail_room.pid"
gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd) gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
...@@ -74,6 +74,11 @@ else ...@@ -74,6 +74,11 @@ else
use_web_server="unicorn" use_web_server="unicorn"
fi fi
if [ -z "$SIDEKIQ_WORKERS" ]; then
sidekiq_pid_path="$pid_path/sidekiq.pid"
else
sidekiq_pid_path="$pid_path/sidekiq-cluster.pid"
fi
### Init Script functions ### Init Script functions
...@@ -295,7 +300,7 @@ start_gitlab() { ...@@ -295,7 +300,7 @@ start_gitlab() {
if [ "$sidekiq_status" = "0" ]; then if [ "$sidekiq_status" = "0" ]; then
echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting" echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting"
else else
RAILS_ENV=$RAILS_ENV bin/background_jobs start & RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs start &
fi fi
if [ "$gitlab_workhorse_status" = "0" ]; then if [ "$gitlab_workhorse_status" = "0" ]; then
...@@ -354,7 +359,7 @@ stop_gitlab() { ...@@ -354,7 +359,7 @@ stop_gitlab() {
fi fi
if [ "$sidekiq_status" = "0" ]; then if [ "$sidekiq_status" = "0" ]; then
echo "Shutting down GitLab Sidekiq" echo "Shutting down GitLab Sidekiq"
RAILS_ENV=$RAILS_ENV bin/background_jobs stop RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs stop
fi fi
if [ "$gitlab_workhorse_status" = "0" ]; then if [ "$gitlab_workhorse_status" = "0" ]; then
echo "Shutting down GitLab Workhorse" echo "Shutting down GitLab Workhorse"
...@@ -458,7 +463,7 @@ reload_gitlab(){ ...@@ -458,7 +463,7 @@ reload_gitlab(){
echo "Done." echo "Done."
echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..." echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..."
RAILS_ENV=$RAILS_ENV bin/background_jobs restart RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs restart
if [ "$mail_room_enabled" != true ]; then if [ "$mail_room_enabled" != true ]; then
echo "Restarting GitLab MailRoom since it isn't capable of reloading its config..." echo "Restarting GitLab MailRoom since it isn't capable of reloading its config..."
......
...@@ -26,7 +26,6 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' ...@@ -26,7 +26,6 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
it 'saves expiration policy submit the form' do it 'saves expiration policy submit the form' do
within '#js-registry-policies' do within '#js-registry-policies' do
within '.card-body' do within '.card-body' do
find('.gl-toggle-wrapper button:not(.is-disabled)').click
select('7 days until tags are automatically removed', from: 'Expiration interval:') select('7 days until tags are automatically removed', from: 'Expiration interval:')
select('Every day', from: 'Expiration schedule:') select('Every day', from: 'Expiration schedule:')
select('50 tags per image name', from: 'Number of tags to retain:') select('50 tags per image name', from: 'Number of tags to retain:')
......
import Sortablejs from 'sortablejs'; const Sortablejs = jest.genMockFromModule('sortablejs');
export default Sortablejs; export default Sortablejs;
export const Sortable = Sortablejs; export const Sortable = Sortablejs;
......
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Board from '~/boards/components/board_column.vue';
import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
describe('Board Column Component', () => {
let wrapper;
let axiosMock;
beforeEach(() => {
window.gon = {};
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
});
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
localStorage.clear();
});
const createComponent = ({
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
} = {}) => {
const boardId = '1';
const listMock = {
...listObj,
list_type: listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
listMock.user = {};
}
// Making List reactive
const list = Vue.observable(new List(listMock));
if (withLocalStorage) {
localStorage.setItem(
`boards.${boardId}.${list.type}.${list.id}.expanded`,
(!collapsed).toString(),
);
}
wrapper = shallowMount(Board, {
propsData: {
boardId,
disabled: false,
issueLinkBase: '/',
rootPath: '/',
list,
},
});
};
const isExpandable = () => wrapper.classes('is-expandable');
const isCollapsed = () => wrapper.classes('is-collapsed');
const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
describe('Add issue button', () => {
const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
createComponent({ listType });
expect(findAddIssueButton().exists()).toBe(false);
});
it.each(hasAddButton)('does render when List Type is `%s`', listType => {
createComponent({ listType });
expect(findAddIssueButton().exists()).toBe(true);
});
it('has a test for each list type', () => {
Object.values(ListType).forEach(value => {
expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
});
});
it('does render when logged out', () => {
createComponent();
expect(findAddIssueButton().exists()).toBe(true);
});
});
describe('Given different list types', () => {
it('is expandable when List Type is `backlog`', () => {
createComponent({ listType: ListType.backlog });
expect(isExpandable()).toBe(true);
});
});
describe('expanding / collapsing the column', () => {
it('does not collapse when clicking the header', () => {
createComponent();
expect(isCollapsed()).toBe(false);
wrapper.find('.board-header').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(false);
});
});
it('collapses expanded Column when clicking the collapse icon', () => {
createComponent();
expect(wrapper.vm.list.isExpanded).toBe(true);
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(true);
});
});
it('expands collapsed Column when clicking the expand icon', () => {
createComponent({ collapsed: true });
expect(isCollapsed()).toBe(true);
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(false);
});
});
it("when logged in it calls list update and doesn't set localStorage", () => {
jest.spyOn(List.prototype, 'update');
window.gon.current_user_id = 1;
createComponent({ withLocalStorage: false });
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
});
});
it("when logged out it doesn't call list update and sets localStorage", () => {
jest.spyOn(List.prototype, 'update');
createComponent();
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.list.update).toHaveBeenCalledTimes(0);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(
String(wrapper.vm.list.isExpanded),
);
});
});
});
});
...@@ -56,7 +56,7 @@ describe('List model', () => { ...@@ -56,7 +56,7 @@ describe('List model', () => {
label: { label: {
id: 1, id: 1,
title: 'test', title: 'test',
color: 'red', color: '#ff0000',
text_color: 'white', text_color: 'white',
}, },
}); });
...@@ -64,8 +64,7 @@ describe('List model', () => { ...@@ -64,8 +64,7 @@ describe('List model', () => {
expect(list.id).toBe(listObj.id); expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label'); expect(list.type).toBe('label');
expect(list.position).toBe(0); expect(list.position).toBe(0);
expect(list.label.color).toBe('red'); expect(list.label).toEqual(listObj.label);
expect(list.label.textColor).toBe('white');
}); });
}); });
......
...@@ -15,7 +15,7 @@ export const listObj = { ...@@ -15,7 +15,7 @@ export const listObj = {
label: { label: {
id: 5000, id: 5000,
title: 'Test', title: 'Test',
color: 'red', color: '#ff0000',
description: 'testing;', description: 'testing;',
textColor: 'white', textColor: 'white',
}, },
...@@ -30,7 +30,7 @@ export const listObjDuplicate = { ...@@ -30,7 +30,7 @@ export const listObjDuplicate = {
label: { label: {
id: listObj.label.id, id: listObj.label.id,
title: 'Test', title: 'Test',
color: 'red', color: '#ff0000',
description: 'testing;', description: 'testing;',
}, },
}; };
......
import fileUpload from '~/lib/utils/file_upload'; import fileUpload, { getFilename } from '~/lib/utils/file_upload';
describe('File upload', () => { describe('File upload', () => {
beforeEach(() => { beforeEach(() => {
...@@ -62,3 +62,15 @@ describe('File upload', () => { ...@@ -62,3 +62,15 @@ describe('File upload', () => {
expect(input.click).not.toHaveBeenCalled(); expect(input.click).not.toHaveBeenCalled();
}); });
}); });
describe('getFilename', () => {
it('returns first value correctly', () => {
const event = {
clipboardData: {
getData: () => 'test.png\rtest.txt',
},
};
expect(getFilename(event)).toBe('test.png');
});
});
...@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; ...@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue'; import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
import { discussionMock } from '../../../javascripts/notes/mock_data'; import { discussionMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_discussions'; import mockDiffFile from '../../diffs/mock_data/diff_discussions';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
......
import Vue from 'vue'; import { mount } from '@vue/test-utils';
import { mountComponentWithStore } from 'spec/helpers';
import DiffWithNote from '~/notes/components/diff_with_note.vue'; import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { createStore } from '~/mr_notes/stores'; import { createStore } from '~/mr_notes/stores';
...@@ -8,25 +7,17 @@ const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; ...@@ -8,25 +7,17 @@ const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
describe('diff_with_note', () => { describe('diff_with_note', () => {
let store; let store;
let vm; let wrapper;
const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
const diffDiscussion = diffDiscussionMock;
const Component = Vue.extend(DiffWithNote);
const props = {
discussion: diffDiscussion,
};
const selectors = { const selectors = {
get container() {
return vm.$el;
},
get diffTable() { get diffTable() {
return this.container.querySelector('.diff-content table'); return wrapper.find('.diff-content table');
}, },
get diffRows() { get diffRows() {
return this.container.querySelectorAll('.diff-content .line_holder'); return wrapper.findAll('.diff-content .line_holder');
}, },
get noteRow() { get noteRow() {
return this.container.querySelector('.diff-content .notes_holder'); return wrapper.find('.diff-content .notes_holder');
}, },
}; };
...@@ -44,25 +35,33 @@ describe('diff_with_note', () => { ...@@ -44,25 +35,33 @@ describe('diff_with_note', () => {
describe('text diff', () => { describe('text diff', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponentWithStore(Component, { props, store }); const diffDiscussion = getJSONFixture(discussionFixture)[0];
wrapper = mount(DiffWithNote, {
propsData: {
discussion: diffDiscussion,
},
store,
});
}); });
it('removes trailing "+" char', () => { it('removes trailing "+" char', () => {
const richText = vm.$el.querySelectorAll('.line_holder')[4].querySelector('.line_content') const richText = wrapper.vm.$el
.textContent[0]; .querySelectorAll('.line_holder')[4]
.querySelector('.line_content').textContent[0];
expect(richText).not.toEqual('+'); expect(richText).not.toEqual('+');
}); });
it('removes trailing "-" char', () => { it('removes trailing "-" char', () => {
const richText = vm.$el.querySelector('#LC13').parentNode.textContent[0]; const richText = wrapper.vm.$el.querySelector('#LC13').parentNode.textContent[0];
expect(richText).not.toEqual('-'); expect(richText).not.toEqual('-');
}); });
it('shows text diff', () => { it('shows text diff', () => {
expect(selectors.container).toHaveClass('text-file'); expect(wrapper.classes('text-file')).toBe(true);
expect(selectors.diffTable).toExist(); expect(selectors.diffTable.exists()).toBe(true);
}); });
it('shows diff lines', () => { it('shows diff lines', () => {
...@@ -70,20 +69,18 @@ describe('diff_with_note', () => { ...@@ -70,20 +69,18 @@ describe('diff_with_note', () => {
}); });
it('shows notes row', () => { it('shows notes row', () => {
expect(selectors.noteRow).toExist(); expect(selectors.noteRow.exists()).toBe(true);
}); });
}); });
describe('image diff', () => { describe('image diff', () => {
beforeEach(() => { beforeEach(() => {
const imageDiffDiscussionMock = getJSONFixture(imageDiscussionFixture)[0]; const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0];
props.discussion = imageDiffDiscussionMock; wrapper = mount(DiffWithNote, { propsData: { discussion: imageDiscussion }, store });
}); });
it('shows image diff', () => { it('shows image diff', () => {
vm = mountComponentWithStore(Component, { props, store }); expect(selectors.diffTable.exists()).toBe(false);
expect(selectors.diffTable).not.toExist();
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import createStore from '~/notes/stores'; import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import notesModule from '~/notes/stores/modules';
import DiscussionFilter from '~/notes/components/discussion_filter.vue'; import DiscussionFilter from '~/notes/components/discussion_filter.vue';
import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { discussionFiltersMock, discussionMock } from '../mock_data'; import { discussionFiltersMock, discussionMock } from '../mock_data';
import { TEST_HOST } from 'jest/helpers/test_constants';
const localVue = createLocalVue();
localVue.use(Vuex);
const DISCUSSION_PATH = `${TEST_HOST}/example`;
describe('DiscussionFilter component', () => { describe('DiscussionFilter component', () => {
let vm; let wrapper;
let store; let store;
let eventHub; let eventHub;
let mock;
const mountComponent = () => { const filterDiscussion = jest.fn();
store = createStore();
const mountComponent = () => {
const discussions = [ const discussions = [
{ {
...discussionMock, ...discussionMock,
...@@ -20,83 +34,101 @@ describe('DiscussionFilter component', () => { ...@@ -20,83 +34,101 @@ describe('DiscussionFilter component', () => {
notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
}, },
]; ];
const Component = Vue.extend(DiscussionFilter);
const selectedValue = DISCUSSION_FILTERS_DEFAULT_VALUE; const defaultStore = { ...notesModule() };
const props = { filters: discussionFiltersMock, selectedValue };
store = new Vuex.Store({
...defaultStore,
actions: {
...defaultStore.actions,
filterDiscussion,
},
});
store.state.notesData.discussionsPath = DISCUSSION_PATH;
store.state.discussions = discussions; store.state.discussions = discussions;
return mountComponentWithStore(Component, {
el: null, return mount(DiscussionFilter, {
store, store,
props, propsData: {
filters: discussionFiltersMock,
selectedValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
},
localVue,
}); });
}; };
beforeEach(() => { beforeEach(() => {
mock = new AxiosMockAdapter(axios);
// We are mocking the discussions retrieval,
// as it doesn't matter for our tests here
mock.onGet(DISCUSSION_PATH).reply(200, '');
window.mrTabs = undefined; window.mrTabs = undefined;
vm = mountComponent(); wrapper = mountComponent();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.vm.$destroy();
mock.restore();
}); });
it('renders the all filters', () => { it('renders the all filters', () => {
expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual( expect(wrapper.findAll('.dropdown-menu li').length).toBe(discussionFiltersMock.length);
discussionFiltersMock.length,
);
}); });
it('renders the default selected item', () => { it('renders the default selected item', () => {
expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual( expect(
discussionFiltersMock[0].title, wrapper
); .find('#discussion-filter-dropdown')
.text()
.trim(),
).toBe(discussionFiltersMock[0].title);
}); });
it('updates to the selected item', () => { it('updates to the selected item', () => {
const filterItem = vm.$el.querySelector( const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
); );
filterItem.click();
expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim()); filterItem.trigger('click');
expect(wrapper.vm.currentFilter.title).toBe(filterItem.text().trim());
}); });
it('only updates when selected filter changes', () => { it('only updates when selected filter changes', () => {
const filterItem = vm.$el.querySelector( wrapper
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, .find(`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`)
); .trigger('click');
spyOn(vm, 'filterDiscussion');
filterItem.click();
expect(vm.filterDiscussion).not.toHaveBeenCalled(); expect(filterDiscussion).not.toHaveBeenCalled();
}); });
it('disables commenting when "Show history only" filter is applied', () => { it('disables commenting when "Show history only" filter is applied', () => {
const filterItem = vm.$el.querySelector( const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
); );
filterItem.click(); filterItem.trigger('click');
expect(vm.$store.state.commentsDisabled).toBe(true); expect(wrapper.vm.$store.state.commentsDisabled).toBe(true);
}); });
it('enables commenting when "Show history only" filter is not applied', () => { it('enables commenting when "Show history only" filter is not applied', () => {
const filterItem = vm.$el.querySelector( const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
); );
filterItem.click(); filterItem.trigger('click');
expect(vm.$store.state.commentsDisabled).toBe(false); expect(wrapper.vm.$store.state.commentsDisabled).toBe(false);
}); });
it('renders a dropdown divider for the default filter', () => { it('renders a dropdown divider for the default filter', () => {
const defaultFilter = vm.$el.querySelector( const defaultFilter = wrapper.findAll(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`, `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] > *`,
); );
expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); expect(defaultFilter.at(defaultFilter.length - 1).classes('dropdown-divider')).toBe(true);
}); });
describe('Merge request tabs', () => { describe('Merge request tabs', () => {
...@@ -108,7 +140,7 @@ describe('DiscussionFilter component', () => { ...@@ -108,7 +140,7 @@ describe('DiscussionFilter component', () => {
currentTab: 'show', currentTab: 'show',
}; };
vm = mountComponent(); wrapper = mountComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -118,8 +150,8 @@ describe('DiscussionFilter component', () => { ...@@ -118,8 +150,8 @@ describe('DiscussionFilter component', () => {
it('only renders when discussion tab is active', done => { it('only renders when discussion tab is active', done => {
eventHub.$emit('MergeRequestTabChange', 'commit'); eventHub.$emit('MergeRequestTabChange', 'commit');
vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(vm.$el.querySelector).toBeUndefined(); expect(wrapper.isEmpty()).toBe(true);
done(); done();
}); });
}); });
...@@ -132,54 +164,54 @@ describe('DiscussionFilter component', () => { ...@@ -132,54 +164,54 @@ describe('DiscussionFilter component', () => {
it('updates the filter when the URL links to a note', done => { it('updates the filter when the URL links to a note', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`; window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.currentValue = discussionFiltersMock[2].value; wrapper.vm.currentValue = discussionFiltersMock[2].value;
vm.handleLocationHash(); wrapper.vm.handleLocationHash();
vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
done(); done();
}); });
}); });
it('does not update the filter when the current filter is "Show all activity"', done => { it('does not update the filter when the current filter is "Show all activity"', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`; window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.handleLocationHash(); wrapper.vm.handleLocationHash();
vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
done(); done();
}); });
}); });
it('only updates filter when the URL links to a note', done => { it('only updates filter when the URL links to a note', done => {
window.location.hash = `testing123`; window.location.hash = `testing123`;
vm.handleLocationHash(); wrapper.vm.handleLocationHash();
vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
done(); done();
}); });
}); });
it('fetches discussions when there is a hash', done => { it('fetches discussions when there is a hash', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`; window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.currentValue = discussionFiltersMock[2].value; wrapper.vm.currentValue = discussionFiltersMock[2].value;
spyOn(vm, 'selectFilter'); jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
vm.handleLocationHash(); wrapper.vm.handleLocationHash();
vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(vm.selectFilter).toHaveBeenCalled(); expect(wrapper.vm.selectFilter).toHaveBeenCalled();
done(); done();
}); });
}); });
it('does not fetch discussions when there is no hash', done => { it('does not fetch discussions when there is no hash', done => {
window.location.hash = ''; window.location.hash = '';
spyOn(vm, 'selectFilter'); jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
vm.handleLocationHash(); wrapper.vm.handleLocationHash();
vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(vm.selectFilter).not.toHaveBeenCalled(); expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
done(); done();
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue'; import awardsNote from '~/notes/components/note_awards_list.vue';
import { noteableDataMock, notesDataMock } from '../mock_data'; import { noteableDataMock, notesDataMock } from '../mock_data';
import { TEST_HOST } from 'jest/helpers/test_constants';
describe('note_awards_list component', () => { describe('note_awards_list component', () => {
let store; let store;
let vm; let vm;
let awardsMock; let awardsMock;
let mock;
const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
beforeEach(() => { beforeEach(() => {
mock = new AxiosMockAdapter(axios);
mock.onPost(toggleAwardPath).reply(200, '');
const Component = Vue.extend(awardsNote); const Component = Vue.extend(awardsNote);
store = createStore(); store = createStore();
...@@ -32,12 +42,13 @@ describe('note_awards_list component', () => { ...@@ -32,12 +42,13 @@ describe('note_awards_list component', () => {
noteAuthorId: 2, noteAuthorId: 2,
noteId: '545', noteId: '545',
canAwardEmoji: true, canAwardEmoji: true,
toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', toggleAwardPath,
}, },
}).$mount(); }).$mount();
}); });
afterEach(() => { afterEach(() => {
mock.restore();
vm.$destroy(); vm.$destroy();
}); });
...@@ -49,8 +60,8 @@ describe('note_awards_list component', () => { ...@@ -49,8 +60,8 @@ describe('note_awards_list component', () => {
}); });
it('should be possible to remove awarded emoji', () => { it('should be possible to remove awarded emoji', () => {
spyOn(vm, 'handleAward').and.callThrough(); jest.spyOn(vm, 'handleAward');
spyOn(vm, 'toggleAwardRequest').and.callThrough(); jest.spyOn(vm, 'toggleAwardRequest');
vm.$el.querySelector('.js-awards-block button').click(); vm.$el.querySelector('.js-awards-block button').click();
expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
...@@ -138,7 +149,7 @@ describe('note_awards_list component', () => { ...@@ -138,7 +149,7 @@ describe('note_awards_list component', () => {
}); });
it('should not be possible to remove awarded emoji', () => { it('should not be possible to remove awarded emoji', () => {
spyOn(vm, 'toggleAwardRequest').and.callThrough(); jest.spyOn(vm, 'toggleAwardRequest');
vm.$el.querySelector('.js-awards-block button').click(); vm.$el.querySelector('.js-awards-block button').click();
......
...@@ -4,6 +4,10 @@ import NoteForm from '~/notes/components/note_form.vue'; ...@@ -4,6 +4,10 @@ import NoteForm from '~/notes/components/note_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { noteableDataMock, notesDataMock } from '../mock_data'; import { noteableDataMock, notesDataMock } from '../mock_data';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
jest.mock('~/lib/utils/autosave');
describe('issue_note_form component', () => { describe('issue_note_form component', () => {
const dummyAutosaveKey = 'some-autosave-key'; const dummyAutosaveKey = 'some-autosave-key';
const dummyDraft = 'dummy draft content'; const dummyDraft = 'dummy draft content';
...@@ -23,7 +27,7 @@ describe('issue_note_form component', () => { ...@@ -23,7 +27,7 @@ describe('issue_note_form component', () => {
}; };
beforeEach(() => { beforeEach(() => {
spyOnDependency(NoteForm, 'getDraft').and.callFake(key => { getDraft.mockImplementation(key => {
if (key === dummyAutosaveKey) { if (key === dummyAutosaveKey) {
return dummyDraft; return dummyDraft;
} }
...@@ -55,19 +59,15 @@ describe('issue_note_form component', () => { ...@@ -55,19 +59,15 @@ describe('issue_note_form component', () => {
expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`); expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`);
}); });
it('return note hash as `#` when `noteId` is empty', done => { it('return note hash as `#` when `noteId` is empty', () => {
wrapper.setProps({ wrapper.setProps({
...props, ...props,
noteId: '', noteId: '',
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(wrapper.vm.noteHash).toBe('#');
.then(() => { });
expect(wrapper.vm.noteHash).toBe('#');
})
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -76,7 +76,7 @@ describe('issue_note_form component', () => { ...@@ -76,7 +76,7 @@ describe('issue_note_form component', () => {
wrapper = createComponentWrapper(); wrapper = createComponentWrapper();
}); });
it('should show conflict message if note changes outside the component', done => { it('should show conflict message if note changes outside the component', () => {
wrapper.setProps({ wrapper.setProps({
...props, ...props,
isEditing: true, isEditing: true,
...@@ -86,21 +86,17 @@ describe('issue_note_form component', () => { ...@@ -86,21 +86,17 @@ describe('issue_note_form component', () => {
const message = const message =
'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() const conflictWarning = wrapper.find('.js-conflict-edit-warning');
.then(() => {
const conflictWarning = wrapper.find('.js-conflict-edit-warning'); expect(conflictWarning.exists()).toBe(true);
expect(
expect(conflictWarning.exists()).toBe(true); conflictWarning
expect( .text()
conflictWarning .replace(/\s+/g, ' ')
.text() .trim(),
.replace(/\s+/g, ' ') ).toBe(message);
.trim(), });
).toBe(message);
})
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -136,7 +132,7 @@ describe('issue_note_form component', () => { ...@@ -136,7 +132,7 @@ describe('issue_note_form component', () => {
describe('up', () => { describe('up', () => {
it('should ender edit mode', () => { it('should ender edit mode', () => {
// TODO: do not spy on vm // TODO: do not spy on vm
spyOn(wrapper.vm, 'editMyLastNote').and.callThrough(); jest.spyOn(wrapper.vm, 'editMyLastNote');
textarea.trigger('keydown.up'); textarea.trigger('keydown.up');
...@@ -164,61 +160,50 @@ describe('issue_note_form component', () => { ...@@ -164,61 +160,50 @@ describe('issue_note_form component', () => {
}); });
describe('actions', () => { describe('actions', () => {
it('should be possible to cancel', done => { it('should be possible to cancel', () => {
// TODO: do not spy on vm // TODO: do not spy on vm
spyOn(wrapper.vm, 'cancelHandler').and.callThrough(); jest.spyOn(wrapper.vm, 'cancelHandler');
wrapper.setProps({ wrapper.setProps({
...props, ...props,
isEditing: true, isEditing: true,
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() const cancelButton = wrapper.find('.note-edit-cancel');
.then(() => { cancelButton.trigger('click');
const cancelButton = wrapper.find('.note-edit-cancel');
cancelButton.trigger('click');
expect(wrapper.vm.cancelHandler).toHaveBeenCalled(); expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
}) });
.then(done)
.catch(done.fail);
}); });
it('should be possible to update the note', done => { it('should be possible to update the note', () => {
wrapper.setProps({ wrapper.setProps({
...props, ...props,
isEditing: true, isEditing: true,
}); });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() const textarea = wrapper.find('textarea');
.then(() => { textarea.setValue('Foo');
const textarea = wrapper.find('textarea'); const saveButton = wrapper.find('.js-vue-issue-save');
textarea.setValue('Foo'); saveButton.trigger('click');
const saveButton = wrapper.find('.js-vue-issue-save');
saveButton.trigger('click'); expect(wrapper.vm.isSubmitting).toBe(true);
});
expect(wrapper.vm.isSubmitting).toEqual(true);
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
describe('with autosaveKey', () => { describe('with autosaveKey', () => {
describe('with draft', () => { describe('with draft', () => {
beforeEach(done => { beforeEach(() => {
Object.assign(props, { Object.assign(props, {
noteBody: '', noteBody: '',
autosaveKey: dummyAutosaveKey, autosaveKey: dummyAutosaveKey,
}); });
wrapper = createComponentWrapper(); wrapper = createComponentWrapper();
wrapper.vm return wrapper.vm.$nextTick();
.$nextTick()
.then(done)
.catch(done.fail);
}); });
it('displays the draft in textarea', () => { it('displays the draft in textarea', () => {
...@@ -229,17 +214,14 @@ describe('issue_note_form component', () => { ...@@ -229,17 +214,14 @@ describe('issue_note_form component', () => {
}); });
describe('without draft', () => { describe('without draft', () => {
beforeEach(done => { beforeEach(() => {
Object.assign(props, { Object.assign(props, {
noteBody: '', noteBody: '',
autosaveKey: 'some key without draft', autosaveKey: 'some key without draft',
}); });
wrapper = createComponentWrapper(); wrapper = createComponentWrapper();
wrapper.vm return wrapper.vm.$nextTick();
.$nextTick()
.then(done)
.catch(done.fail);
}); });
it('leaves the textarea empty', () => { it('leaves the textarea empty', () => {
...@@ -250,7 +232,6 @@ describe('issue_note_form component', () => { ...@@ -250,7 +232,6 @@ describe('issue_note_form component', () => {
}); });
it('updates the draft if textarea content changes', () => { it('updates the draft if textarea content changes', () => {
const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub();
Object.assign(props, { Object.assign(props, {
noteBody: '', noteBody: '',
autosaveKey: dummyAutosaveKey, autosaveKey: dummyAutosaveKey,
...@@ -261,7 +242,7 @@ describe('issue_note_form component', () => { ...@@ -261,7 +242,7 @@ describe('issue_note_form component', () => {
textarea.setValue(dummyContent); textarea.setValue(dummyContent);
expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent); expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
}); });
}); });
}); });
...@@ -12,8 +12,8 @@ import { ...@@ -12,8 +12,8 @@ import {
loggedOutnoteableData, loggedOutnoteableData,
userDataMock, userDataMock,
} from '../mock_data'; } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file'; import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import { trimText } from '../../helpers/text_helper'; import { trimText } from 'helpers/text_helper';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
...@@ -47,27 +47,24 @@ describe('noteable_discussion component', () => { ...@@ -47,27 +47,24 @@ describe('noteable_discussion component', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false); expect(wrapper.find('.discussion-header').exists()).toBe(false);
}); });
it('should render thread header', done => { it('should render thread header', () => {
const discussion = { ...discussionMock }; const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile; discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true; discussion.diff_discussion = true;
discussion.expanded = false;
wrapper.setProps({ discussion }); wrapper.setProps({ discussion });
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(wrapper.find('.discussion-header').exists()).toBe(true);
.then(() => { });
expect(wrapper.find('.discussion-header').exists()).toBe(true);
})
.then(done)
.catch(done.fail);
}); });
describe('actions', () => { describe('actions', () => {
it('should toggle reply form', done => { it('should toggle reply form', () => {
const replyPlaceholder = wrapper.find(ReplyPlaceholder); const replyPlaceholder = wrapper.find(ReplyPlaceholder);
wrapper.vm return wrapper.vm
.$nextTick() .$nextTick()
.then(() => { .then(() => {
expect(wrapper.vm.isReplying).toEqual(false); expect(wrapper.vm.isReplying).toEqual(false);
...@@ -89,9 +86,7 @@ describe('noteable_discussion component', () => { ...@@ -89,9 +86,7 @@ describe('noteable_discussion component', () => {
expect(noteFormProps.line).toBe(null); expect(noteFormProps.line).toBe(null);
expect(noteFormProps.saveButtonTitle).toBe('Comment'); expect(noteFormProps.saveButtonTitle).toBe('Comment');
expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`); expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`);
}) });
.then(done)
.catch(done.fail);
}); });
it('does not render jump to thread button', () => { it('does not render jump to thread button', () => {
...@@ -115,7 +110,7 @@ describe('noteable_discussion component', () => { ...@@ -115,7 +110,7 @@ describe('noteable_discussion component', () => {
}); });
describe('for unresolved thread', () => { describe('for unresolved thread', () => {
beforeEach(done => { beforeEach(() => {
const discussion = { const discussion = {
...getJSONFixture(discussionWithTwoUnresolvedNotes)[0], ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0],
expanded: true, expanded: true,
...@@ -131,10 +126,7 @@ describe('noteable_discussion component', () => { ...@@ -131,10 +126,7 @@ describe('noteable_discussion component', () => {
wrapper.setProps({ discussion }); wrapper.setProps({ discussion });
wrapper.vm return wrapper.vm.$nextTick();
.$nextTick()
.then(done)
.catch(done.fail);
}); });
it('displays a button to resolve with issue', () => { it('displays a button to resolve with issue', () => {
......
...@@ -86,7 +86,7 @@ describe('issue_note', () => { ...@@ -86,7 +86,7 @@ describe('issue_note', () => {
it('prevents note preview xss', done => { it('prevents note preview xss', done => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
const alertSpy = spyOn(window, 'alert'); const alertSpy = jest.spyOn(window, 'alert');
store.hotUpdate({ store.hotUpdate({
actions: { actions: {
updateNote() {}, updateNote() {},
...@@ -96,11 +96,11 @@ describe('issue_note', () => { ...@@ -96,11 +96,11 @@ describe('issue_note', () => {
noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {}); noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
setTimeout(() => { setImmediate(() => {
expect(alertSpy).not.toHaveBeenCalled(); expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.vm.note.note_html).toEqual(escape(noteBody)); expect(wrapper.vm.note.note_html).toEqual(escape(noteBody));
done(); done();
}, 0); });
}); });
describe('cancel edit', () => { describe('cancel edit', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'helpers/vue_mount_component_helper';
import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
import { note } from '../mock_data'; import { note } from '../mock_data';
...@@ -44,7 +44,7 @@ describe('toggle replies widget for notes', () => { ...@@ -44,7 +44,7 @@ describe('toggle replies widget for notes', () => {
}); });
it('should emit toggle event when the replies text clicked', () => { it('should emit toggle event when the replies text clicked', () => {
const spy = spyOn(vm, '$emit'); const spy = jest.spyOn(vm, '$emit');
vm.$el.querySelector('.js-replies-text').click(); vm.$el.querySelector('.js-replies-text').click();
...@@ -68,7 +68,7 @@ describe('toggle replies widget for notes', () => { ...@@ -68,7 +68,7 @@ describe('toggle replies widget for notes', () => {
}); });
it('should emit toggle event when the collapse replies text called', () => { it('should emit toggle event when the collapse replies text called', () => {
const spy = spyOn(vm, '$emit'); const spy = jest.spyOn(vm, '$emit');
vm.$el.querySelector('.js-collapse-replies').click(); vm.$el.querySelector('.js-collapse-replies').click();
......
...@@ -37,8 +37,8 @@ describe ContainerExpirationPoliciesHelper do ...@@ -37,8 +37,8 @@ describe ContainerExpirationPoliciesHelper do
expected_result = [ expected_result = [
{ key: '7d', label: '7 days until tags are automatically removed' }, { key: '7d', label: '7 days until tags are automatically removed' },
{ key: '14d', label: '14 days until tags are automatically removed' }, { key: '14d', label: '14 days until tags are automatically removed' },
{ key: '30d', label: '30 days until tags are automatically removed', default: true }, { key: '30d', label: '30 days until tags are automatically removed' },
{ key: '90d', label: '90 days until tags are automatically removed' } { key: '90d', label: '90 days until tags are automatically removed', default: true }
] ]
expect(helper.older_than_options).to eq(expected_result) expect(helper.older_than_options).to eq(expected_result)
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import initMRPage from '~/mr_notes/index'; import initMRPage from '~/mr_notes/index';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data'; import { userDataMock, notesDataMock, noteableDataMock } from '../../frontend/notes/mock_data';
import diffFileMockData from '../diffs/mock_data/diff_file'; import diffFileMockData from '../diffs/mock_data/diff_file';
export default function initVueMRPage() { export default function initVueMRPage() {
......
export * from '../../frontend/notes/helpers.js';
export * from '../../frontend/notes/mock_data.js';
...@@ -9,7 +9,14 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do ...@@ -9,7 +9,14 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do
let(:queue) { :test } let(:queue) { :test }
let(:worker_class) { worker.class } let(:worker_class) { worker.class }
let(:job) { {} } let(:job) { {} }
let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } } let(:default_labels) do
{ queue: queue.to_s,
worker: worker_class.to_s,
boundary: "",
external_dependencies: "no",
feature_category: "",
urgency: "low" }
end
shared_examples "a metrics client middleware" do shared_examples "a metrics client middleware" do
context "with mocked prometheus" do context "with mocked prometheus" do
......
...@@ -11,7 +11,14 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do ...@@ -11,7 +11,14 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do
let(:job) { {} } let(:job) { {} }
let(:job_status) { :done } let(:job_status) { :done }
let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } } let(:default_labels) do
{ queue: queue.to_s,
worker: worker_class.to_s,
boundary: "",
external_dependencies: "no",
feature_category: "",
urgency: "low" }
end
shared_examples "a metrics middleware" do shared_examples "a metrics middleware" do
context "with mocked prometheus" do context "with mocked prometheus" do
......
...@@ -1924,7 +1924,7 @@ describe Ci::Pipeline, :mailer do ...@@ -1924,7 +1924,7 @@ describe Ci::Pipeline, :mailer do
describe '#update_status' do describe '#update_status' do
context 'when pipeline is empty' do context 'when pipeline is empty' do
it 'updates does not change pipeline status' do it 'updates does not change pipeline status' do
expect(pipeline.statuses.latest.slow_composite_status).to be_nil expect(pipeline.statuses.latest.slow_composite_status(project: project)).to be_nil
expect { pipeline.update_legacy_status } expect { pipeline.update_legacy_status }
.to change { pipeline.reload.status } .to change { pipeline.reload.status }
......
...@@ -423,7 +423,7 @@ describe CommitStatus do ...@@ -423,7 +423,7 @@ describe CommitStatus do
end end
it 'returns a correct compound status' do it 'returns a correct compound status' do
expect(described_class.all.slow_composite_status).to eq 'running' expect(described_class.all.slow_composite_status(project: project)).to eq 'running'
end end
end end
...@@ -433,7 +433,7 @@ describe CommitStatus do ...@@ -433,7 +433,7 @@ describe CommitStatus do
end end
it 'returns status that indicates success' do it 'returns status that indicates success' do
expect(described_class.all.slow_composite_status).to eq 'success' expect(described_class.all.slow_composite_status(project: project)).to eq 'success'
end end
end end
...@@ -444,7 +444,7 @@ describe CommitStatus do ...@@ -444,7 +444,7 @@ describe CommitStatus do
end end
it 'returns status according to the scope' do it 'returns status according to the scope' do
expect(described_class.latest.slow_composite_status).to eq 'success' expect(described_class.latest.slow_composite_status(project: project)).to eq 'success'
end end
end end
end end
......
...@@ -6,7 +6,7 @@ describe HasStatus do ...@@ -6,7 +6,7 @@ describe HasStatus do
describe '.slow_composite_status' do describe '.slow_composite_status' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
subject { CommitStatus.slow_composite_status } subject { CommitStatus.slow_composite_status(project: nil) }
shared_examples 'build status summary' do shared_examples 'build status summary' do
context 'all successful' do context 'all successful' do
......
...@@ -4400,6 +4400,12 @@ describe User, :do_not_mock_admin_mode do ...@@ -4400,6 +4400,12 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to be expected_result } it { is_expected.to be expected_result }
end end
context 'when email is of Gitlab and is not confirmed' do
let(:user) { build(:user, email: 'test@gitlab.com', confirmed_at: nil) }
it { is_expected.to be false }
end
end end
describe '#current_highest_access_level' do describe '#current_highest_access_level' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment