diff options
75 files changed, 865 insertions, 782 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 3ea4a24f421..1d12e1bfcbd 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -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', diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 0c2eafeed54..ec1570f0ef9 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -140,7 +140,6 @@ export const dateFormats = { * Currently used in `receiveMetricsDashboardSuccess` action. */ export const endpointKeys = [ - 'metricsEndpoint', 'deploymentsEndpoint', 'dashboardEndpoint', 'dashboardsEndpoint', diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 5acb9ebc043..42d9cd1265c 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -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, }, }); diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index fec2aba4575..0de0c362b0c 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -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, diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js index f08a6402aa6..213a8508aa2 100644 --- a/app/assets/javascripts/monitoring/stores/index.js +++ b/app/assets/javascripts/monitoring/stores/index.js @@ -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(); diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 9d47a9a2ad7..29d6f0cf704 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -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, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js index ab652c9356a..e94e7d46f85 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js @@ -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; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue index 1ef2e8b3bed..af16088b6b9 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue index c95221d71b5..002e741ab96 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue @@ -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; diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 6ac385a8e31..cb4ff7ea0c5 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -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, @@ -3,7 +3,6 @@ set -e cd $(dirname $0)/.. -app_root=$(pwd) case "$USE_WEB_SERVER" in puma|"") # and the "" defines default diff --git a/changelogs/unreleased/ajk-gql-add-mr-author.yml b/changelogs/unreleased/ajk-gql-add-mr-author.yml new file mode 100644 index 00000000000..f4d8048f54b --- /dev/null +++ b/changelogs/unreleased/ajk-gql-add-mr-author.yml @@ -0,0 +1,5 @@ +--- +title: Add missing Merge Request fields +merge_request: 30935 +author: +type: added diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 1c5b00b36ad..54c5181853f 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -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. diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index f0903e39012..1a8fbf054ac 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -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: diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 9e351124126..7cfd4196a34 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -5768,6 +5768,11 @@ type MergeRequest implements Noteable { ): UserConnection """ + User who created this merge request + """ + author: User + + """ Timestamp of when the merge request was created """ createdAt: Time! @@ -5918,6 +5923,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 """ milestone: Milestone diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index c1addf68b79..db737743d32 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -16086,6 +16086,20 @@ "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", "args": [ @@ -16500,6 +16514,20 @@ "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", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index dfeb4e0f46d..7d0cf9eb322 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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 | diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md index 6630f8e779e..e2033e97d0d 100644 --- a/doc/development/telemetry/usage_ping.md +++ b/doc/development/telemetry/usage_ping.md @@ -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, + ... + } + } +} +``` diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 093780f36db..21a4155df40 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -8,35 +8,28 @@ 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). - -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. - -![Container Scanning Widget](img/container_scanning_v13_0.png) - -## Contribute your scanner +NOTE: **Note:** +To integrate security scanners other than Clair and Klar into GitLab, see +[Security scanner integration](../../../development/integrations/secure.md). -The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab. +You can enable container scanning by doing one of the following: -## Use cases +- [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). -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. +GitLab compares the found vulnerabilities between the source and target branches, and shows the +information directly in the merge request. -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. +![Container Scanning Widget](img/container_scanning_v13_0.png) <!-- 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 @@ -44,26 +37,21 @@ to perform audits for your Docker-based apps. ## Requirements -To enable Container Scanning in your pipeline, you need: - -- 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: +To enable Container Scanning in your pipeline, you need the following: + +- [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: + +- 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). -Add the following to your `.gitlab-ci.yml` file: +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 diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 9a08dd0c979..bd6a2dda6f8 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -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 diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 0d191931169..47250df97a6 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -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) diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb index 0af621b6741..cbf3e7b8429 100644 --- a/lib/gitlab/graphql/authorize/authorize_field_service.rb +++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb @@ -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 diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 8e9f220ec85..795878b71f2 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -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 diff --git a/scripts/lint-conflicts.sh b/scripts/lint-conflicts.sh index 239e227a41e..2d240e420d9 100755 --- a/scripts/lint-conflicts.sh +++ b/scripts/lint-conflicts.sh @@ -1,5 +1,5 @@ #!/bin/sh -output=`git grep -En '^<<<<<<< '` +output=$(git grep -En '^<<<<<<< ') echo $output test -z "$output" diff --git a/spec/frontend/droplab/hook_spec.js b/spec/frontend/droplab/hook_spec.js new file mode 100644 index 00000000000..11488cab521 --- /dev/null +++ b/spec/frontend/droplab/hook_spec.js @@ -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([]); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index b2c9fe93cde..a616c051f20 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -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(); }); diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js index cc0ac348b11..125b81b5b96 100644 --- a/spec/frontend/monitoring/components/dashboard_template_spec.js +++ b/spec/frontend/monitoring/components/dashboard_template_spec.js @@ -14,7 +14,9 @@ describe('Dashboard template', () => { let mock; beforeEach(() => { - store = createStore(); + store = createStore({ + currentEnvironmentName: 'production', + }); mock = new MockAdapter(axios); setupAllDashboards(store); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 4acf2b87ee1..298687341e4 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -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: '', diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index 8914f2e66ea..423ff59188d 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -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, diff --git a/spec/frontend/monitoring/store/index_spec.js b/spec/frontend/monitoring/store/index_spec.js new file mode 100644 index 00000000000..4184687eec8 --- /dev/null +++ b/spec/frontend/monitoring/store/index_spec.js @@ -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), + }); + }); +}); diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 7bd1b3f5c5b..31002ebd860 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -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'); diff --git a/spec/javascripts/toggle_buttons_spec.js b/spec/frontend/toggle_buttons_spec.js index 09756ff76ec..09a4bd53c09 100644 --- a/spec/javascripts/toggle_buttons_spec.js +++ b/spec/frontend/toggle_buttons_spec.js @@ -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); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js index f78fcfb52b4..f78fcfb52b4 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js index a942a9dec87..05690aa1248 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js index 55af2baa924..58ed92298bf 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js index 3cbaa47c832..b492a69fb3d 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js index d15c3552b4a..7a932feb3a7 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -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; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js index b566876fe1d..00e79a22485 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js index 883c41085fa..309aec179d9 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -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'; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js index 5b293862b16..c8c2bff033c 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -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: { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js index a152bd01916..0c4ec7ed99b 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js index 20bda024d89..6c3b4a01659 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/frontend/vue_mr_widget/components/review_app_link_spec.js index 242193c7b3d..7b063653a93 100644 --- a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js +++ b/spec/frontend/vue_mr_widget/components/review_app_link_spec.js @@ -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', { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js index 29a257b0e24..4bdc6c95f22 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js index 73b65178ecf..e2caa6e8092 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js @@ -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); + }); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js index efccd507fe2..56d55c9afac 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js index bbbaed0d2f5..322f440763c 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 9035bc6f65d..d3482b457ad 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -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: '', }, diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index ef76e617c07..f591393d721 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -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); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js index 423c800bfbc..1921599ae95 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -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); + }); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js index 06d236064dd..222cb74cc66 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js index 47b989e2022..3f03ebdb047 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js index b1cb91663c9..63e93074857 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js index bd0bd36ebc2..bd0bd36ebc2 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index 0bca86b12b2..8847e4e6bdd 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js index 85f65d024a8..179adef12d9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 9ba429c3d20..1f0d6a7378c 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -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); + }); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js index 11eb0fef9b2..38920846a50 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -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', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js index b70d580ed04..b70d580ed04 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js index e8367caa438..33e52f4fd36 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js @@ -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); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js index 9153231b974..6fa555b4fc4 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -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); + }); }); }); }); diff --git a/spec/javascripts/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js index bb927b7d7f2..f9e56774526 100644 --- a/spec/javascripts/vue_shared/components/file_finder/index_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js @@ -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); }); diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/frontend/vue_shared/components/icon_spec.js index 5a3e483fb03..a448953cc8e 100644 --- a/spec/javascripts/vue_shared/components/icon_spec.js +++ b/spec/frontend/vue_shared/components/icon_spec.js @@ -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 }, diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/frontend/vue_shared/components/panel_resizer_spec.js index d65ee8eeb2d..d8b903e5bfd 100644 --- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js +++ b/spec/frontend/vue_shared/components/panel_resizer_spec.js @@ -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], diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 74c769f86a3..1504e1521d3 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -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); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js index 401d208da5c..ad3f073fdf9 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js @@ -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); }); }); }); diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js index 47ebdc505c9..e5f9b94128e 100644 --- a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js +++ b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js @@ -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(() => { diff --git a/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js index f1ca5f61496..90530b7d5c2 100644 --- a/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js +++ b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js @@ -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); diff --git a/spec/javascripts/vuex_shared/modules/modal/actions_spec.js b/spec/frontend/vuex_shared/modules/modal/actions_spec.js index 2c4cb845424..353dbcb522f 100644 --- a/spec/javascripts/vuex_shared/modules/modal/actions_spec.js +++ b/spec/frontend/vuex_shared/modules/modal/actions_spec.js @@ -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'; diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index ec8ea01bcea..0f48264c99f 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -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) diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js deleted file mode 100644 index 40470436f19..00000000000 --- a/spec/javascripts/droplab/hook_spec.js +++ /dev/null @@ -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([]); - }); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js deleted file mode 100644 index 7783fcb6f93..00000000000 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from '../../frontend/vue_mr_widget/mock_data'; -export * from '../../frontend/vue_mr_widget/mock_data'; diff --git a/spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js b/spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js deleted file mode 100644 index 3c42f0c2aa9..00000000000 --- a/spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../../../frontend/vue_shared/components/issue/related_issuable_mock_data'; diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index 8d8c31c335d..643532bf2e2 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -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 = { diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 6e584cb44e2..e52ac02a2cc 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -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) |