Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-22 18:08:21 +00:00
parent d6e421b21e
commit 1cf95147ea
75 changed files with 865 additions and 782 deletions

View file

@ -111,28 +111,10 @@ export default {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
logsPath: {
type: String,
required: false,
default: invalidUrl,
},
defaultBranch: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentsEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
@ -153,10 +135,6 @@ export default {
type: String,
required: true,
},
currentEnvironmentName: {
type: String,
required: true,
},
customMetricsAvailable: {
type: Boolean,
required: false,
@ -172,21 +150,6 @@ export default {
required: false,
default: invalidUrl,
},
dashboardEndpoint: {
type: String,
required: false,
default: invalidUrl,
},
dashboardsEndpoint: {
type: String,
required: false,
default: invalidUrl,
},
currentDashboard: {
type: String,
required: false,
default: '',
},
smallEmptyState: {
type: Boolean,
required: false,
@ -228,6 +191,8 @@ export default {
'expandedPanel',
'variables',
'isUpdatingStarredValue',
'currentDashboard',
'currentEnvironmentName',
]),
...mapGetters('monitoringDashboard', [
'selectedDashboard',
@ -281,16 +246,6 @@ export default {
},
},
created() {
this.setInitialState({
metricsEndpoint: this.metricsEndpoint,
deploymentsEndpoint: this.deploymentsEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
dashboardsEndpoint: this.dashboardsEndpoint,
currentDashboard: this.currentDashboard,
projectPath: this.projectPath,
logsPath: this.logsPath,
currentEnvironmentName: this.currentEnvironmentName,
});
window.addEventListener('keyup', this.onKeyup);
},
destroyed() {
@ -310,7 +265,6 @@ export default {
'fetchData',
'fetchDashboardData',
'setGettingStartedEmptyState',
'setInitialState',
'setPanelGroupMetrics',
'filterEnvironments',
'setExpandedPanel',

View file

@ -140,7 +140,6 @@ export const dateFormats = {
* Currently used in `receiveMetricsDashboardSuccess` action.
*/
export const endpointKeys = [
'metricsEndpoint',
'deploymentsEndpoint',
'dashboardEndpoint',
'dashboardsEndpoint',

View file

@ -3,7 +3,7 @@ import { GlToast } from '@gitlab/ui';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import store from './stores';
import { createStore } from './stores';
Vue.use(GlToast);
@ -13,6 +13,31 @@ export default (props = {}) => {
if (el && el.dataset) {
const [currentDashboard] = getParameterValues('dashboard');
const {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
projectPath,
logsPath,
currentEnvironmentName,
...dataProps
} = el.dataset;
const store = createStore({
currentDashboard,
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
projectPath,
logsPath,
currentEnvironmentName,
});
// HTML attributes are always strings, parse other types.
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
// eslint-disable-next-line no-new
new Vue({
el,
@ -20,11 +45,7 @@ export default (props = {}) => {
render(createElement) {
return createElement(Dashboard, {
props: {
...el.dataset,
currentDashboard,
customMetricsAvailable: parseBoolean(el.dataset.customMetricsAvailable),
prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable),
hasMetrics: parseBoolean(el.dataset.hasMetrics),
...dataProps,
...props,
},
});

View file

@ -314,8 +314,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
export const fetchAnnotations = ({ state, dispatch }) => {
const { start } = convertToFixedRange(state.timeRange);
const dashboardPath =
state.currentDashboard === '' ? DEFAULT_DASHBOARD_PATH : state.currentDashboard;
const dashboardPath = state.currentDashboard || DEFAULT_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getAnnotations,

View file

@ -15,11 +15,15 @@ export const monitoringDashboard = {
state,
};
export const createStore = () =>
export const createStore = (initState = {}) =>
new Vuex.Store({
modules: {
monitoringDashboard,
monitoringDashboard: {
...monitoringDashboard,
state: {
...state(),
...initState,
},
},
},
});
export default createStore();

View file

@ -2,9 +2,9 @@ import invalidUrl from '~/lib/utils/invalid_url';
export default () => ({
// API endpoints
metricsEndpoint: null,
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
dashboardsEndpoint: invalidUrl,
// Dashboard request parameters
timeRange: null,
@ -46,6 +46,7 @@ export default () => ({
environments: [],
environmentsSearchTerm: '',
environmentsLoading: false,
currentEnvironmentName: null,
// GitLab paths to other pages
projectPath: null,

View file

@ -1,5 +1,6 @@
// eslint-disable-next-line import/prefer-default-export
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
};
export const LIST_BUFFER_SIZE = 5;

View file

@ -3,15 +3,20 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import LabelItem from './label_item.vue';
import { LIST_BUFFER_SIZE } from './constants';
export default {
LIST_BUFFER_SIZE,
components: {
GlLoadingIcon,
GlButton,
GlSearchBoxByType,
GlLink,
SmartVirtualList,
LabelItem,
},
data() {
@ -139,10 +144,18 @@ export default {
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
</div>
<div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
<ul class="list-unstyled mb-0">
<smart-virtual-list
:length="visibleLabels.length"
:remain="$options.LIST_BUFFER_SIZE"
:size="$options.LIST_BUFFER_SIZE"
wclass="list-unstyled mb-0"
wtag="ul"
class="h-100"
>
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
<label-item
:label="label"
:is-label-set="label.set"
:highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)"
/>
@ -150,7 +163,7 @@ export default {
<li v-show="!visibleLabels.length" class="p-2 text-center">
{{ __('No matching results') }}
</li>
</ul>
</smart-virtual-list>
</div>
<div v-if="isDropdownVariantSidebar" class="dropdown-footer">
<ul class="list-unstyled">
@ -162,9 +175,9 @@ export default {
>
</li>
<li>
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{
footerManageLabelTitle
}}</gl-link>
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
{{ footerManageLabelTitle }}
</gl-link>
</li>
</ul>
</div>

View file

@ -11,6 +11,10 @@ export default {
type: Object,
required: true,
},
isLabelSet: {
type: Boolean,
required: true,
},
highlight: {
type: Boolean,
required: false,
@ -19,7 +23,7 @@ export default {
},
data() {
return {
isSet: this.label.set,
isSet: this.isLabelSet,
};
},
computed: {
@ -29,6 +33,16 @@ export default {
};
},
},
watch: {
/**
* This watcher assures that if user used
* `Enter` key to set/unset label, changes
* are reflected here too.
*/
isLabelSet(value) {
this.isSet = value;
},
},
methods: {
handleClick() {
this.isSet = !this.isSet;

View file

@ -28,6 +28,8 @@ module Types
description: 'Timestamp of when the merge request was created'
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of when the merge request was last updated'
field :merged_at, Types::TimeType, null: true, complexity: 5,
description: 'Timestamp of when the merge request was merged, null if not merged'
field :source_project, Types::ProjectType, null: true,
description: 'Source project of the merge request'
field :target_project, Types::ProjectType, null: false,
@ -109,6 +111,8 @@ module Types
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the merge request'
field :author, Types::UserType, null: true,
description: 'User who created this merge request'
field :participants, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Participants in the merge request'
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,

View file

@ -3,7 +3,6 @@
set -e
cd $(dirname $0)/..
app_root=$(pwd)
case "$USE_WEB_SERVER" in
puma|"") # and the "" defines default

View file

@ -0,0 +1,5 @@
---
title: Add missing Merge Request fields
merge_request: 30935
author:
type: added

View file

@ -32,3 +32,6 @@ providers:
- [Shibboleth](../../integration/shibboleth.md)
- [Smartcard](smartcard.md) **(PREMIUM ONLY)**
- [Twitter](../../integration/twitter.md)
NOTE: **Note:**
UltraAuth has removed their software which supports OmniAuth integration. We have therefore removed all references to UltraAuth integration.

View file

@ -116,7 +116,7 @@ expose the Registry on a port so that you can reuse the existing GitLab TLS
certificate.
Assuming that the GitLab domain is `https://gitlab.example.com` and the port the
Registry is exposed to the outside world is `4567`, here is what you need to set
Registry is exposed to the outside world is `5050`, here is what you need to set
in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed
GitLab from source respectively.
@ -130,7 +130,7 @@ otherwise you will run into conflicts.
path to the existing TLS certificate and key used by GitLab:
```ruby
registry_external_url 'https://gitlab.example.com:4567'
registry_external_url 'https://gitlab.example.com:5050'
```
Note how the `registry_external_url` is listening on HTTPS under the
@ -151,7 +151,7 @@ otherwise you will run into conflicts.
1. Validate using:
```shell
openssl s_client -showcerts -servername gitlab.example.com -connect gitlab.example.com:443 > cacert.pem
openssl s_client -showcerts -servername gitlab.example.com -connect gitlab.example.com:5050 > cacert.pem
```
NOTE: **Note:**
@ -166,7 +166,7 @@ If your certificate provider provides the CA Bundle certificates, append them to
registry:
enabled: true
host: gitlab.example.com
port: 4567
port: 5050
```
1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
@ -176,7 +176,7 @@ Users should now be able to login to the Container Registry with their GitLab
credentials using:
```shell
docker login gitlab.example.com:4567
docker login gitlab.example.com:5050
```
### Configure Container Registry under its own domain
@ -1062,7 +1062,7 @@ A user attempted to enable an S3-backed Registry. The `docker login` step went
fine. However, when pushing an image, the output showed:
```plaintext
The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test/docker-image]
The push refers to a repository [s3-testing.myregistry.com:5050/root/docker-test/docker-image]
dc5e59c14160: Pushing [==================================================>] 14.85 kB
03c20c1a019a: Pushing [==================================================>] 2.048 kB
a08f14ef632e: Pushing [==================================================>] 2.048 kB
@ -1154,8 +1154,8 @@ Now that we have mitmproxy and Docker running, we can attempt to login and push
a container image. You may need to run as root to do this. For example:
```shell
docker login s3-testing.myregistry.com:4567
docker push s3-testing.myregistry.com:4567/root/docker-test/docker-image
docker login s3-testing.myregistry.com:5050
docker push s3-testing.myregistry.com:5050/root/docker-test/docker-image
```
In the example above, we see the following trace on the mitmproxy window:

View file

@ -5767,6 +5767,11 @@ type MergeRequest implements Noteable {
last: Int
): UserConnection
"""
User who created this merge request
"""
author: User
"""
Timestamp of when the merge request was created
"""
@ -5917,6 +5922,11 @@ type MergeRequest implements Noteable {
"""
mergeableDiscussionsState: Boolean
"""
Timestamp of when the merge request was merged, null if not merged
"""
mergedAt: Time
"""
The milestone of the merge request
"""

View file

@ -16085,6 +16085,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "author",
"description": "User who created this merge request",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of when the merge request was created",
@ -16499,6 +16513,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergedAt",
"description": "Timestamp of when the merge request was merged, null if not merged",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "milestone",
"description": "The milestone of the merge request",

View file

@ -860,6 +860,7 @@ Autogenerated return type of MarkAsSpamSnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork |
| `author` | User | User who created this merge request |
| `createdAt` | Time! | Timestamp of when the merge request was created |
| `defaultMergeCommitMessage` | String | Default merge commit message of the merge request |
| `description` | String | Description of the merge request (Markdown rendered as HTML for caching) |
@ -880,6 +881,7 @@ Autogenerated return type of MarkAsSpamSnippet
| `mergeStatus` | String | Status of the merge request |
| `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) |
| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
| `mergedAt` | Time | Timestamp of when the merge request was merged, null if not merged |
| `milestone` | Milestone | The milestone of the merge request |
| `project` | Project! | Alias for target_project |
| `projectId` | Int! | ID of the merge request project |

View file

@ -55,199 +55,7 @@ You can view the exact JSON payload sent to GitLab Inc. in the administration pa
1. Expand the **Usage statistics** section.
1. Click the **Preview payload** button.
<details>
<summary>Click to view an example of the payload structure.</summary>
```json
{
"uuid": "0000000-0000-0000-0000-000000000000",
"hostname": "example.com",
"version": "12.10.0-pre",
"installation_type": "omnibus-gitlab",
"active_user_count": 999,
"recorded_at": "2020-04-17T07:43:54.162+00:00",
"edition": "EEU",
"license_md5": "00000000000000000000000000000000",
"license_id": null,
"historical_max_users": 999,
"licensee": {
"Name": "ABC, Inc.",
"Email": "email@example.com",
"Company": "ABC, Inc."
},
"license_user_count": 999,
"license_starts_at": "2020-01-01",
"license_expires_at": "2021-01-01",
"license_plan": "ultimate",
"license_add_ons": {
},
"license_trial": false,
"counts": {
"assignee_lists": 999,
"boards": 999,
"ci_builds": 999,
...
},
"container_registry_enabled": true,
"dependency_proxy_enabled": false,
"gitlab_shared_runners_enabled": true,
"gravatar_enabled": true,
"influxdb_metrics_enabled": true,
"ldap_enabled": false,
"mattermost_enabled": false,
"omniauth_enabled": true,
"prometheus_metrics_enabled": false,
"reply_by_email_enabled": "incoming+%{key}@incoming.gitlab.com",
"signup_enabled": true,
"web_ide_clientside_preview_enabled": true,
"ingress_modsecurity_enabled": true,
"projects_with_expiration_policy_disabled": 999,
"projects_with_expiration_policy_enabled": 999,
...
"elasticsearch_enabled": true,
"license_trial_ends_on": null,
"geo_enabled": false,
"git": {
"version": {
"major": 2,
"minor": 26,
"patch": 1
}
},
"gitaly": {
"version": "12.10.0-rc1-93-g40980d40",
"servers": 56,
"filesystems": [
"EXT_2_3_4"
]
},
"gitlab_pages": {
"enabled": true,
"version": "1.17.0"
},
"database": {
"adapter": "postgresql",
"version": "9.6.15"
},
"app_server": {
"type": "console"
},
"avg_cycle_analytics": {
"issue": {
"average": 999,
"sd": 999,
"missing": 999
},
"plan": {
"average": null,
"sd": 999,
"missing": 999
},
"code": {
"average": null,
"sd": 999,
"missing": 999
},
"test": {
"average": null,
"sd": 999,
"missing": 999
},
"review": {
"average": null,
"sd": 999,
"missing": 999
},
"staging": {
"average": null,
"sd": 999,
"missing": 999
},
"production": {
"average": null,
"sd": 999,
"missing": 999
},
"total": 999
},
"usage_activity_by_stage": {
"configure": {
"project_clusters_enabled": 999,
...
},
"create": {
"merge_requests": 999,
...
},
"manage": {
"events": 999,
...
},
"monitor": {
"clusters": 999,
...
},
"package": {
"projects_with_packages": 999
},
"plan": {
"issues": 999,
...
},
"release": {
"deployments": 999,
...
},
"secure": {
"user_container_scanning_jobs": 999,
...
},
"verify": {
"ci_builds": 999,
...
}
},
"usage_activity_by_stage_monthly": {
"configure": {
"project_clusters_enabled": 999,
...
},
"create": {
"merge_requests": 999,
...
},
"manage": {
"events": 999,
...
},
"monitor": {
"clusters": 999,
...
},
"package": {
"projects_with_packages": 999
},
"plan": {
"issues": 999,
...
},
"release": {
"deployments": 999,
...
},
"secure": {
"user_container_scanning_jobs": 999,
...
},
"verify": {
"ci_builds": 999,
...
}
}
}
```
</details>
For an example payload, see [Example Usage Ping payload](#example-usage-ping-payload).
## Disable Usage Ping
@ -484,3 +292,196 @@ Examples of query optimization work:
### 4. Ask for a Telemetry Review
On GitLab.com, we have DangerBot setup to monitor Telemetry related files and DangerBot will recommend a Telemetry review. Mention `@gitlab-org/growth/telemetry/engineers` in your MR for a review.
## Example Usage Ping payload
The following is example content of the Usage Ping payload.
```json
{
"uuid": "0000000-0000-0000-0000-000000000000",
"hostname": "example.com",
"version": "12.10.0-pre",
"installation_type": "omnibus-gitlab",
"active_user_count": 999,
"recorded_at": "2020-04-17T07:43:54.162+00:00",
"edition": "EEU",
"license_md5": "00000000000000000000000000000000",
"license_id": null,
"historical_max_users": 999,
"licensee": {
"Name": "ABC, Inc.",
"Email": "email@example.com",
"Company": "ABC, Inc."
},
"license_user_count": 999,
"license_starts_at": "2020-01-01",
"license_expires_at": "2021-01-01",
"license_plan": "ultimate",
"license_add_ons": {
},
"license_trial": false,
"counts": {
"assignee_lists": 999,
"boards": 999,
"ci_builds": 999,
...
},
"container_registry_enabled": true,
"dependency_proxy_enabled": false,
"gitlab_shared_runners_enabled": true,
"gravatar_enabled": true,
"influxdb_metrics_enabled": true,
"ldap_enabled": false,
"mattermost_enabled": false,
"omniauth_enabled": true,
"prometheus_metrics_enabled": false,
"reply_by_email_enabled": "incoming+%{key}@incoming.gitlab.com",
"signup_enabled": true,
"web_ide_clientside_preview_enabled": true,
"ingress_modsecurity_enabled": true,
"projects_with_expiration_policy_disabled": 999,
"projects_with_expiration_policy_enabled": 999,
...
"elasticsearch_enabled": true,
"license_trial_ends_on": null,
"geo_enabled": false,
"git": {
"version": {
"major": 2,
"minor": 26,
"patch": 1
}
},
"gitaly": {
"version": "12.10.0-rc1-93-g40980d40",
"servers": 56,
"filesystems": [
"EXT_2_3_4"
]
},
"gitlab_pages": {
"enabled": true,
"version": "1.17.0"
},
"database": {
"adapter": "postgresql",
"version": "9.6.15"
},
"app_server": {
"type": "console"
},
"avg_cycle_analytics": {
"issue": {
"average": 999,
"sd": 999,
"missing": 999
},
"plan": {
"average": null,
"sd": 999,
"missing": 999
},
"code": {
"average": null,
"sd": 999,
"missing": 999
},
"test": {
"average": null,
"sd": 999,
"missing": 999
},
"review": {
"average": null,
"sd": 999,
"missing": 999
},
"staging": {
"average": null,
"sd": 999,
"missing": 999
},
"production": {
"average": null,
"sd": 999,
"missing": 999
},
"total": 999
},
"usage_activity_by_stage": {
"configure": {
"project_clusters_enabled": 999,
...
},
"create": {
"merge_requests": 999,
...
},
"manage": {
"events": 999,
...
},
"monitor": {
"clusters": 999,
...
},
"package": {
"projects_with_packages": 999
},
"plan": {
"issues": 999,
...
},
"release": {
"deployments": 999,
...
},
"secure": {
"user_container_scanning_jobs": 999,
...
},
"verify": {
"ci_builds": 999,
...
}
},
"usage_activity_by_stage_monthly": {
"configure": {
"project_clusters_enabled": 999,
...
},
"create": {
"merge_requests": 999,
...
},
"manage": {
"events": 999,
...
},
"monitor": {
"clusters": 999,
...
},
"package": {
"projects_with_packages": 999
},
"plan": {
"issues": 999,
...
},
"release": {
"deployments": 999,
...
},
"secure": {
"user_container_scanning_jobs": 999,
...
},
"verify": {
"ci_builds": 999,
...
}
}
}
```

View file

@ -8,62 +8,50 @@ type: reference, howto
## Overview
If you are using [GitLab CI/CD](../../../ci/README.md), you can check your Docker
images (or more precisely the containers) for known vulnerabilities by using
[Clair](https://github.com/quay/clair) and [klar](https://github.com/optiopay/klar),
two open source tools for Vulnerability Static Analysis for containers.
Your application's Docker image may itself be based on Docker images that contain known
vulnerabilities. By including an extra job in your pipeline that scans for those vulnerabilities and
displays them in a merge request, you can use GitLab to audit your Docker-based apps.
By default, container scanning in GitLab is based on [Clair](https://github.com/quay/clair) and
[Klar](https://github.com/optiopay/klar), which are open-source tools for vulnerability static analysis in
containers. [GitLab's Klar analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/klar/)
scans the containers and serves as a wrapper for Clair.
You can take advantage of Container Scanning by either [including the CI job](#configuration) in
your existing `.gitlab-ci.yml` file or by implicitly using
[Auto Container Scanning](../../../topics/autodevops/stages.md#auto-container-scanning-ultimate)
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
NOTE: **Note:**
To integrate security scanners other than Clair and Klar into GitLab, see
[Security scanner integration](../../../development/integrations/secure.md).
GitLab checks the Container Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information right on the
merge request.
You can enable container scanning by doing one of the following:
- [Include the CI job](#configuration) in your existing `.gitlab-ci.yml` file.
- Implicitly use [Auto Container Scanning](../../../topics/autodevops/stages.md#auto-container-scanning-ultimate)
provided by [Auto DevOps](../../../topics/autodevops/index.md).
GitLab compares the found vulnerabilities between the source and target branches, and shows the
information directly in the merge request.
![Container Scanning Widget](img/container_scanning_v13_0.png)
## Contribute your scanner
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab.
## Use cases
If you distribute your application with Docker, then there's a great chance
that your image is based on other Docker images that may in turn contain some
known vulnerabilities that could be exploited.
Having an extra job in your pipeline that checks for those vulnerabilities,
and the fact that they are displayed inside a merge request, makes it very easy
to perform audits for your Docker-based apps.
<!-- NOTE: The container scanning tool references the following heading in the code, so if you
make a change to this heading, make sure to update the documentation URLs used in the
container scanning tool (https://gitlab.com/gitlab-org/security-products/analyzers/klar) -->
## Requirements
To enable Container Scanning in your pipeline, you need:
To enable Container Scanning in your pipeline, you need the following:
- A GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html)
executor.
- Docker `18.09.03` or higher installed on the machine where the Runners are
running. If you're using the shared Runners on GitLab.com, this is already
the case.
- To [build and push](../../packages/container_registry/index.md#container-registry-examples-with-gitlab-cicd)
your Docker image to your project's Container Registry.
The name of the Docker image should use the following
[predefined environment variables](../../../ci/variables/predefined_variables.md)
as defined below:
- [GitLab Runner](https://docs.gitlab.com/runner/) with the [Docker](https://docs.gitlab.com/runner/executors/docker.html)
or [Kubernetes](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
- Docker `18.09.03` or higher installed on the same computer as the Runner. If you're using the
shared Runners on GitLab.com, then this is already the case.
- [Build and push](../../packages/container_registry/index.md#container-registry-examples-with-gitlab-cicd)
your Docker image to your project's container registry. The name of the Docker image should use
the following [predefined environment variables](../../../ci/variables/predefined_variables.md):
```plaintext
$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
```
These can be used directly in your `.gitlab-ci.yml` file:
You can use these directly in your `.gitlab-ci.yml` file:
```yaml
build:
@ -81,37 +69,35 @@ To enable Container Scanning in your pipeline, you need:
## Configuration
For GitLab 11.9 and later, to enable Container Scanning, you must
[include](../../../ci/yaml/README.md#includetemplate) the
[`Container-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml)
that's provided as a part of your GitLab installation.
For GitLab versions earlier than 11.9, you can copy and use the job as defined
in that template.
How you enable Container Scanning depends on your GitLab version:
Add the following to your `.gitlab-ci.yml` file:
- GitLab 11.9 and later: [Include](../../../ci/yaml/README.md#includetemplate) the
[`Container-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml)
that comes with your GitLab installation.
- GitLab versions earlier than 11.9: Copy and use the job from the
[`Container-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml).
To include the `Container-Scanning.gitlab-ci.yml` template (GitLab 11.9 and later), add the
following to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Container-Scanning.gitlab-ci.yml
```
The included template will:
The included template:
1. Create a `container_scanning` job in your CI/CD pipeline.
1. Pull the already built Docker image from your project's
[Container Registry](../../packages/container_registry/index.md) (see [requirements](#requirements))
and scan it for possible vulnerabilities.
- Creates a `container_scanning` job in your CI/CD pipeline.
- Pulls the built Docker image from your project's [Container Registry](../../packages/container_registry/index.md)
(see [requirements](#requirements)) and scans it for possible vulnerabilities.
The results will be saved as a
GitLab saves the results as a
[Container Scanning report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscontainer_scanning-ultimate)
that you can later download and analyze.
Due to implementation limitations, we always take the latest Container Scanning
artifact available. Behind the scenes, the
[GitLab Klar analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/klar/)
is used and runs the scans.
that you can download and analyze later. When downloading, you always receive the most-recent
artifact.
The following is a sample `.gitlab-ci.yml` that will build your Docker image,
push it to the Container Registry, and run Container Scanning:
The following is a sample `.gitlab-ci.yml` that builds your Docker image, pushes it to the Container
Registry, and scans the containers:
```yaml
variables:
@ -141,11 +127,15 @@ include:
### Customizing the Container Scanning settings
You can change container scanning settings by using the [`variables`](../../../ci/yaml/README.md#variables)
parameter in your `.gitlab-ci.yml` to change [environment variables](#available-variables).
There may be cases where you want to customize how GitLab scans your containers. For example, you
may want to enable more verbose output from Clair or Klar, access a Docker registry that requires
authentication, and more. To change such settings, use the [`variables`](../../../ci/yaml/README.md#variables)
parameter in your `.gitlab-ci.yml` to set [environment variables](#available-variables).
The environment variables you set in your `.gitlab-ci.yml` overwrite those in
`Container-Scanning.gitlab-ci.yml`.
In the following example, we [include](../../../ci/yaml/README.md#include) the template and also
set the `CLAIR_OUTPUT` variable to `High`:
This example [includes](../../../ci/yaml/README.md#include) the Container Scanning template and
enables verbose output from Clair by setting the `CLAIR_OUTPUT` environment variable to `High`:
```yaml
include:
@ -155,9 +145,6 @@ variables:
CLAIR_OUTPUT: High
```
The `CLAIR_OUTPUT` variable defined in the main `gitlab-ci.yml` will overwrite what's
defined in `Container-Scanning.gitlab-ci.yml`, changing the Container Scanning behavior.
<!-- NOTE: The container scanning tool references the following heading in the code, so if you"
make a change to this heading, make sure to update the documentation URLs used in the"
container scanning tool (https://gitlab.com/gitlab-org/security-products/analyzers/klar)" -->
@ -188,13 +175,9 @@ using environment variables.
### Overriding the Container Scanning template
CAUTION: **Deprecation:**
Beginning in GitLab 13.0, the use of [`only` and `except`](../../../ci/yaml/README.md#onlyexcept-basic)
is no longer supported. When overriding the template, you must use [`rules`](../../../ci/yaml/README.md#rules) instead.
If you want to override the job definition (for example, change properties like
`variables`), you need to declare a `container_scanning` job after the
template inclusion and specify any additional keys under it. For example:
If you want to override the job definition (for example, to change properties like `variables`), you
must declare a `container_scanning` job after the template inclusion, and then
specify any additional keys. For example:
```yaml
include:
@ -205,15 +188,20 @@ container_scanning:
GIT_STRATEGY: fetch
```
CAUTION: **Deprecated:**
GitLab 13.0 and later doesn't support [`only` and `except`](../../../ci/yaml/README.md#onlyexcept-basic).
When overriding the template, you must use [`rules`](../../../ci/yaml/README.md#rules)
instead.
### Vulnerability whitelisting
If you want to whitelist specific vulnerabilities, you'll need to:
To whitelist specific vulnerabilities, follow these steps:
1. Set `GIT_STRATEGY: fetch` in your `.gitlab-ci.yml` file by following the instructions described in the
[overriding the Container Scanning template](#overriding-the-container-scanning-template) section of this document.
1. Define the whitelisted vulnerabilities in a YAML file named `clair-whitelist.yml` which must use the format described
in the [whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml).
1. Add the `clair-whitelist.yml` file to the Git repository of your project.
1. Set `GIT_STRATEGY: fetch` in your `.gitlab-ci.yml` file by following the instructions in
[overriding the Container Scanning template](#overriding-the-container-scanning-template).
1. Define the whitelisted vulnerabilities in a YAML file named `clair-whitelist.yml`. This must use
the format described in the [whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml).
1. Add the `clair-whitelist.yml` file to your project's Git repository.
### Running Container Scanning in an offline environment

View file

@ -498,42 +498,6 @@ BUNDLER_AUDIT_ADVISORY_DB_REF_NAME: "master"
BUNDLER_AUDIT_ADVISORY_DB_URL: "gitlab.example.com/ruby-advisory-db.git"
```
#### Java (Maven) projects
When using self-signed certificates, add the following job section to the `.gitlab-ci.yml`:
```yaml
gemnasium-maven-dependency_scanning:
variables:
MAVEN_CLI_OPTS: "-s settings.xml -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
```
#### Java (Gradle) projects
When using self-signed certificates, add the following job section to the `.gitlab-ci.yml`:
```yaml
gemnasium-maven-dependency_scanning:
before_script:
- echo -n | openssl s_client -connect maven-repo.example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/internal.crt
- keytool -importcert -file /tmp/internal.crt -cacerts -storepass changeit -noprompt
```
This adds the self-signed certificates of your Maven repository to the Java KeyStore of the analyzer's Docker image.
#### Scala (sbt) projects
When using self-signed certificates, add the following job section to the `.gitlab-ci.yml`:
```yaml
gemnasium-maven-dependency_scanning:
before_script:
- echo -n | openssl s_client -connect maven-repo.example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/internal.crt
- keytool -importcert -file /tmp/internal.crt -cacerts -storepass changeit -noprompt
```
This adds the self-signed certificates of your Maven repository to the Java KeyStore of the analyzer's Docker image.
#### Python (setuptools)
When using self-signed certificates for your private PyPi repository, no extra job configuration (aside

View file

@ -348,6 +348,32 @@ npmRegistryServer: "https://npm.example.com"
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
### Configuring Bower projects
You can configure Bower projects by using a [`.bowerrc`](https://bower.io/docs/config/#bowerrc-specification)
file.
#### Using private Bower registries
If you have a private Bower registry you can use the
[`registry`](https://bower.io/docs/config/#bowerrc-specification)
setting to specify its location.
For example:
```plaintext
{
"registry": "https://registry.bower.io"
}
```
#### Custom root certificates for Bower
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables), or by
specifying a `ca` setting in a [`.bowerrc`](https://bower.io/docs/config/#bowerrc-specification)
file.
### Migration from `license_management` to `license_scanning`
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.
@ -451,6 +477,7 @@ The License Compliance job should now use local copies of the License Compliance
your code and generate security reports, without requiring internet access.
Additional configuration may be needed for connecting to [private Maven repositories](#using-private-maven-repos),
[private Bower registries](#using-private-bower-registries),
[private NPM registries](#using-private-npm-registries), [private Yarn registries](#using-private-yarn-registries), and [private Python repositories](#using-private-python-repos).
Exact name matches are required for [project policies](#project-policies-for-license-compliance)

View file

@ -84,13 +84,25 @@ module Gitlab
elsif resolved_type.is_a? Array
# A simple list of rendered types each object being an object to authorize
resolved_type.select do |single_object_type|
allowed_access?(current_user, unpromise(single_object_type).object)
allowed_access?(current_user, realized(single_object_type).object)
end
else
raise "Can't authorize #{@field}"
end
end
# Ensure that we are dealing with realized objects, not delayed promises
def realized(thing)
case thing
when BatchLoader::GraphQL
thing.sync
when GraphQL::Execution::Lazy
thing.value # part of the private api, but we need to unwrap it here.
else
thing
end
end
def allowed_access?(current_user, object)
object = object.sync if object.respond_to?(:sync)
@ -113,17 +125,6 @@ module Gitlab
def scalar_type?
node_type_for_basic_connection(@field.type).kind.scalar?
end
# Sometimes we get promises, and have to resolve them. The dedicated way
# of doing this (GitlabSchema.after_lazy) is a private framework method,
# and so we use duck-typing interface inference here instead.
def unpromise(maybe_promise)
if maybe_promise.respond_to?(:value) && !maybe_promise.respond_to?(:object)
maybe_promise.value
else
maybe_promise
end
end
end
end
end

View file

@ -67,7 +67,7 @@ gitaly_log="$app_root/log/gitaly.log"
test -f /etc/default/gitlab && . /etc/default/gitlab
# Switch to the app_user if it is not they who are running the script.
if [ `whoami` != "$app_user" ]; then
if [ $(whoami) != "$app_user" ]; then
eval su - "$app_user" -c $(echo \")$shell_path -l -c \'$0 "$@"\'$(echo \"); exit;
fi

View file

@ -1,5 +1,5 @@
#!/bin/sh
output=`git grep -En '^<<<<<<< '`
output=$(git grep -En '^<<<<<<< ')
echo $output
test -z "$output"

View file

@ -0,0 +1,94 @@
import Hook from '~/droplab/hook';
import DropDown from '~/droplab/drop_down';
jest.mock('~/droplab/drop_down', () => jest.fn());
describe('Hook', () => {
let testContext;
beforeEach(() => {
testContext = {};
});
describe('class constructor', () => {
beforeEach(() => {
testContext.trigger = { id: 'id' };
testContext.list = {};
testContext.plugins = {};
testContext.config = {};
testContext.hook = new Hook(
testContext.trigger,
testContext.list,
testContext.plugins,
testContext.config,
);
});
it('should set .trigger', () => {
expect(testContext.hook.trigger).toBe(testContext.trigger);
});
it('should set .list', () => {
expect(testContext.hook.list).toEqual({});
});
it('should call DropDown constructor', () => {
expect(DropDown).toHaveBeenCalledWith(testContext.list, testContext.config);
});
it('should set .type', () => {
expect(testContext.hook.type).toBe('Hook');
});
it('should set .event', () => {
expect(testContext.hook.event).toBe('click');
});
it('should set .plugins', () => {
expect(testContext.hook.plugins).toBe(testContext.plugins);
});
it('should set .config', () => {
expect(testContext.hook.config).toBe(testContext.config);
});
it('should set .id', () => {
expect(testContext.hook.id).toBe(testContext.trigger.id);
});
describe('if config argument is undefined', () => {
beforeEach(() => {
testContext.config = undefined;
testContext.hook = new Hook(
testContext.trigger,
testContext.list,
testContext.plugins,
testContext.config,
);
});
it('should set .config to an empty object', () => {
expect(testContext.hook.config).toEqual({});
});
});
describe('if plugins argument is undefined', () => {
beforeEach(() => {
testContext.plugins = undefined;
testContext.hook = new Hook(
testContext.trigger,
testContext.list,
testContext.plugins,
testContext.config,
);
});
it('should set .plugins to an empty array', () => {
expect(testContext.hook.plugins).toEqual([]);
});
});
});
});

View file

@ -6,7 +6,6 @@ import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
@ -80,19 +79,6 @@ describe('Dashboard', () => {
it('shows the environment selector', () => {
expect(findEnvironmentsDropdown().exists()).toBe(true);
});
it('sets initial state', () => {
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setInitialState', {
currentDashboard: '',
currentEnvironmentName: 'production',
dashboardEndpoint: 'https://invalid',
dashboardsEndpoint: 'https://invalid',
deploymentsEndpoint: null,
logsPath: '/path/to/logs',
metricsEndpoint: 'http://test.host/monitoring/mock',
projectPath: '/path/to/project',
});
});
});
describe('no data found', () => {
@ -288,7 +274,10 @@ describe('Dashboard', () => {
it('URL is updated with panel parameters and custom dashboard', () => {
const dashboard = 'dashboard.yml';
createMountedWrapper({ hasMetrics: true, currentDashboard: dashboard });
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboard,
});
createMountedWrapper({ hasMetrics: true });
expandPanel(group, panel);
const expectedSearch = objectToQuery({
@ -326,8 +315,10 @@ describe('Dashboard', () => {
describe('when all requests have been commited by the store', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentEnvironmentName: 'production',
});
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
return wrapper.vm.$nextTick();
@ -785,7 +776,6 @@ describe('Dashboard', () => {
describe('cluster health', () => {
beforeEach(() => {
mock.onGet(propsData.metricsEndpoint).reply(statusCodes.OK, JSON.stringify({}));
createShallowWrapper({ hasMetrics: true, showHeader: false });
// all_dashboards is not defined in health dashboards
@ -877,7 +867,10 @@ describe('Dashboard', () => {
beforeEach(() => {
setupStoreWithData(store);
createShallowWrapper({ hasMetrics: true, currentDashboard });
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard,
});
createShallowWrapper({ hasMetrics: true });
return wrapper.vm.$nextTick();
});

View file

@ -14,7 +14,9 @@ describe('Dashboard template', () => {
let mock;
beforeEach(() => {
store = createStore();
store = createStore({
currentEnvironmentName: 'production',
});
mock = new MockAdapter(axios);
setupAllDashboards(store);

View file

@ -11,17 +11,12 @@ export const propsData = {
settingsPath: '/path/to/settings',
clustersPath: '/path/to/clusters',
tagsPath: '/path/to/tags',
projectPath: '/path/to/project',
logsPath: '/path/to/logs',
defaultBranch: 'master',
metricsEndpoint: mockApiEndpoint,
deploymentsEndpoint: null,
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg',
emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
currentEnvironmentName: 'production',
customMetricsAvailable: false,
customMetricsPath: '',
validateQueryPath: '',

View file

@ -8,7 +8,7 @@ import createFlash from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import store from '~/monitoring/stores';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
fetchData,
@ -52,20 +52,16 @@ import {
jest.mock('~/flash');
const resetStore = str => {
str.replaceState({
showEmptyState: true,
emptyState: 'loading',
groups: [],
});
};
describe('Monitoring store actions', () => {
const { convertObjectPropsToCamelCase } = commonUtils;
let mock;
let store;
let state;
beforeEach(() => {
store = createStore();
state = store.state.monitoringDashboard;
mock = new MockAdapter(axios);
jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
@ -83,7 +79,6 @@ describe('Monitoring store actions', () => {
});
});
afterEach(() => {
resetStore(store);
mock.reset();
commonUtils.backOff.mockReset();
@ -92,8 +87,6 @@ describe('Monitoring store actions', () => {
describe('fetchData', () => {
it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
const { state } = store;
return testAction(
fetchData,
null,
@ -111,8 +104,6 @@ describe('Monitoring store actions', () => {
const origGon = window.gon;
window.gon = { features: { metricsDashboardAnnotations: true } };
const { state } = store;
return testAction(
fetchData,
null,
@ -131,7 +122,6 @@ describe('Monitoring store actions', () => {
describe('fetchDeploymentsData', () => {
it('dispatches receiveDeploymentsDataSuccess on success', () => {
const { state } = store;
state.deploymentsEndpoint = '/success';
mock.onGet(state.deploymentsEndpoint).reply(200, {
deployments: deploymentData,
@ -146,7 +136,6 @@ describe('Monitoring store actions', () => {
);
});
it('dispatches receiveDeploymentsDataFailure on error', () => {
const { state } = store;
state.deploymentsEndpoint = '/error';
mock.onGet(state.deploymentsEndpoint).reply(500);
@ -164,11 +153,8 @@ describe('Monitoring store actions', () => {
});
describe('fetchEnvironmentsData', () => {
const { state } = store;
state.projectPath = 'gitlab-org/gitlab-test';
afterEach(() => {
resetStore(store);
beforeEach(() => {
state.projectPath = 'gitlab-org/gitlab-test';
});
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
@ -269,17 +255,14 @@ describe('Monitoring store actions', () => {
});
describe('fetchAnnotations', () => {
const { state } = store;
state.timeRange = {
start: '2020-04-15T12:54:32.137Z',
end: '2020-08-15T12:54:32.137Z',
};
state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
afterEach(() => {
resetStore(store);
beforeEach(() => {
state.timeRange = {
start: '2020-04-15T12:54:32.137Z',
end: '2020-08-15T12:54:32.137Z',
};
state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
});
it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
@ -353,7 +336,6 @@ describe('Monitoring store actions', () => {
});
describe('Toggles starred value of current dashboard', () => {
const { state } = store;
let unstarredDashboard;
let starredDashboard;
@ -396,23 +378,19 @@ describe('Monitoring store actions', () => {
});
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {
mockedState = storeState();
});
it('should commit SET_INITIAL_STATE mutation', done => {
testAction(
setInitialState,
{
metricsEndpoint: 'additional_metrics.json',
currentDashboard: '.gitlab/dashboards/dashboard.yml',
deploymentsEndpoint: 'deployments.json',
},
mockedState,
state,
[
{
type: types.SET_INITIAL_STATE,
payload: {
metricsEndpoint: 'additional_metrics.json',
currentDashboard: '.gitlab/dashboards/dashboard.yml',
deploymentsEndpoint: 'deployments.json',
},
},
@ -423,15 +401,11 @@ describe('Monitoring store actions', () => {
});
});
describe('Set empty states', () => {
let mockedState;
beforeEach(() => {
mockedState = storeState();
});
it('should commit SET_METRICS_ENDPOINT mutation', done => {
testAction(
setGettingStartedEmptyState,
null,
mockedState,
state,
[
{
type: types.SET_GETTING_STARTED_EMPTY_STATE,
@ -444,15 +418,11 @@ describe('Monitoring store actions', () => {
});
describe('updateVariableValues', () => {
let mockedState;
beforeEach(() => {
mockedState = storeState();
});
it('should commit UPDATE_VARIABLE_VALUES mutation', done => {
testAction(
updateVariableValues,
{ pod: 'POD' },
mockedState,
state,
[
{
type: types.UPDATE_VARIABLE_VALUES,
@ -467,13 +437,11 @@ describe('Monitoring store actions', () => {
describe('fetchDashboard', () => {
let dispatch;
let state;
let commit;
const response = metricsDashboardResponse;
beforeEach(() => {
dispatch = jest.fn();
commit = jest.fn();
state = storeState();
state.dashboardEndpoint = '/dashboard';
});
@ -557,12 +525,10 @@ describe('Monitoring store actions', () => {
describe('receiveMetricsDashboardSuccess', () => {
let commit;
let dispatch;
let state;
beforeEach(() => {
commit = jest.fn();
dispatch = jest.fn();
state = storeState();
});
it('stores groups', () => {
@ -623,13 +589,11 @@ describe('Monitoring store actions', () => {
describe('fetchDashboardData', () => {
let commit;
let dispatch;
let state;
beforeEach(() => {
jest.spyOn(Tracking, 'event');
commit = jest.fn();
dispatch = jest.fn();
state = storeState();
state.timeRange = defaultTimeRange;
});
@ -731,7 +695,6 @@ describe('Monitoring store actions', () => {
step: 60,
};
let metric;
let state;
let data;
let prometheusEndpointPath;
@ -929,10 +892,7 @@ describe('Monitoring store actions', () => {
});
describe('duplicateSystemDashboard', () => {
let state;
beforeEach(() => {
state = storeState();
state.dashboardsEndpoint = '/dashboards.json';
});
@ -1010,12 +970,6 @@ describe('Monitoring store actions', () => {
});
describe('setExpandedPanel', () => {
let state;
beforeEach(() => {
state = storeState();
});
it('Sets a panel as expanded', () => {
const group = 'group_1';
const panel = { title: 'A Panel' };
@ -1031,12 +985,6 @@ describe('Monitoring store actions', () => {
});
describe('clearExpandedPanel', () => {
let state;
beforeEach(() => {
state = storeState();
});
it('Clears a panel as expanded', () => {
return testAction(
clearExpandedPanel,

View file

@ -0,0 +1,23 @@
import { createStore } from '~/monitoring/stores';
describe('Monitoring Store Index', () => {
it('creates store with a `monitoringDashboard` namespace', () => {
expect(createStore().state).toEqual({
monitoringDashboard: expect.any(Object),
});
});
it('creates store with initial values', () => {
const defaults = {
deploymentsEndpoint: '/mock/deployments',
dashboardEndpoint: '/mock/dashboard',
dashboardsEndpoint: '/mock/dashboards',
};
const { state } = createStore(defaults);
expect(state).toEqual({
monitoringDashboard: expect.objectContaining(defaults),
});
});
});

View file

@ -128,13 +128,11 @@ describe('Monitoring mutations', () => {
describe('SET_INITIAL_STATE', () => {
it('should set all the endpoints', () => {
mutations[types.SET_INITIAL_STATE](stateCopy, {
metricsEndpoint: 'additional_metrics.json',
deploymentsEndpoint: 'deployments.json',
dashboardEndpoint: 'dashboard.json',
projectPath: '/gitlab-org/gitlab-foss',
currentEnvironmentName: 'production',
});
expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json');
expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
@ -179,12 +177,10 @@ describe('Monitoring mutations', () => {
describe('SET_ENDPOINTS', () => {
it('should set all the endpoints', () => {
mutations[types.SET_ENDPOINTS](stateCopy, {
metricsEndpoint: 'additional_metrics.json',
deploymentsEndpoint: 'deployments.json',
dashboardEndpoint: 'dashboard.json',
projectPath: '/gitlab-org/gitlab-foss',
});
expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json');
expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');

View file

@ -1,6 +1,6 @@
import $ from 'jquery';
import setupToggleButtons from '~/toggle_buttons';
import getSetTimeoutPromise from './helpers/set_timeout_promise_helper';
import waitForPromises from './helpers/wait_for_promises';
function generateMarkup(isChecked = true) {
return `
@ -31,19 +31,16 @@ describe('ToggleButtons', () => {
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
});
it('should toggle to unchecked when clicked', done => {
it('should toggle to unchecked when clicked', () => {
const wrapper = setupFixture(true);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
toggleButton.click();
getSetTimeoutPromise()
.then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
})
.then(done)
.catch(done.fail);
return waitForPromises().then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
});
});
});
@ -58,24 +55,21 @@ describe('ToggleButtons', () => {
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
});
it('should toggle to checked when clicked', done => {
it('should toggle to checked when clicked', () => {
const wrapper = setupFixture(false);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
toggleButton.click();
getSetTimeoutPromise()
.then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(true);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
})
.then(done)
.catch(done.fail);
return waitForPromises().then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(true);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
});
});
});
it('should emit `trigger-change` event', done => {
const changeSpy = jasmine.createSpy('changeEventHandler');
it('should emit `trigger-change` event', () => {
const changeSpy = jest.fn();
const wrapper = setupFixture(false);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
const input = wrapper.querySelector('.js-project-feature-toggle-input');
@ -84,16 +78,13 @@ describe('ToggleButtons', () => {
toggleButton.click();
getSetTimeoutPromise()
.then(() => {
expect(changeSpy).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
return waitForPromises().then(() => {
expect(changeSpy).toHaveBeenCalled();
});
});
describe('clickCallback', () => {
it('should show loading indicator while waiting', done => {
it('should show loading indicator while waiting', () => {
const isChecked = true;
const clickCallback = (newValue, toggleButton) => {
const input = toggleButton.querySelector('.js-project-feature-toggle-input');
@ -107,15 +98,12 @@ describe('ToggleButtons', () => {
expect(input.value).toEqual('true');
// After the callback finishes, check that the loading state is gone
getSetTimeoutPromise()
.then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(toggleButton.classList.contains('is-loading')).toEqual(false);
expect(toggleButton.disabled).toEqual(false);
expect(input.value).toEqual('false');
})
.then(done)
.catch(done.fail);
return waitForPromises().then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(toggleButton.classList.contains('is-loading')).toEqual(false);
expect(toggleButton.disabled).toEqual(false);
expect(input.value).toEqual('false');
});
};
const wrapper = setupFixture(isChecked, clickCallback);

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
describe('MrWidgetAuthor', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
describe('MrWidgetAuthorTime', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
describe('MRWidgetHeader', () => {

View file

@ -1,4 +1,6 @@
import Vue from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
@ -59,12 +61,20 @@ const messages = {
describe('MemoryUsage', () => {
let vm;
let el;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`${url}.json`).reply(200);
vm = createComponent();
el = vm.$el;
});
afterEach(() => {
mock.restore();
});
describe('data', () => {
it('should have default data', () => {
const data = MemoryUsage.data();
@ -127,6 +137,9 @@ describe('MemoryUsage', () => {
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
// ignore BoostrapVue warnings
jest.spyOn(console, 'warn').mockImplementation();
vm.computeGraphData(metrics, deployment_time);
const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
@ -147,15 +160,15 @@ describe('MemoryUsage', () => {
});
it('should load metrics data using MRWidgetService', done => {
spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true));
spyOn(vm, 'computeGraphData');
jest.spyOn(MRWidgetService, 'fetchMetrics').mockReturnValue(returnServicePromise(true));
jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {});
vm.loadMetrics();
setTimeout(() => {
setImmediate(() => {
expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
done();
}, 333);
});
});
});
});
@ -182,6 +195,9 @@ describe('MemoryUsage', () => {
});
it('should show deployment memory usage when metrics are loaded', done => {
// ignore BoostrapVue warnings
jest.spyOn(console, 'warn').mockImplementation();
vm.loadingMetrics = false;
vm.hasMetrics = true;
vm.loadFailed = false;

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
describe('MRWidgetMergeHelp', () => {

View file

@ -1,6 +1,6 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { trimText } from 'spec/helpers/text_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import { trimText } from 'helpers/text_helper';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import mockData from '../mock_data';

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import eventHub from '~/vue_merge_request_widget/event_hub';
import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
@ -105,7 +105,7 @@ describe('Merge request widget rebase component', () => {
describe('methods', () => {
it('checkRebaseStatus', done => {
spyOn(eventHub, '$emit');
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Component, {
mr: {},
service: {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue';
describe('MRWidgetRelatedLinks', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('MR widget status icon component', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import component from '~/vue_merge_request_widget/components/review_app_link.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
@ -42,7 +42,7 @@ describe('review app link', () => {
});
it('tracks an event when clicked', () => {
const spy = mockTracking('_category_', el, spyOn);
const spy = mockTracking('_category_', el, jest.spyOn);
triggerEvent(el);
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
describe('MRWidgetArchived', () => {

View file

@ -1,6 +1,6 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { trimText } from 'spec/helpers/text_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import { trimText } from 'helpers/text_helper';
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import eventHub from '~/vue_merge_request_widget/event_hub';
@ -14,7 +14,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
beforeEach(() => {
const Component = Vue.extend(autoMergeEnabledComponent);
spyOn(eventHub, '$emit');
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Component, {
mr: {
@ -103,7 +103,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
const mrObj = {
is_new_mr_data: true,
};
spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(
jest.spyOn(vm.service, 'cancelAutomaticMerge').mockReturnValue(
new Promise(resolve => {
resolve({
data: mrObj,
@ -112,17 +112,17 @@ describe('MRWidgetAutoMergeEnabled', () => {
);
vm.cancelAutomaticMerge();
setTimeout(() => {
setImmediate(() => {
expect(vm.isCancellingAutoMerge).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
done();
}, 333);
});
});
});
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', done => {
spyOn(vm.service, 'merge').and.returnValue(
jest.spyOn(vm.service, 'merge').mockReturnValue(
Promise.resolve({
data: {
status: MWPS_MERGE_STRATEGY,
@ -131,7 +131,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
);
vm.removeSourceBranch();
setTimeout(() => {
setImmediate(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.merge).toHaveBeenCalledWith({
sha,
@ -139,7 +139,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
should_remove_source_branch: true,
});
done();
}, 333);
});
});
});
});

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
describe('MRWidgetChecking', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
describe('MRWidgetClosed', () => {

View file

@ -1,7 +1,8 @@
import $ from 'jquery';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { removeBreakLine } from 'spec/helpers/text_helper';
import { removeBreakLine } from 'helpers/text_helper';
import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import { TEST_HOST } from 'helpers/test_constants';
describe('MRWidgetConflicts', () => {
let vm;
@ -16,7 +17,7 @@ describe('MRWidgetConflicts', () => {
}
beforeEach(() => {
spyOn($.fn, 'popover').and.callThrough();
jest.spyOn($.fn, 'popover');
});
afterEach(() => {
@ -185,7 +186,7 @@ describe('MRWidgetConflicts', () => {
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: gl.TEST_HOST,
conflictResolutionPath: TEST_HOST,
sourceBranchProtected: true,
conflictsDocsPath: '',
},
@ -207,7 +208,7 @@ describe('MRWidgetConflicts', () => {
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: gl.TEST_HOST,
conflictResolutionPath: TEST_HOST,
sourceBranchProtected: false,
conflictsDocsPath: '',
},

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@ -11,9 +11,9 @@ describe('MRWidgetFailedToMerge', () => {
beforeEach(() => {
Component = Vue.extend(failedToMergeComponent);
spyOn(eventHub, '$emit');
spyOn(window, 'setInterval').and.returnValue(dummyIntervalId);
spyOn(window, 'clearInterval').and.stub();
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(window, 'setInterval').mockReturnValue(dummyIntervalId);
jest.spyOn(window, 'clearInterval').mockImplementation();
mr = {
mergeError: 'Merge error happened',
};
@ -83,7 +83,7 @@ describe('MRWidgetFailedToMerge', () => {
describe('updateTimer', () => {
it('should update timer and emit event when timer end', () => {
spyOn(vm, 'refresh');
jest.spyOn(vm, 'refresh').mockImplementation(() => {});
expect(vm.timer).toEqual(10);

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@ -52,7 +52,7 @@ describe('MRWidgetMerged', () => {
removeSourceBranch() {},
};
spyOn(eventHub, '$emit');
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm = mountComponent(Component, { mr, service });
});
@ -124,7 +124,7 @@ describe('MRWidgetMerged', () => {
describe('methods', () => {
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', done => {
spyOn(vm.service, 'removeSourceBranch').and.returnValue(
jest.spyOn(vm.service, 'removeSourceBranch').mockReturnValue(
new Promise(resolve => {
resolve({
data: {
@ -135,14 +135,14 @@ describe('MRWidgetMerged', () => {
);
vm.removeSourceBranch();
setTimeout(() => {
const args = eventHub.$emit.calls.argsFor(0);
setImmediate(() => {
const args = eventHub.$emit.mock.calls[0];
expect(vm.isMakingRequest).toEqual(true);
expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).not.toThrow();
done();
}, 333);
});
});
});
});

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
describe('MRWidgetMerging', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
describe('MRWidgetMissingBranch', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
describe('MRWidgetNotAllowed', () => {

View file

@ -1,6 +1,6 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { removeBreakLine } from 'spec/helpers/text_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import { removeBreakLine } from 'helpers/text_helper';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
describe('MRWidgetPipelineBlocked', () => {

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import { removeBreakLine } from 'spec/helpers/text_helper';
import { removeBreakLine } from 'helpers/text_helper';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
describe('PipelineFailed', () => {

View file

@ -7,6 +7,15 @@ import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import { MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import simplePoll from '~/lib/utils/simple_poll';
jest.mock('~/lib/utils/simple_poll', () =>
jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default),
);
jest.mock('~/commons/nav/user_merge_requests', () => ({
refreshUserMergeRequestCounts: jest.fn(),
}));
const commitMessage = 'This is the commit message';
const squashCommitMessage = 'This is the squash commit message';
@ -33,6 +42,7 @@ const createTestMr = customConfig => {
targetBranch: 'master',
preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
};
Object.assign(mr, customConfig.mr);
@ -41,8 +51,8 @@ const createTestMr = customConfig => {
};
const createTestService = () => ({
merge() {},
poll() {},
merge: jest.fn(),
poll: jest.fn().mockResolvedValue(),
});
const createComponent = (customConfig = {}) => {
@ -59,11 +69,9 @@ const createComponent = (customConfig = {}) => {
describe('ReadyToMerge', () => {
let vm;
let updateMrCountSpy;
beforeEach(() => {
vm = createComponent();
updateMrCountSpy = spyOnDependency(ReadyToMerge, 'refreshUserMergeRequestCounts');
});
afterEach(() => {
@ -347,19 +355,21 @@ describe('ReadyToMerge', () => {
});
it('should handle merge when pipeline succeeds', done => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'merge').and.returnValue(returnPromise('merge_when_pipeline_succeeds'));
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest
.spyOn(vm.service, 'merge')
.mockReturnValue(returnPromise('merge_when_pipeline_succeeds'));
vm.removeSourceBranch = false;
vm.handleMergeButtonClick(true);
setTimeout(() => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
const params = vm.service.merge.calls.argsFor(0)[0];
const params = vm.service.merge.mock.calls[0][0];
expect(params).toEqual(
jasmine.objectContaining({
expect.objectContaining({
sha: vm.mr.sha,
commit_message: vm.mr.commitMessage,
should_remove_source_branch: false,
@ -367,67 +377,56 @@ describe('ReadyToMerge', () => {
}),
);
done();
}, 333);
});
});
it('should handle merge failed', done => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'merge').and.returnValue(returnPromise('failed'));
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('failed'));
vm.handleMergeButtonClick(false, true);
setTimeout(() => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
const params = vm.service.merge.calls.argsFor(0)[0];
const params = vm.service.merge.mock.calls[0][0];
expect(params.should_remove_source_branch).toBeTruthy();
expect(params.auto_merge_strategy).toBeUndefined();
done();
}, 333);
});
});
it('should handle merge action accepted case', done => {
spyOn(vm.service, 'merge').and.returnValue(returnPromise('success'));
spyOn(vm, 'initiateMergePolling');
jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('success'));
jest.spyOn(vm, 'initiateMergePolling').mockImplementation(() => {});
vm.handleMergeButtonClick();
setTimeout(() => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(vm.initiateMergePolling).toHaveBeenCalled();
const params = vm.service.merge.calls.argsFor(0)[0];
const params = vm.service.merge.mock.calls[0][0];
expect(params.should_remove_source_branch).toBeTruthy();
expect(params.auto_merge_strategy).toBeUndefined();
done();
}, 333);
});
});
});
describe('initiateMergePolling', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should call simplePoll', () => {
const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateMergePolling();
expect(simplePoll).toHaveBeenCalledWith(jasmine.any(Function), { timeout: 0 });
expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 });
});
it('should call handleMergePolling', () => {
spyOn(vm, 'handleMergePolling');
jest.spyOn(vm, 'handleMergePolling').mockImplementation(() => {});
vm.initiateMergePolling();
jasmine.clock().tick(2000);
expect(vm.handleMergePolling).toHaveBeenCalled();
});
});
@ -448,9 +447,9 @@ describe('ReadyToMerge', () => {
});
it('should call start and stop polling when MR merged', done => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
@ -463,26 +462,26 @@ describe('ReadyToMerge', () => {
spc = true;
},
);
setTimeout(() => {
setImmediate(() => {
expect(vm.service.poll).toHaveBeenCalled();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent');
expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled();
expect(updateMrCountSpy).toHaveBeenCalled();
expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
expect(cpc).toBeFalsy();
expect(spc).toBeTruthy();
done();
}, 333);
});
});
it('updates status box', done => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
vm.handleMergePolling(() => {}, () => {});
setTimeout(() => {
setImmediate(() => {
const statusBox = document.querySelector('.status-box');
expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy();
@ -493,12 +492,12 @@ describe('ReadyToMerge', () => {
});
it('hides close button', done => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
vm.handleMergePolling(() => {}, () => {});
setTimeout(() => {
setImmediate(() => {
expect(document.querySelector('.btn-close').classList.contains('hidden')).toBeTruthy();
done();
@ -506,12 +505,12 @@ describe('ReadyToMerge', () => {
});
it('updates merge request count badge', done => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
vm.handleMergePolling(() => {}, () => {});
setTimeout(() => {
setImmediate(() => {
expect(document.querySelector('.js-merge-counter').textContent).toBe('0');
done();
@ -519,8 +518,8 @@ describe('ReadyToMerge', () => {
});
it('should continue polling until MR is merged', done => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('some_other_state'));
jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
@ -533,19 +532,18 @@ describe('ReadyToMerge', () => {
spc = true;
},
);
setTimeout(() => {
setImmediate(() => {
expect(cpc).toBeTruthy();
expect(spc).toBeFalsy();
done();
}, 333);
});
});
});
describe('initiateRemoveSourceBranchPolling', () => {
it('should emit event and call simplePoll', () => {
spyOn(eventHub, '$emit');
const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm.initiateRemoveSourceBranchPolling();
@ -565,8 +563,8 @@ describe('ReadyToMerge', () => {
});
it('should call start and stop polling when MR merged', done => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'poll').and.returnValue(returnPromise(false));
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(false));
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
@ -579,10 +577,10 @@ describe('ReadyToMerge', () => {
spc = true;
},
);
setTimeout(() => {
setImmediate(() => {
expect(vm.service.poll).toHaveBeenCalled();
const args = eventHub.$emit.calls.argsFor(0);
const args = eventHub.$emit.mock.calls[0];
expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).toBeDefined();
@ -594,11 +592,11 @@ describe('ReadyToMerge', () => {
expect(spc).toBeTruthy();
done();
}, 333);
});
});
it('should continue polling until MR is merged', done => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise(true));
jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(true));
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
@ -611,12 +609,12 @@ describe('ReadyToMerge', () => {
spc = true;
},
);
setTimeout(() => {
setImmediate(() => {
expect(cpc).toBeTruthy();
expect(spc).toBeFalsy();
done();
}, 333);
});
});
});
});

View file

@ -1,6 +1,6 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { removeBreakLine } from 'spec/helpers/text_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import { removeBreakLine } from 'helpers/text_helper';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
describe('ShaMismatch', () => {

View file

@ -1,6 +1,7 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
import { TEST_HOST } from 'helpers/test_constants';
describe('UnresolvedDiscussions', () => {
const Component = Vue.extend(UnresolvedDiscussions);
@ -14,7 +15,7 @@ describe('UnresolvedDiscussions', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mr: {
createIssueToResolveDiscussionsPath: gl.TEST_HOST,
createIssueToResolveDiscussionsPath: TEST_HOST,
},
});
});
@ -25,7 +26,7 @@ describe('UnresolvedDiscussions', () => {
);
expect(vm.$el.innerText).toContain('Create an issue to resolve them later');
expect(vm.$el.querySelector('.js-create-issue').getAttribute('href')).toEqual(gl.TEST_HOST);
expect(vm.$el.querySelector('.js-create-issue').getAttribute('href')).toEqual(TEST_HOST);
});
});

View file

@ -1,6 +1,9 @@
import Vue from 'vue';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import createFlash from '~/flash';
jest.mock('~/flash');
const createComponent = () => {
const Component = Vue.extend(WorkInProgress);
@ -47,9 +50,8 @@ describe('Wip', () => {
it('should make a request to service and handle response', done => {
const vm = createComponent();
const flashSpy = spyOnDependency(WorkInProgress, 'createFlash').and.returnValue(true);
spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeWIP').and.returnValue(
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(vm.service, 'removeWIP').mockReturnValue(
new Promise(resolve => {
resolve({
data: mrObj,
@ -58,12 +60,15 @@ describe('Wip', () => {
);
vm.handleRemoveWIP();
setTimeout(() => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(flashSpy).toHaveBeenCalledWith('The merge request can now be merged.', 'notice');
expect(createFlash).toHaveBeenCalledWith(
'The merge request can now be merged.',
'notice',
);
done();
}, 333);
});
});
});
});

View file

@ -1,7 +1,7 @@
import Vue from 'vue';
import Mousetrap from 'mousetrap';
import { file } from 'jest/ide/helpers';
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import waitForPromises from 'helpers/wait_for_promises';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
@ -48,7 +48,7 @@ describe('File finder item spec', () => {
],
});
setTimeout(done);
setImmediate(done);
});
it('renders list of blobs', () => {
@ -60,7 +60,7 @@ describe('File finder item spec', () => {
it('filters entries', done => {
vm.searchText = 'index';
setTimeout(() => {
setImmediate(() => {
expect(vm.$el.textContent).toContain('index.js');
expect(vm.$el.textContent).not.toContain('component.js');
@ -71,7 +71,7 @@ describe('File finder item spec', () => {
it('shows clear button when searchText is not empty', done => {
vm.searchText = 'index';
setTimeout(() => {
setImmediate(() => {
expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
@ -82,11 +82,11 @@ describe('File finder item spec', () => {
it('clear button resets searchText', done => {
vm.searchText = 'index';
timeoutPromise()
waitForPromises()
.then(() => {
vm.$el.querySelector('.dropdown-input-clear').click();
})
.then(timeoutPromise)
.then(waitForPromises)
.then(() => {
expect(vm.searchText).toBe('');
})
@ -95,14 +95,14 @@ describe('File finder item spec', () => {
});
it('clear button focues search input', done => {
spyOn(vm.$refs.searchInput, 'focus');
jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {});
vm.searchText = 'index';
timeoutPromise()
waitForPromises()
.then(() => {
vm.$el.querySelector('.dropdown-input-clear').click();
})
.then(timeoutPromise)
.then(waitForPromises)
.then(() => {
expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
})
@ -114,7 +114,7 @@ describe('File finder item spec', () => {
it('returns 1 when no filtered entries exist', done => {
vm.searchText = 'testing 123';
setTimeout(() => {
setImmediate(() => {
expect(vm.listShowCount).toBe(1);
done();
@ -134,7 +134,7 @@ describe('File finder item spec', () => {
it('returns 33 when entries dont exist', done => {
vm.searchText = 'testing 123';
setTimeout(() => {
setImmediate(() => {
expect(vm.listHeight).toBe(33);
done();
@ -146,7 +146,7 @@ describe('File finder item spec', () => {
it('returns length of filtered blobs', done => {
vm.searchText = 'index';
setTimeout(() => {
setImmediate(() => {
expect(vm.filteredBlobsLength).toBe(1);
done();
@ -160,7 +160,7 @@ describe('File finder item spec', () => {
vm.focusedIndex = 1;
vm.searchText = 'test';
setTimeout(() => {
setImmediate(() => {
expect(vm.focusedIndex).toBe(0);
done();
@ -173,11 +173,11 @@ describe('File finder item spec', () => {
vm.searchText = 'test';
vm.visible = true;
timeoutPromise()
waitForPromises()
.then(() => {
vm.visible = false;
})
.then(timeoutPromise)
.then(waitForPromises)
.then(() => {
expect(vm.searchText).toBe('');
})
@ -189,7 +189,7 @@ describe('File finder item spec', () => {
describe('openFile', () => {
beforeEach(() => {
spyOn(vm, '$emit');
jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
it('closes file finder', () => {
@ -210,11 +210,11 @@ describe('File finder item spec', () => {
const event = new CustomEvent('keyup');
event.keyCode = ENTER_KEY_CODE;
spyOn(vm, 'openFile');
jest.spyOn(vm, 'openFile').mockImplementation(() => {});
vm.$refs.searchInput.dispatchEvent(event);
setTimeout(() => {
setImmediate(() => {
expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
done();
@ -225,11 +225,11 @@ describe('File finder item spec', () => {
const event = new CustomEvent('keyup');
event.keyCode = ESC_KEY_CODE;
spyOn(vm, '$emit');
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$refs.searchInput.dispatchEvent(event);
setTimeout(() => {
setImmediate(() => {
expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
done();
@ -303,7 +303,7 @@ describe('File finder item spec', () => {
beforeEach(done => {
createComponent();
spyOn(vm, 'toggle');
jest.spyOn(vm, 'toggle').mockImplementation(() => {});
vm.$nextTick(done);
});

View file

@ -1,13 +1,16 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import Icon from '~/vue_shared/components/icon.vue';
import iconsPath from '@gitlab/svgs/dist/icons.svg';
describe('Sprite Icon Component', function() {
describe('Initialization', function() {
jest.mock('@gitlab/svgs/dist/icons.svg', () => 'testing');
describe('Sprite Icon Component', () => {
describe('Initialization', () => {
let icon;
beforeEach(function() {
beforeEach(() => {
const IconComponent = Vue.extend(Icon);
icon = mountComponent(IconComponent, {
@ -20,20 +23,20 @@ describe('Sprite Icon Component', function() {
icon.$destroy();
});
it('should return a defined Vue component', function() {
it('should return a defined Vue component', () => {
expect(icon).toBeDefined();
});
it('should have <svg> as a child element', function() {
it('should have <svg> as a child element', () => {
expect(icon.$el.tagName).toBe('svg');
});
it('should have <use> as a child element with the correct href', function() {
it('should have <use> as a child element with the correct href', () => {
expect(icon.$el.firstChild.tagName).toBe('use');
expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#commit`);
expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${iconsPath}#commit`);
});
it('should properly compute iconSizeClass', function() {
it('should properly compute iconSizeClass', () => {
expect(icon.iconSizeClass).toBe('s32');
});
@ -43,7 +46,7 @@ describe('Sprite Icon Component', function() {
expect(icon.$options.props.size.validator(9001)).toBeFalsy();
});
it('should properly render img css', function() {
it('should properly render img css', () => {
const { classList } = icon.$el;
const containsSizeClass = classList.contains('s32');
@ -51,16 +54,18 @@ describe('Sprite Icon Component', function() {
});
it('`name` validator should return false for non existing icons', () => {
jest.spyOn(console, 'warn').mockImplementation();
expect(Icon.props.name.validator('non_existing_icon_sprite')).toBe(false);
});
it('`name` validator should return false for existing icons', () => {
it('`name` validator should return true for existing icons', () => {
expect(Icon.props.name.validator('commit')).toBe(true);
});
});
it('should call registered listeners when they are triggered', () => {
const clickHandler = jasmine.createSpy('clickHandler');
const clickHandler = jest.fn();
const wrapper = mount(Icon, {
propsData: { name: 'commit' },
listeners: { click: clickHandler },

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
describe('Panel Resizer component', () => {
@ -69,12 +69,12 @@ describe('Panel Resizer component', () => {
side: 'left',
});
spyOn(vm, '$emit');
jest.spyOn(vm, '$emit').mockImplementation(() => {});
triggerEvent('mousedown', vm.$el);
triggerEvent('mousemove', document);
triggerEvent('mouseup', document);
expect(vm.$emit.calls.allArgs()).toEqual([
expect(vm.$emit.mock.calls).toEqual([
['resize-start', 100],
['update:size', 100],
['resize-end', 100],

View file

@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
@ -224,6 +225,10 @@ describe('DropdownContentsLabelsView', () => {
expect(searchInputEl.attributes('autofocus')).toBe('true');
});
it('renders smart-virtual-list element', () => {
expect(wrapper.find(SmartVirtualList).exists()).toBe(true);
});
it('renders label elements for all labels', () => {
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});

View file

@ -4,10 +4,13 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
import { mockRegularLabel } from './mock_data';
const createComponent = ({ label = mockRegularLabel, highlight = true } = {}) =>
const mockLabel = { ...mockRegularLabel, set: true };
const createComponent = ({ label = mockLabel, highlight = true } = {}) =>
shallowMount(LabelItem, {
propsData: {
label,
isLabelSet: label.set,
highlight,
},
});
@ -28,13 +31,29 @@ describe('LabelItem', () => {
it('returns an object containing `backgroundColor` based on `label` prop', () => {
expect(wrapper.vm.labelBoxStyle).toEqual(
expect.objectContaining({
backgroundColor: mockRegularLabel.color,
backgroundColor: mockLabel.color,
}),
);
});
});
});
describe('watchers', () => {
describe('isLabelSet', () => {
it('sets value of `isLabelSet` to `isSet` data prop', () => {
expect(wrapper.vm.isSet).toBe(true);
wrapper.setProps({
isLabelSet: false,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.isSet).toBe(false);
});
});
});
});
describe('methods', () => {
describe('handleClick', () => {
it('sets value of `isSet` data prop to opposite of its current value', () => {
@ -52,7 +71,7 @@ describe('LabelItem', () => {
wrapper.vm.handleClick();
expect(wrapper.emitted('clickLabel')).toBeTruthy();
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockRegularLabel]);
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockLabel]);
});
});
});
@ -105,7 +124,7 @@ describe('LabelItem', () => {
});
it('renders label title', () => {
expect(wrapper.text()).toContain(mockRegularLabel.title);
expect(wrapper.text()).toContain(mockLabel.title);
});
});
});

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mount } from '@vue/test-utils';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => {
@ -28,7 +28,7 @@ describe('Toggle Button', () => {
</smart-virtual-scroll-list>`,
});
return mountComponent(Component);
return mount(Component).vm;
};
afterEach(() => {

View file

@ -9,13 +9,21 @@ describe('AutofocusOnShow directive', () => {
describe('with input invisible on component render', () => {
let el;
beforeAll(() => {
beforeEach(() => {
setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>');
el = document.querySelector('#inputel');
window.IntersectionObserver = class {
observe = jest.fn();
};
});
afterEach(() => {
delete window.IntersectionObserver;
});
it('should bind IntersectionObserver on input element', () => {
spyOn(el, 'focus');
jest.spyOn(el, 'focus').mockImplementation(() => {});
autofocusonshow.inserted(el);
@ -27,7 +35,7 @@ describe('AutofocusOnShow directive', () => {
el.visibilityObserver = {
disconnect: () => {},
};
spyOn(el.visibilityObserver, 'disconnect');
jest.spyOn(el.visibilityObserver, 'disconnect').mockImplementation(() => {});
autofocusonshow.unbind(el);

View file

@ -1,4 +1,4 @@
import testAction from 'spec/helpers/vuex_action_helper';
import testAction from 'helpers/vuex_action_helper';
import * as types from '~/vuex_shared/modules/modal/mutation_types';
import * as actions from '~/vuex_shared/modules/modal/actions';

View file

@ -23,7 +23,7 @@ describe GitlabSchema.types['MergeRequest'] do
source_branch_exists target_branch_exists
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees participants subscribed labels discussion_locked time_estimate
total_time_spent reference
total_time_spent reference author merged_at
]
expect(described_class).to have_graphql_fields(*expected_fields)

View file

@ -1,73 +0,0 @@
import Hook from '~/droplab/hook';
describe('Hook', function() {
describe('class constructor', function() {
beforeEach(function() {
this.trigger = { id: 'id' };
this.list = {};
this.plugins = {};
this.config = {};
this.dropdown = {};
this.dropdownConstructor = spyOnDependency(Hook, 'DropDown').and.returnValue(this.dropdown);
this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
});
it('should set .trigger', function() {
expect(this.hook.trigger).toBe(this.trigger);
});
it('should set .list', function() {
expect(this.hook.list).toBe(this.dropdown);
});
it('should call DropDown constructor', function() {
expect(this.dropdownConstructor).toHaveBeenCalledWith(this.list, this.config);
});
it('should set .type', function() {
expect(this.hook.type).toBe('Hook');
});
it('should set .event', function() {
expect(this.hook.event).toBe('click');
});
it('should set .plugins', function() {
expect(this.hook.plugins).toBe(this.plugins);
});
it('should set .config', function() {
expect(this.hook.config).toBe(this.config);
});
it('should set .id', function() {
expect(this.hook.id).toBe(this.trigger.id);
});
describe('if config argument is undefined', function() {
beforeEach(function() {
this.config = undefined;
this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
});
it('should set .config to an empty object', function() {
expect(this.hook.config).toEqual({});
});
});
describe('if plugins argument is undefined', function() {
beforeEach(function() {
this.plugins = undefined;
this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
});
it('should set .plugins to an empty array', function() {
expect(this.hook.plugins).toEqual([]);
});
});
});
});

View file

@ -1,2 +0,0 @@
export { default } from '../../frontend/vue_mr_widget/mock_data';
export * from '../../frontend/vue_mr_widget/mock_data';

View file

@ -1 +0,0 @@
export * from '../../../../frontend/vue_shared/components/issue/related_issuable_mock_data';

View file

@ -37,6 +37,30 @@ describe 'getting merge request information nested in a project' do
expect(merge_request_graphql_data['webUrl']).to be_present
end
it 'includes author' do
post_graphql(query, current_user: current_user)
expect(merge_request_graphql_data['author']['username']).to eq(merge_request.author.username)
end
it 'includes correct mergedAt value when merged' do
time = 1.week.ago
merge_request.mark_as_merged
merge_request.metrics.update_columns(merged_at: time)
post_graphql(query, current_user: current_user)
retrieved = merge_request_graphql_data['mergedAt']
expect(Time.zone.parse(retrieved)).to be_within(1.second).of(time)
end
it 'includes nil mergedAt value when not merged' do
post_graphql(query, current_user: current_user)
retrieved = merge_request_graphql_data['mergedAt']
expect(retrieved).to be_nil
end
context 'permissions on the merge request' do
it 'includes the permissions for the current user on a public project' do
expected_permissions = {

View file

@ -25,7 +25,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
if resource_name == 'issue'
it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do
it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/218757' do
find("#{form_selector} .note-textarea").send_keys(comment)
click_button 'Comment & close issue'
@ -206,7 +206,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
if resource_name == 'issue'
it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}" do
it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/218757' do
click_button 'Start thread & close issue'
expect(page).to have_content(comment)