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:
- 'spec/rubocop/cop/migration/update_large_table_spec.rb'
- 'spec/services/notification_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'
......@@ -16,6 +16,14 @@ import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_
import { ListType } from '../constants';
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({
components: {
BoardBlankState,
......@@ -54,6 +62,13 @@ export default Vue.extend({
type: String,
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() {
return {
......
This diff is collapsed.
......@@ -3,7 +3,6 @@ import Vue from 'vue';
import 'ee_else_ce/boards/models/issue';
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 initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
......@@ -65,7 +64,15 @@ export default () => {
issueBoardsApp = new Vue({
el: $boardApp,
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,
BoardAddIssuesModal,
BoardSettingsSidebar: () =>
......
......@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
Dropzone.autoDiscover = false;
......@@ -41,7 +42,6 @@ export default function dropzoneInput(form) {
let addFileToForm;
let updateAttachingMessage;
let isImage;
let getFilename;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
......@@ -235,17 +235,6 @@ export default function dropzoneInput(form) {
$(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 closeSpinner = () => $uploadingProgressContainer.addClass('hide');
......
......@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => {
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 {
},
},
mounted() {
if (!this.hasTruncatedDiffLines) {
if (this.isTextFile && !this.hasTruncatedDiffLines) {
this.fetchDiff();
}
},
......
......@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end
private
......
......@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end
private
......
......@@ -20,7 +20,7 @@ module ContainerExpirationPoliciesHelper
def older_than_options
ContainerExpirationPolicy.older_than_options.map do |key, val|
{ 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
......
......@@ -32,7 +32,7 @@ module Ci
end
def status
@status ||= statuses.latest.slow_composite_status
@status ||= statuses.latest.slow_composite_status(project: project)
end
def detailed_status(current_user)
......
......@@ -968,7 +968,7 @@ module Ci
def latest_builds_status
return 'failed' unless yaml_errors.blank?
statuses.latest.slow_composite_status || 'skipped'
statuses.latest.slow_composite_status(project: project) || 'skipped'
end
def keep_around_commits
......
......@@ -138,7 +138,7 @@ module Ci
end
def latest_stage_status
statuses.latest.slow_composite_status || 'skipped'
statuses.latest.slow_composite_status(project: project) || 'skipped'
end
end
end
......@@ -178,12 +178,12 @@ class CommitStatus < ApplicationRecord
select(:name)
end
def self.status_for_prior_stages(index)
before_stage(index).latest.slow_composite_status || 'success'
def self.status_for_prior_stages(index, project:)
before_stage(index).latest.slow_composite_status(project: project) || 'success'
end
def self.status_for_names(names)
where(name: names).latest.slow_composite_status || 'success'
def self.status_for_names(names, project:)
where(name: names).latest.slow_composite_status(project: project) || 'success'
end
def self.update_as_processed!
......
......@@ -65,8 +65,8 @@ module HasStatus
# This method performs expensive calculation of status:
# 1. By plucking all related objects,
# 2. Or executes expensive SQL query
def slow_composite_status
if Feature.enabled?(:ci_composite_status, default_enabled: false)
def slow_composite_status(project:)
if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
Gitlab::Ci::Status::Composite
.new(all, with_allow_failure: columns_hash.key?('allow_failure'))
.status
......
......@@ -1689,7 +1689,7 @@ class User < ApplicationRecord
def gitlab_employee?
strong_memoize(:gitlab_employee) do
if Gitlab.com?
Mail::Address.new(email).domain == "gitlab.com"
Mail::Address.new(email).domain == "gitlab.com" && confirmed?
else
false
end
......
......@@ -89,11 +89,11 @@ module Ci
end
def status_for_prior_stages(index)
pipeline.processables.status_for_prior_stages(index)
pipeline.processables.status_for_prior_stages(index, project: pipeline.project)
end
def status_for_build_needs(needs)
pipeline.processables.status_for_names(needs)
pipeline.processables.status_for_names(needs, project: pipeline.project)
end
# rubocop: disable CodeReuse/ActiveRecord
......
- board = local_assigns.fetch(:board, nil)
- 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_container = true
- @content_class = "issue-boards-content js-focus-mode-board"
......@@ -22,6 +26,8 @@
%board{ "v-cloak" => "true",
"v-for" => "list in state.lists",
"ref" => "board",
":can-admin-list" => can_admin_list,
":group-id" => group_id,
":list" => "list",
":disabled" => "disabled",
":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" }',
":data-id" => "list.id", data: { qa_selector: "board_list" } }
.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 (
cadence character varying(12) DEFAULT '7d'::character varying NOT NULL,
older_than character varying(12),
keep_n integer,
enabled boolean DEFAULT false NOT NULL
enabled boolean DEFAULT true NOT NULL
);
CREATE TABLE public.container_repositories (
......@@ -12926,5 +12926,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326145443
20200330074719
20200330132913
20200331220930
\.
......@@ -436,6 +436,24 @@ Keyset-based pagination is only supported for selected resources and ordering op
| ------------------------- | -------------------------- |
| [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
If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_PATH` is
......
......@@ -98,6 +98,8 @@ Complementary reads:
- [Application limits](application_limits.md)
- [Redis guidelines](redis.md)
- [Rails initializers](rails_initializers.md)
- [Code comments](code_comments.md)
- [Renaming features](renaming_features.md)
## Performance guides
......@@ -150,9 +152,7 @@ Complementary reads:
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Database Debugging and Troubleshooting](database_debugging.md)
- [Query Count Limits](query_count_limits.md)
- [Code comments](code_comments.md)
- [Creating enums](creating_enums.md)
- [Renaming features](renaming_features.md)
### Case studies
......
......@@ -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
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
[installing GitLab under a relative URL]: https://docs.gitlab.com/ee/install/relative_url.html
......
......@@ -79,6 +79,7 @@ the following preparations into account.
- Include either a rollback procedure or describe how to rollback changes.
- 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.
- 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
......
......@@ -171,7 +171,7 @@ lock allow the database to process other statements.
### Examples
Removing a column:
**Removing a column:**
```ruby
include Gitlab::Database::MigrationHelpers
......@@ -189,7 +189,7 @@ def down
end
```
Removing a foreign key:
**Removing a foreign key:**
```ruby
include Gitlab::Database::MigrationHelpers
......@@ -207,7 +207,7 @@ def down
end
```
Changing default value for a column:
**Changing default value for a column:**
```ruby
include Gitlab::Database::MigrationHelpers
......@@ -225,6 +225,88 @@ def down
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
The `with_lock_retries` helper method can be used when you normally use
......
......@@ -247,7 +247,17 @@ create the actual RDS instance.
![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:
......@@ -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. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier.
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. For **Database authentication**, select **Password authentication**.
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.
1. Leave the rest of the settings to their default values or edit to your liking.
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
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:
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.
### 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
### "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.
![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
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.
......
......@@ -9,7 +9,13 @@ module Gitlab
private
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)
labels[:urgency] = worker_class.get_urgency.to_s
......
......@@ -27,6 +27,7 @@
### Environment variables
RAILS_ENV="production"
USE_UNICORN=""
SIDEKIQ_WORKERS=1
# Script variable names should be lower-case not to conflict with
# internal /bin/sh variables such as PATH, EDITOR or SHELL.
......@@ -36,7 +37,6 @@ pid_path="$app_root/tmp/pids"
socket_path="$app_root/tmp/sockets"
rails_socket="$socket_path/gitlab.socket"
web_server_pid_path="$pid_path/unicorn.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid"
gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
......@@ -74,6 +74,11 @@ else
use_web_server="unicorn"
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
......@@ -295,7 +300,7 @@ start_gitlab() {
if [ "$sidekiq_status" = "0" ]; then
echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting"
else
RAILS_ENV=$RAILS_ENV bin/background_jobs start &
RAILS_ENV=$RAILS_ENV SIDEKIQ_WORKERS=$SIDEKIQ_WORKERS bin/background_jobs start &
fi
if [ "$gitlab_workhorse_status" = "0" ]; then
......@@ -354,7 +359,7 @@ stop_gitlab() {
fi
if [ "$sidekiq_status" = "0" ]; then
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
if [ "$gitlab_workhorse_status" = "0" ]; then
echo "Shutting down GitLab Workhorse"
......@@ -458,7 +463,7 @@ reload_gitlab(){
echo "Done."
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
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'
it 'saves expiration policy submit the form' do
within '#js-registry-policies' 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('Every day', from: 'Expiration schedule:')
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 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', () => {
label: {
id: 1,
title: 'test',
color: 'red',
color: '#ff0000',
text_color: 'white',
},
});
......@@ -64,8 +64,7 @@ describe('List model', () => {
expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
expect(list.label.color).toBe('red');
expect(list.label.textColor).toBe('white');
expect(list.label).toEqual(listObj.label);
});
});
......
......@@ -15,7 +15,7 @@ export const listObj = {
label: {
id: 5000,
title: 'Test',
color: 'red',
color: '#ff0000',
description: 'testing;',
textColor: 'white',
},
......@@ -30,7 +30,7 @@ export const listObjDuplicate = {
label: {
id: listObj.label.id,
title: 'Test',
color: 'red',
color: '#ff0000',
description: 'testing;',
},
};
......
import fileUpload from '~/lib/utils/file_upload';
import fileUpload, { getFilename } from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
......@@ -62,3 +62,15 @@ describe('File upload', () => {
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';
import createStore from '~/notes/stores';
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';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
......
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers';
import { mount } from '@vue/test-utils';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { createStore } from '~/mr_notes/stores';
......@@ -8,25 +7,17 @@ const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
describe('diff_with_note', () => {
let store;
let vm;
const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
const diffDiscussion = diffDiscussionMock;
const Component = Vue.extend(DiffWithNote);
const props = {
discussion: diffDiscussion,
};
let wrapper;
const selectors = {
get container() {
return vm.$el;
},
get diffTable() {
return this.container.querySelector('.diff-content table');
return wrapper.find('.diff-content table');
},
get diffRows() {
return this.container.querySelectorAll('.diff-content .line_holder');
return wrapper.findAll('.diff-content .line_holder');
},
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', () => {
describe('text diff', () => {
beforeEach(() => {
vm = mountComponentWithStore(Component, { props, store });
const diffDiscussion = getJSONFixture(discussionFixture)[0];
wrapper = mount(DiffWithNote, {
propsData: {
discussion: diffDiscussion,
},
store,
});
});
it('removes trailing "+" char', () => {
const richText = vm.$el.querySelectorAll('.line_holder')[4].querySelector('.line_content')
.textContent[0];
const richText = wrapper.vm.$el
.querySelectorAll('.line_holder')[4]
.querySelector('.line_content').textContent[0];
expect(richText).not.toEqual('+');
});
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('-');
});
it('shows text diff', () => {
expect(selectors.container).toHaveClass('text-file');
expect(selectors.diffTable).toExist();
expect(wrapper.classes('text-file')).toBe(true);
expect(selectors.diffTable.exists()).toBe(true);
});
it('shows diff lines', () => {
......@@ -70,20 +69,18 @@ describe('diff_with_note', () => {
});
it('shows notes row', () => {
expect(selectors.noteRow).toExist();
expect(selectors.noteRow.exists()).toBe(true);
});
});
describe('image diff', () => {
beforeEach(() => {
const imageDiffDiscussionMock = getJSONFixture(imageDiscussionFixture)[0];
props.discussion = imageDiffDiscussionMock;
const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0];
wrapper = mount(DiffWithNote, { propsData: { discussion: imageDiscussion }, store });
});
it('shows image diff', () => {
vm = mountComponentWithStore(Component, { props, store });
expect(selectors.diffTable).not.toExist();
expect(selectors.diffTable.exists()).toBe(false);
});
});
});
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 { 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 { TEST_HOST } from 'jest/helpers/test_constants';
const localVue = createLocalVue();
localVue.use(Vuex);
const DISCUSSION_PATH = `${TEST_HOST}/example`;
describe('DiscussionFilter component', () => {
let vm;
let wrapper;
let store;
let eventHub;
let mock;
const mountComponent = () => {
store = createStore();
const filterDiscussion = jest.fn();
const mountComponent = () => {
const discussions = [
{
...discussionMock,
......@@ -20,83 +34,101 @@ describe('DiscussionFilter component', () => {
notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
},
];
const Component = Vue.extend(DiscussionFilter);
const selectedValue = DISCUSSION_FILTERS_DEFAULT_VALUE;
const props = { filters: discussionFiltersMock, selectedValue };
const defaultStore = { ...notesModule() };
store = new Vuex.Store({
...defaultStore,
actions: {
...defaultStore.actions,
filterDiscussion,
},
});
store.state.notesData.discussionsPath = DISCUSSION_PATH;
store.state.discussions = discussions;
return mountComponentWithStore(Component, {
el: null,
return mount(DiscussionFilter, {
store,
props,
propsData: {
filters: discussionFiltersMock,
selectedValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
},
localVue,
});
};
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;
vm = mountComponent();
wrapper = mountComponent();
});
afterEach(() => {
vm.$destroy();
wrapper.vm.$destroy();
mock.restore();
});
it('renders the all filters', () => {
expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(
discussionFiltersMock.length,
);
expect(wrapper.findAll('.dropdown-menu li').length).toBe(discussionFiltersMock.length);
});
it('renders the default selected item', () => {
expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(
discussionFiltersMock[0].title,
);
expect(
wrapper
.find('#discussion-filter-dropdown')
.text()
.trim(),
).toBe(discussionFiltersMock[0].title);
});
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`,
);
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', () => {
const filterItem = vm.$el.querySelector(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
);
spyOn(vm, 'filterDiscussion');
filterItem.click();
wrapper
.find(`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`)
.trigger('click');
expect(vm.filterDiscussion).not.toHaveBeenCalled();
expect(filterDiscussion).not.toHaveBeenCalled();
});
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`,
);
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', () => {
const filterItem = vm.$el.querySelector(
const filterItem = wrapper.find(
`.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', () => {
const defaultFilter = vm.$el.querySelector(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
const defaultFilter = wrapper.findAll(
`.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', () => {
......@@ -108,7 +140,7 @@ describe('DiscussionFilter component', () => {
currentTab: 'show',
};
vm = mountComponent();
wrapper = mountComponent();
});
afterEach(() => {
......@@ -118,8 +150,8 @@ describe('DiscussionFilter component', () => {
it('only renders when discussion tab is active', done => {
eventHub.$emit('MergeRequestTabChange', 'commit');
vm.$nextTick(() => {
expect(vm.$el.querySelector).toBeUndefined();
wrapper.vm.$nextTick(() => {
expect(wrapper.isEmpty()).toBe(true);
done();
});
});
......@@ -132,54 +164,54 @@ describe('DiscussionFilter component', () => {
it('updates the filter when the URL links to a note', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.currentValue = discussionFiltersMock[2].value;
vm.handleLocationHash();
wrapper.vm.currentValue = discussionFiltersMock[2].value;
wrapper.vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
done();
});
});
it('does not update the filter when the current filter is "Show all activity"', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.handleLocationHash();
wrapper.vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
done();
});
});
it('only updates filter when the URL links to a note', done => {
window.location.hash = `testing123`;
vm.handleLocationHash();
wrapper.vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
done();
});
});
it('fetches discussions when there is a hash', done => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
vm.currentValue = discussionFiltersMock[2].value;
spyOn(vm, 'selectFilter');
vm.handleLocationHash();
wrapper.vm.currentValue = discussionFiltersMock[2].value;
jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
wrapper.vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.selectFilter).toHaveBeenCalled();
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.selectFilter).toHaveBeenCalled();
done();
});
});
it('does not fetch discussions when there is no hash', done => {
window.location.hash = '';
spyOn(vm, 'selectFilter');
vm.handleLocationHash();
jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
wrapper.vm.handleLocationHash();
vm.$nextTick(() => {
expect(vm.selectFilter).not.toHaveBeenCalled();
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
done();
});
});
......
import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
import { TEST_HOST } from 'jest/helpers/test_constants';
describe('note_awards_list component', () => {
let store;
let vm;
let awardsMock;
let mock;
const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
mock.onPost(toggleAwardPath).reply(200, '');
const Component = Vue.extend(awardsNote);
store = createStore();
......@@ -32,12 +42,13 @@ describe('note_awards_list component', () => {
noteAuthorId: 2,
noteId: '545',
canAwardEmoji: true,
toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
toggleAwardPath,
},
}).$mount();
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
......@@ -49,8 +60,8 @@ describe('note_awards_list component', () => {
});
it('should be possible to remove awarded emoji', () => {
spyOn(vm, 'handleAward').and.callThrough();
spyOn(vm, 'toggleAwardRequest').and.callThrough();
jest.spyOn(vm, 'handleAward');
jest.spyOn(vm, 'toggleAwardRequest');
vm.$el.querySelector('.js-awards-block button').click();
expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
......@@ -138,7 +149,7 @@ describe('note_awards_list component', () => {
});
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();
......
......@@ -4,6 +4,10 @@ import NoteForm from '~/notes/components/note_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
jest.mock('~/lib/utils/autosave');
describe('issue_note_form component', () => {
const dummyAutosaveKey = 'some-autosave-key';
const dummyDraft = 'dummy draft content';
......@@ -23,7 +27,7 @@ describe('issue_note_form component', () => {
};
beforeEach(() => {
spyOnDependency(NoteForm, 'getDraft').and.callFake(key => {
getDraft.mockImplementation(key => {
if (key === dummyAutosaveKey) {
return dummyDraft;
}
......@@ -55,19 +59,15 @@ describe('issue_note_form component', () => {
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({
...props,
noteId: '',
});
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.vm.noteHash).toBe('#');
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.noteHash).toBe('#');
});
});
});
......@@ -76,7 +76,7 @@ describe('issue_note_form component', () => {
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({
...props,
isEditing: true,
......@@ -86,21 +86,17 @@ describe('issue_note_form component', () => {
const message =
'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
wrapper.vm
.$nextTick()
.then(() => {
const conflictWarning = wrapper.find('.js-conflict-edit-warning');
expect(conflictWarning.exists()).toBe(true);
expect(
conflictWarning
.text()
.replace(/\s+/g, ' ')
.trim(),
).toBe(message);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
const conflictWarning = wrapper.find('.js-conflict-edit-warning');
expect(conflictWarning.exists()).toBe(true);
expect(
conflictWarning
.text()
.replace(/\s+/g, ' ')
.trim(),
).toBe(message);
});
});
});
......@@ -136,7 +132,7 @@ describe('issue_note_form component', () => {
describe('up', () => {
it('should ender edit mode', () => {
// TODO: do not spy on vm
spyOn(wrapper.vm, 'editMyLastNote').and.callThrough();
jest.spyOn(wrapper.vm, 'editMyLastNote');
textarea.trigger('keydown.up');
......@@ -164,61 +160,50 @@ describe('issue_note_form component', () => {
});
describe('actions', () => {
it('should be possible to cancel', done => {
it('should be possible to cancel', () => {
// TODO: do not spy on vm
spyOn(wrapper.vm, 'cancelHandler').and.callThrough();
jest.spyOn(wrapper.vm, 'cancelHandler');
wrapper.setProps({
...props,
isEditing: true,
});
wrapper.vm
.$nextTick()
.then(() => {
const cancelButton = wrapper.find('.note-edit-cancel');
cancelButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
const cancelButton = wrapper.find('.note-edit-cancel');
cancelButton.trigger('click');
expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
});
});
it('should be possible to update the note', done => {
it('should be possible to update the note', () => {
wrapper.setProps({
...props,
isEditing: true,
});
wrapper.vm
.$nextTick()
.then(() => {
const textarea = wrapper.find('textarea');
textarea.setValue('Foo');
const saveButton = wrapper.find('.js-vue-issue-save');
saveButton.trigger('click');
expect(wrapper.vm.isSubmitting).toEqual(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
const textarea = wrapper.find('textarea');
textarea.setValue('Foo');
const saveButton = wrapper.find('.js-vue-issue-save');
saveButton.trigger('click');
expect(wrapper.vm.isSubmitting).toBe(true);
});
});
});
});
describe('with autosaveKey', () => {
describe('with draft', () => {
beforeEach(done => {
beforeEach(() => {
Object.assign(props, {
noteBody: '',
autosaveKey: dummyAutosaveKey,
});
wrapper = createComponentWrapper();
wrapper.vm
.$nextTick()
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick();
});
it('displays the draft in textarea', () => {
......@@ -229,17 +214,14 @@ describe('issue_note_form component', () => {
});
describe('without draft', () => {
beforeEach(done => {
beforeEach(() => {
Object.assign(props, {
noteBody: '',
autosaveKey: 'some key without draft',
});
wrapper = createComponentWrapper();
wrapper.vm
.$nextTick()
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick();
});
it('leaves the textarea empty', () => {
......@@ -250,7 +232,6 @@ describe('issue_note_form component', () => {
});
it('updates the draft if textarea content changes', () => {
const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub();
Object.assign(props, {
noteBody: '',
autosaveKey: dummyAutosaveKey,
......@@ -261,7 +242,7 @@ describe('issue_note_form component', () => {
textarea.setValue(dummyContent);
expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
});
});
});
......@@ -12,8 +12,8 @@ import {
loggedOutnoteableData,
userDataMock,
} from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file';
import { trimText } from '../../helpers/text_helper';
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import { trimText } from 'helpers/text_helper';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
......@@ -47,27 +47,24 @@ describe('noteable_discussion component', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
it('should render thread header', done => {
it('should render thread header', () => {
const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
discussion.expanded = false;
wrapper.setProps({ discussion });
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.find('.discussion-header').exists()).toBe(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.discussion-header').exists()).toBe(true);
});
});
describe('actions', () => {
it('should toggle reply form', done => {
it('should toggle reply form', () => {
const replyPlaceholder = wrapper.find(ReplyPlaceholder);
wrapper.vm
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.vm.isReplying).toEqual(false);
......@@ -89,9 +86,7 @@ describe('noteable_discussion component', () => {
expect(noteFormProps.line).toBe(null);
expect(noteFormProps.saveButtonTitle).toBe('Comment');
expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`);
})
.then(done)
.catch(done.fail);
});
});
it('does not render jump to thread button', () => {
......@@ -115,7 +110,7 @@ describe('noteable_discussion component', () => {
});
describe('for unresolved thread', () => {
beforeEach(done => {
beforeEach(() => {
const discussion = {
...getJSONFixture(discussionWithTwoUnresolvedNotes)[0],
expanded: true,
......@@ -131,10 +126,7 @@ describe('noteable_discussion component', () => {
wrapper.setProps({ discussion });
wrapper.vm
.$nextTick()
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick();
});
it('displays a button to resolve with issue', () => {
......
......@@ -86,7 +86,7 @@ describe('issue_note', () => {
it('prevents note preview xss', done => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
const alertSpy = spyOn(window, 'alert');
const alertSpy = jest.spyOn(window, 'alert');
store.hotUpdate({
actions: {
updateNote() {},
......@@ -96,11 +96,11 @@ describe('issue_note', () => {
noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
setTimeout(() => {
setImmediate(() => {
expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.vm.note.note_html).toEqual(escape(noteBody));
done();
}, 0);
});
});
describe('cancel edit', () => {
......
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 { note } from '../mock_data';
......@@ -44,7 +44,7 @@ describe('toggle replies widget for notes', () => {
});
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();
......@@ -68,7 +68,7 @@ describe('toggle replies widget for notes', () => {
});
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();
......
......@@ -37,8 +37,8 @@ describe ContainerExpirationPoliciesHelper do
expected_result = [
{ key: '7d', label: '7 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: '90d', label: '90 days until tags are automatically removed' }
{ key: '30d', label: '30 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)
......
import MockAdapter from 'axios-mock-adapter';
import initMRPage from '~/mr_notes/index';
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';
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
let(:queue) { :test }
let(:worker_class) { worker.class }
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
context "with mocked prometheus" do
......
......@@ -11,7 +11,14 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do
let(:job) { {} }
let(:job_status) { :done }
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
context "with mocked prometheus" do
......
......@@ -1924,7 +1924,7 @@ describe Ci::Pipeline, :mailer do
describe '#update_status' do
context 'when pipeline is empty' 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 }
.to change { pipeline.reload.status }
......
......@@ -423,7 +423,7 @@ describe CommitStatus do
end
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
......@@ -433,7 +433,7 @@ describe CommitStatus do
end
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
......@@ -444,7 +444,7 @@ describe CommitStatus do
end
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
......
......@@ -6,7 +6,7 @@ describe HasStatus do
describe '.slow_composite_status' do
using RSpec::Parameterized::TableSyntax
subject { CommitStatus.slow_composite_status }
subject { CommitStatus.slow_composite_status(project: nil) }
shared_examples 'build status summary' do
context 'all successful' do
......
......@@ -4400,6 +4400,12 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to be expected_result }
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
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