diff options
78 files changed, 1269 insertions, 228 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue index f1716182e5f..7fe74eb1da8 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue @@ -197,11 +197,11 @@ export default { </template> <template #cell(startedAt)="{ item }"> - <time-ago :time="item.startedAt" /> + <time-ago v-if="item.startedAt" :time="item.startedAt" /> </template> <template #cell(endedAt)="{ item }"> - <time-ago :time="item.endedAt" /> + <time-ago v-if="item.endedAt" :time="item.endedAt" /> </template> <template #cell(title)="{ item }"> diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 057cdb6cc4c..e42f6e5ba48 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -82,7 +82,7 @@ function renderMermaidEl(el) { return; } - svg.classList.add('mermaid'); + svg.classList.add('mermaid', 'mw-100'); // pre > code > svg svg.closest('pre').replaceWith(svg); diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js index 5fe1e32e37e..a4db6481720 100644 --- a/app/assets/javascripts/ci_variable_list/constants.js +++ b/app/assets/javascripts/ci_variable_list/constants.js @@ -4,7 +4,7 @@ import { __ } from '~/locale'; export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable'; export const displayText = { - variableText: __('Var'), + variableText: __('Variable'), fileText: __('File'), allEnvironmentsText: __('All (default)'), }; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 3adf0cf073f..3d11c683711 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -88,8 +88,8 @@ export default { commit(projectId, payload) { return Api.commitMultiple(projectId, payload); }, - getFiles(projectUrl, ref) { - const url = `${projectUrl}/-/files/${ref}`; + getFiles(projectPath, ref) { + const url = `${gon.relative_url_root}/${projectPath}/-/files/${ref}`; return axios.get(url, { params: { format: 'json' } }); }, lastCommitPipelines({ getters }) { diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 7d48f0adc4c..1ca608f1287 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -59,7 +59,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) => commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); service - .getFiles(selectedProject.web_url, ref) + .getFiles(selectedProject.path_with_namespace, ref) .then(({ data }) => { const { entries, treeList } = decorateFiles({ data, diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js index 7921650e8a0..229e0a62c51 100644 --- a/app/assets/javascripts/image_diff/helpers/badge_helper.js +++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js @@ -15,7 +15,7 @@ export function createImageBadge(noteId, { x, y }, classNames = []) { export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) { const buttonEl = createImageBadge(noteId, coordinate, ['badge', 'badge-pill']); - buttonEl.innerText = badgeText; + buttonEl.textContent = badgeText; containerEl.appendChild(buttonEl); } @@ -32,6 +32,6 @@ export function addAvatarBadge(el, event) { // Add badge to new comment const avatarBadgeEl = el.querySelector(`#${noteId} .badge`); - avatarBadgeEl.innerText = badgeNumber; + avatarBadgeEl.textContent = badgeNumber; avatarBadgeEl.classList.remove('hidden'); } diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js index 74ca907c99f..a61e5f01f9b 100644 --- a/app/assets/javascripts/image_diff/helpers/dom_helper.js +++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js @@ -11,12 +11,12 @@ export function setPositionDataAttribute(el, options) { export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) { const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge'); - avatarBadgeEl.innerText = newBadgeNumber; + avatarBadgeEl.textContent = newBadgeNumber; } export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) { const discussionBadgeEl = discussionEl.querySelector('.badge'); - discussionBadgeEl.innerText = newBadgeNumber; + discussionBadgeEl.textContent = newBadgeNumber; } export function toggleCollapsed(event) { diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index 89f696dd1d8..079f4a63f6e 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -128,7 +128,7 @@ export default class ImageDiff { const updatedBadgeNumber = index; const discussionEl = this.el.querySelector(`#discussion_${discussionId}`); - imageBadgeEls[index].innerText = updatedBadgeNumber; + imageBadgeEls[index].textContent = updatedBadgeNumber; imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber); imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a525f660801..6e695de447d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1396,7 +1396,7 @@ export default class Notes { } /** - * Check if note does not exists on page + * Check if note does not exist on page */ static isNewNote(noteEntity, noteIds) { return $.inArray(noteEntity.id, noteIds) === -1; diff --git a/app/controllers/projects/design_management/designs/raw_images_controller.rb b/app/controllers/projects/design_management/designs/raw_images_controller.rb new file mode 100644 index 00000000000..beb7e9d294b --- /dev/null +++ b/app/controllers/projects/design_management/designs/raw_images_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Returns full-size design images +module Projects + module DesignManagement + module Designs + class RawImagesController < Projects::DesignManagement::DesignsController + include SendsBlob + + skip_before_action :default_cache_headers, only: :show + + def show + blob = design_repository.blob_at(ref, design.full_path) + + send_blob(design_repository, blob, inline: false, allow_caching: project.public?) + end + + private + + def design_repository + @design_repository ||= project.design_repository + end + + def ref + sha || design_repository.root_ref + end + end + end + end +end diff --git a/app/controllers/projects/design_management/designs/resized_image_controller.rb b/app/controllers/projects/design_management/designs/resized_image_controller.rb new file mode 100644 index 00000000000..50a997f32db --- /dev/null +++ b/app/controllers/projects/design_management/designs/resized_image_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Returns smaller sized design images +module Projects + module DesignManagement + module Designs + class ResizedImageController < Projects::DesignManagement::DesignsController + include SendFileUpload + + before_action :validate_size! + + skip_before_action :default_cache_headers, only: :show + + def show + relation = design.actions + relation = relation.up_to_version(sha) if sha + action = relation.most_recent.first + + return render_404 unless action + + # This controller returns a 404 if the the `size` param + # is not one of our specific sizes, so using `send` here is safe. + uploader = action.public_send(:"image_#{size}") # rubocop:disable GitlabSecurity/PublicSend + + return render_404 unless uploader.file # The image has not been processed + + if stale?(etag: action.cache_key) + workhorse_set_content_type! + + send_upload(uploader, attachment: design.filename) + end + end + + private + + def validate_size! + render_404 unless ::DesignManagement::DESIGN_IMAGE_SIZES.include?(size) + end + + def size + params[:id] + end + end + end + end +end diff --git a/app/controllers/projects/design_management/designs_controller.rb b/app/controllers/projects/design_management/designs_controller.rb new file mode 100644 index 00000000000..fec09fa9515 --- /dev/null +++ b/app/controllers/projects/design_management/designs_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Projects::DesignManagement::DesignsController < Projects::ApplicationController + before_action :authorize_read_design! + + private + + def authorize_read_design! + unless can?(current_user, :read_design, design) + access_denied! + end + end + + def design + @design ||= project.designs.find(params[:design_id]) + end + + def sha + params[:sha].presence + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d759983dafa..fa4b91c5e02 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -21,7 +21,6 @@ class Projects::IssuesController < Projects::ApplicationController prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) } prepend_before_action :authenticate_user!, only: [:new, :export_csv] - # designs is only applicable to EE, but defining a prepend_before_action in EE code would overwrite this prepend_before_action :store_uri, only: [:new, :show, :designs] before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] diff --git a/app/graphql/types/alert_management/alert_sort_enum.rb b/app/graphql/types/alert_management/alert_sort_enum.rb index 76ef43d9dcf..e6d38af8170 100644 --- a/app/graphql/types/alert_management/alert_sort_enum.rb +++ b/app/graphql/types/alert_management/alert_sort_enum.rb @@ -11,9 +11,9 @@ module Types value 'END_TIME_ASC', 'End time by ascending order', value: :end_time_asc value 'END_TIME_DESC', 'End time by descending order', value: :end_time_desc value 'CREATED_TIME_ASC', 'Created time by ascending order', value: :created_at_asc - value 'CREATED_TIME_DESC', 'Created time by ascending order', value: :created_at_desc - value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_desc - value 'UPDATED_TIME_DESC', 'Created time by ascending order', value: :updated_at_desc + value 'CREATED_TIME_DESC', 'Created time by descending order', value: :created_at_desc + value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_asc + value 'UPDATED_TIME_DESC', 'Created time by descending order', value: :updated_at_desc value 'EVENTS_COUNT_ASC', 'Events count by ascending order', value: :events_count_asc value 'EVENTS_COUNT_DESC', 'Events count by descending order', value: :events_count_desc value 'SEVERITY_ASC', 'Severity by ascending order', value: :severity_asc diff --git a/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml new file mode 100644 index 00000000000..dd36d52f1c4 --- /dev/null +++ b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml @@ -0,0 +1,5 @@ +--- +title: Fix incorrect number of errors returned when querying sentry errors +merge_request: 31252 +author: +type: fixed diff --git a/changelogs/unreleased/36810-webide-branch-with-path.yml b/changelogs/unreleased/36810-webide-branch-with-path.yml new file mode 100644 index 00000000000..2238101799c --- /dev/null +++ b/changelogs/unreleased/36810-webide-branch-with-path.yml @@ -0,0 +1,5 @@ +--- +title: In WebIDE get files with relative path instead of web_url +merge_request: 31478 +author: +type: fixed diff --git a/changelogs/unreleased/change-var-to-variable.yml b/changelogs/unreleased/change-var-to-variable.yml new file mode 100644 index 00000000000..ec2983696d4 --- /dev/null +++ b/changelogs/unreleased/change-var-to-variable.yml @@ -0,0 +1,5 @@ +--- +title: Change Var to Variable text +merge_request: 30878 +author: +type: changed diff --git a/changelogs/unreleased/sh-handle-invalid-gitattributes.yml b/changelogs/unreleased/sh-handle-invalid-gitattributes.yml new file mode 100644 index 00000000000..56378d18297 --- /dev/null +++ b/changelogs/unreleased/sh-handle-invalid-gitattributes.yml @@ -0,0 +1,5 @@ +--- +title: Ignore .gitattributes if they contain invalid byte sequences +merge_request: 30922 +author: +type: fixed diff --git a/config/routes/issues.rb b/config/routes/issues.rb index 51b4637b89f..04a935c1016 100644 --- a/config/routes/issues.rb +++ b/config/routes/issues.rb @@ -13,6 +13,7 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do get :realtime_changes post :create_merge_request get :discussions, format: :json + get '/designs(/*vueroute)', to: 'issues#designs', as: :designs, format: false end collection do diff --git a/config/routes/project.rb b/config/routes/project.rb index ac8f621b2b6..0cd880a8c46 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -305,6 +305,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + namespace :design_management do + namespace :designs, path: 'designs/:design_id(/:sha)', constraints: -> (params) { params[:sha].nil? || Gitlab::Git.commit_id?(params[:sha]) } do + resource :raw_image, only: :show + resources :resized_image, only: :show, constraints: -> (params) { DesignManagement::DESIGN_IMAGE_SIZES.include?(params[:id]) } + end + end + draw :issues draw :merge_requests diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 89b3bbac938..cc21b169120 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -258,7 +258,7 @@ enum AlertManagementAlertSort { CREATED_TIME_ASC """ - Created time by ascending order + Created time by descending order """ CREATED_TIME_DESC @@ -318,7 +318,7 @@ enum AlertManagementAlertSort { UPDATED_TIME_ASC """ - Created time by ascending order + Created time by descending order """ UPDATED_TIME_DESC @@ -4303,6 +4303,41 @@ type Group { ): VulnerabilityConnection """ + Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups + """ + vulnerabilitiesCountByDayAndSeverity( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Last day for which to fetch vulnerability history + """ + endDate: ISO8601Date! + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + First day for which to fetch vulnerability history + """ + startDate: ISO8601Date! + ): VulnerabilitiesCountByDayAndSeverityConnection + + """ Web URL of the group """ webUrl: String! @@ -4324,6 +4359,11 @@ enum HealthStatus { onTrack } +""" +An ISO 8601-encoded date +""" +scalar ISO8601Date + type InstanceSecurityDashboard { """ Projects selected in Instance Security Dashboard @@ -8223,6 +8263,41 @@ type Query { """ state: [VulnerabilityState!] ): VulnerabilityConnection + + """ + Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard + """ + vulnerabilitiesCountByDayAndSeverity( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Last day for which to fetch vulnerability history + """ + endDate: ISO8601Date! + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + First day for which to fetch vulnerability history + """ + startDate: ISO8601Date! + ): VulnerabilitiesCountByDayAndSeverityConnection } """ @@ -10721,6 +10796,61 @@ enum VisibilityScopesEnum { } """ +Represents the number of vulnerabilities for a particular severity on a particular day +""" +type VulnerabilitiesCountByDayAndSeverity { + """ + Number of vulnerabilities + """ + count: Int + + """ + Date for the count + """ + day: ISO8601Date + + """ + Severity of the counted vulnerabilities + """ + severity: VulnerabilitySeverity +} + +""" +The connection type for VulnerabilitiesCountByDayAndSeverity. +""" +type VulnerabilitiesCountByDayAndSeverityConnection { + """ + A list of edges. + """ + edges: [VulnerabilitiesCountByDayAndSeverityEdge] + + """ + A list of nodes. + """ + nodes: [VulnerabilitiesCountByDayAndSeverity] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type VulnerabilitiesCountByDayAndSeverityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: VulnerabilitiesCountByDayAndSeverity +} + +""" Represents a vulnerability. """ type Vulnerability { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 3e845667e80..c7614b8dd6b 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -786,7 +786,7 @@ }, { "name": "CREATED_TIME_DESC", - "description": "Created time by ascending order", + "description": "Created time by descending order", "isDeprecated": false, "deprecationReason": null }, @@ -798,7 +798,7 @@ }, { "name": "UPDATED_TIME_DESC", - "description": "Created time by ascending order", + "description": "Created time by descending order", "isDeprecated": false, "deprecationReason": null }, @@ -11940,6 +11940,87 @@ "deprecationReason": null }, { + "name": "vulnerabilitiesCountByDayAndSeverity", + "description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups", + "args": [ + { + "name": "startDate", + "description": "First day for which to fetch vulnerability history", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "endDate", + "description": "Last day for which to fetch vulnerability history", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverityConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "webUrl", "description": "Web URL of the group", "args": [ @@ -12036,6 +12117,16 @@ "possibleTypes": null }, { + "kind": "SCALAR", + "name": "ISO8601Date", + "description": "An ISO 8601-encoded date", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "InstanceSecurityDashboard", "description": null, @@ -24232,6 +24323,87 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "vulnerabilitiesCountByDayAndSeverity", + "description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard", + "args": [ + { + "name": "startDate", + "description": "First day for which to fetch vulnerability history", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "endDate", + "description": "Last day for which to fetch vulnerability history", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverityConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -31895,6 +32067,173 @@ }, { "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverity", + "description": "Represents the number of vulnerabilities for a particular severity on a particular day", + "fields": [ + { + "name": "count", + "description": "Number of vulnerabilities", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "day", + "description": "Date for the count", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "severity", + "description": "Severity of the counted vulnerabilities", + "args": [ + + ], + "type": { + "kind": "ENUM", + "name": "VulnerabilitySeverity", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverityConnection", + "description": "The connection type for VulnerabilitiesCountByDayAndSeverity.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverityEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverity", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverityEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilitiesCountByDayAndSeverity", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "Vulnerability", "description": "Represents a vulnerability.", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3ca7164bff5..f289a057cbc 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1633,6 +1633,16 @@ Autogenerated return type of UpdateSnippet | --- | ---- | ---------- | | `createSnippet` | Boolean! | Indicates the user can perform `create_snippet` on this resource | +## VulnerabilitiesCountByDayAndSeverity + +Represents the number of vulnerabilities for a particular severity on a particular day + +| Name | Type | Description | +| --- | ---- | ---------- | +| `count` | Int | Number of vulnerabilities | +| `day` | ISO8601Date | Date for the count | +| `severity` | VulnerabilitySeverity | Severity of the counted vulnerabilities | + ## Vulnerability Represents a vulnerability. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 507e548b8d8..a7524070494 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -756,7 +756,7 @@ Note that `script: rake test` has been overwritten by `script: rake rspec`. If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script). -`.tests` in this example is a [hidden key](#hide-jobs), but it's +`.tests` in this example is a [hidden job](#hide-jobs), but it's possible to inherit from regular jobs as well. `extends` supports multi-level inheritance, however it's not recommended to diff --git a/doc/user/analytics/value_stream_analytics.md b/doc/user/analytics/value_stream_analytics.md index 1ded4a0cf0a..a544de60413 100644 --- a/doc/user/analytics/value_stream_analytics.md +++ b/doc/user/analytics/value_stream_analytics.md @@ -18,9 +18,6 @@ spent in each stage defined in the process. For information on how to contribute to the development of Value Stream Analytics, see our [contributor documentation](../../development/value_stream_analytics.md). -NOTE: **Note:** -Use the `cycle_analytics` feature flag to enable at the group level. - Value Stream Analytics is useful in order to quickly determine the velocity of a given project. It points to bottlenecks in the development process, enabling management to uncover, triage, and identify the root cause of slowdowns in the software development life cycle. @@ -33,7 +30,7 @@ calculates a separate median for each stage. Value Stream Analytics is available: - From GitLab 12.9, at the group level via **Group > Analytics > Value Stream**. **(PREMIUM)** -- At the project level via **Project > Value Stream Analytics**. +- At the project level via **Project > Analytics > Value Stream**. There are seven stages that are tracked as part of the Value Stream Analytics calculations. @@ -300,15 +297,6 @@ toggled to show data for merge requests and further refined for specific group-l By default the top group-level labels (max. 10) are pre-selected, with the ability to select up to a total of 15 labels. -### Disabling chart - -This chart is enabled by default. If you have a self-managed instance, an -administrator can open a Rails console and disable it with the following command: - -```ruby -Feature.disable(:tasks_by_type_chart) -``` - ## Permissions The current permissions on the Project Value Stream Analytics dashboard are: @@ -331,14 +319,6 @@ For Value Stream Analytics functionality introduced in GitLab 12.3 and later: - Features are available only on [Premium or Silver tiers](https://about.gitlab.com/pricing/) and above. -## Troubleshooting - -If you see an error as listed in the following table, try the noted solution: - -| Error | Solution | -|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| There was an error fetching the top labels. | Manually enable tasks by type feature in the [rails console](../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session), specifically `Feature.enable(:tasks_by_type_chart)`. | - ## More resources Learn more about Value Stream Analytics in the following resources: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 5ce3353b734..2561be148ac 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -179,6 +179,14 @@ module API end end + def find_tag!(tag_name) + if Gitlab::GitRefValidator.validate(tag_name) + user_project.repository.find_tag(tag_name) || not_found!('Tag') + else + render_api_error!('The tag refname is invalid', 400) + end + end + # rubocop: disable CodeReuse/ActiveRecord def find_project_issue(iid, project_id = nil) project = project_id ? find_project!(project_id) : user_project diff --git a/lib/gitlab/git/attributes_parser.rb b/lib/gitlab/git/attributes_parser.rb index 8b9d74ae8e7..630b1aba2f5 100644 --- a/lib/gitlab/git/attributes_parser.rb +++ b/lib/gitlab/git/attributes_parser.rb @@ -85,6 +85,8 @@ module Gitlab yield line.strip end + # Catch invalid byte sequences + rescue ArgumentError end private diff --git a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb index 1f01dd07571..12e047420bf 100644 --- a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb +++ b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb @@ -23,6 +23,20 @@ module Gitlab alias_method :has_next_page, :next_page? alias_method :has_previous_page, :previous_page? + + private + + def load_nodes + @nodes ||= begin + # As the pagination happens externally we just grab all the nodes + limited_nodes = items + + limited_nodes = limited_nodes.first(first) if first + limited_nodes = limited_nodes.last(last) if last + + limited_nodes + end + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b339c52ce09..80100b281dd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23340,7 +23340,7 @@ msgstr "" msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" -msgid "Var" +msgid "Variable" msgstr "" msgid "Variable will be masked in job logs." diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb new file mode 100644 index 00000000000..30d2b79a92f --- /dev/null +++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::DesignManagement::Designs::RawImagesController do + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project, :private) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:viewer) { issue.author } + let(:design_id) { design.id } + let(:sha) { design.versions.first.sha } + let(:filename) { design.filename } + + before do + enable_design_management + end + + describe 'GET #show' do + subject do + get(:show, + params: { + namespace_id: project.namespace, + project_id: project, + design_id: design_id, + sha: sha + }) + end + + before do + sign_in(viewer) + end + + context 'when the design is not an LFS file' do + let_it_be(:design) { create(:design, :with_file, issue: issue, versions_count: 2) } + + # For security, .svg images should only ever be served with Content-Disposition: attachment. + # If this specs ever fails we must assess whether we should be serving svg images. + # See https://gitlab.com/gitlab-org/gitlab/issues/12771 + it 'serves files with `Content-Disposition: attachment`' do + subject + + expect(response.header['Content-Disposition']).to eq('attachment') + expect(response).to have_gitlab_http_status(:ok) + end + + it 'serves files with Workhorse' do + subject + + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + expect(response).to have_gitlab_http_status(:ok) + end + + it_behaves_like 'project cache control headers' + + context 'when the user does not have permission' do + let_it_be(:viewer) { create(:user) } + + specify do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when design does not exist' do + let(:design_id) { 'foo' } + + specify do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'sha param' do + let(:newest_version) { design.versions.ordered.first } + let(:oldest_version) { design.versions.ordered.last } + + shared_examples 'a successful request for sha' do + it do + expect_next_instance_of(DesignManagement::Repository) do |repository| + expect(repository).to receive(:blob_at).with(expected_ref, design.full_path).and_call_original + end + + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + specify { expect(newest_version.sha).not_to eq(oldest_version.sha) } + + context 'when sha is the newest version sha' do + let(:sha) { newest_version.sha } + let(:expected_ref) { sha } + + it_behaves_like 'a successful request for sha' + end + + context 'when sha is the oldest version sha' do + let(:sha) { oldest_version.sha } + let(:expected_ref) { sha } + + it_behaves_like 'a successful request for sha' + end + + context 'when sha is nil' do + let(:sha) { nil } + let(:expected_ref) { 'master' } + + it_behaves_like 'a successful request for sha' + end + end + end + + context 'when the design is an LFS file' do + let_it_be(:design) { create(:design, :with_lfs_file, issue: issue) } + + # For security, .svg images should only ever be served with Content-Disposition: attachment. + # If this specs ever fails we must assess whether we should be serving svg images. + # See https://gitlab.com/gitlab-org/gitlab/issues/12771 + it 'serves files with `Content-Disposition: attachment`' do + subject + + expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename})) + end + + it 'sets appropriate caching headers' do + subject + + expect(response.header['ETag']).to be_present + expect(response.header['Cache-Control']).to eq("max-age=60, private") + end + end + + # Pass `skip_lfs_disabled_tests: true` to this shared example to disable + # the test scenarios for when LFS is disabled globally. + # + # When LFS is disabled then the design management feature also becomes disabled. + # When the feature is disabled, the `authorize :read_design` check within the + # controller will never authorize the user. Therefore #show will return a 403 and + # we cannot test the data that it serves. + it_behaves_like 'a controller that can serve LFS files', skip_lfs_disabled_tests: true do + let(:file) { fixture_file_upload('spec/fixtures/dk.png', '`/png') } + let(:lfs_pointer) { Gitlab::Git::LfsPointerFile.new(file.read) } + let(:design) { create(:design, :with_lfs_file, file: lfs_pointer.pointer, issue: issue) } + let(:lfs_oid) { project.design_repository.blob_at('HEAD', design.full_path).lfs_oid } + let(:filepath) { design.full_path } + end + end +end diff --git a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb new file mode 100644 index 00000000000..9a3fee5b43a --- /dev/null +++ b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::DesignManagement::Designs::ResizedImageController do + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project, :private) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:viewer) { issue.author } + let_it_be(:size) { :v432x230 } + let(:design) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 2) } + let(:design_id) { design.id } + let(:sha) { design.versions.first.sha } + + before do + # TODO these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, + # and the `design` factory used in this test uses the `:with_smaller_image_versions` + # trait, which calls `GenerateImageVersionsService` to generate the + # smaller image versions. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + enable_design_management + end + + describe 'GET #show' do + subject do + get(:show, + params: { + namespace_id: project.namespace, + project_id: project, + design_id: design_id, + sha: sha, + id: size + }) + end + + before do + sign_in(viewer) + subject + end + + context 'when the user does not have permission' do + let_it_be(:viewer) { create(:user) } + + specify do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'Response headers' do + it 'completes the request successfully' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'sets Content-Disposition as attachment' do + filename = design.filename + + expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename})) + end + + it 'serves files with Workhorse' do + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + end + + it 'sets appropriate caching headers' do + expect(response.header['Cache-Control']).to eq('private') + expect(response.header['ETag']).to be_present + end + end + + context 'when design does not exist' do + let(:design_id) { 'foo' } + + specify do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when size is invalid' do + let_it_be(:size) { :foo } + + it 'returns a 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'sha param' do + let(:newest_version) { design.versions.ordered.first } + let(:oldest_version) { design.versions.ordered.last } + + # The design images generated by Factorybot are identical, so + # refer to the `ETag` header, which is uniquely generated from the Action + # (the record that represents the design at a specific version), to + # verify that the correct file is being returned. + def etag(action) + ActionDispatch::TestResponse.new.send(:generate_weak_etag, [action.cache_key, '']) + end + + specify { expect(newest_version.sha).not_to eq(oldest_version.sha) } + + context 'when sha is the newest version sha' do + let(:sha) { newest_version.sha } + + it 'serves the newest image' do + action = newest_version.actions.first + + expect(response.header['ETag']).to eq(etag(action)) + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when sha is the oldest version sha' do + let(:sha) { oldest_version.sha } + + it 'serves the oldest image' do + action = oldest_version.actions.first + + expect(response.header['ETag']).to eq(etag(action)) + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when sha is nil' do + let(:sha) { nil } + + it 'serves the newest image' do + action = newest_version.actions.first + + expect(response.header['ETag']).to eq(etag(action)) + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when sha is not a valid version sha' do + let(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } + + it 'returns a 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when design does not have a smaller image size available' do + let(:design) { create(:design, :with_file, issue: issue) } + + it 'returns a 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 053932944bb..a22dc77997b 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1719,6 +1719,33 @@ describe Projects::IssuesController do end end + describe 'GET #designs' do + context 'when project has moved' do + let(:new_project) { create(:project) } + let(:issue) { create(:issue, project: new_project) } + + before do + sign_in(user) + + project.route.destroy + new_project.redirect_routes.create!(path: project.full_path) + new_project.add_developer(user) + end + + it 'redirects from an old issue/designs correctly' do + get :designs, + params: { + namespace_id: project.namespace, + project_id: project, + id: issue + } + + expect(response).to redirect_to(designs_project_issue_path(new_project, issue)) + expect(response).to have_gitlab_http_status(:found) + end + end + end + context 'private project with token authentication' do let(:private_project) { create(:project, :private) } diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js index cb2531663bd..d7170e71a96 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -9,6 +9,7 @@ import { GlIcon, GlTab, } from '@gitlab/ui'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import AlertManagementList from '~/alert_management/components/alert_management_list.vue'; import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants'; @@ -24,6 +25,7 @@ describe('AlertManagementList', () => { const findStatusDropdown = () => wrapper.find(GlNewDropdown); const findStatusFilterTabs = () => wrapper.findAll(GlTab); const findNumberOfAlertsBadge = () => wrapper.findAll(GlBadge); + const findDateFields = () => wrapper.findAll(TimeAgo); function mountComponent({ props = { @@ -198,5 +200,45 @@ describe('AlertManagementList', () => { ).toBe(true); }); }); + + describe('handle date fields', () => { + it('should display time ago dates when values provided', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: [ + { + iid: 1, + startedAt: '2020-03-17T23:18:14.996Z', + endedAt: '2020-04-17T23:18:14.996Z', + severity: 'high', + }, + ], + errored: false, + }, + loading: false, + }); + expect(findDateFields().length).toBe(2); + }); + + it('should not display time ago dates when values not provided', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: [ + { + iid: 1, + startedAt: null, + endedAt: null, + severity: 'high', + }, + ], + errored: false, + }, + loading: false, + }); + expect(findDateFields().exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js index 09c6cd9de21..7dab33050d9 100644 --- a/spec/frontend/ci_variable_list/services/mock_data.js +++ b/spec/frontend/ci_variable_list/services/mock_data.js @@ -8,7 +8,7 @@ export default { protected: false, secret_value: 'test_val', value: 'test_val', - variable_type: 'Var', + variable_type: 'Variable', }, ], @@ -44,7 +44,7 @@ export default { protected: false, secret_value: 'test_val', value: 'test_val', - variable_type: 'Var', + variable_type: 'Variable', }, { environment_scope: 'All (default)', @@ -104,7 +104,7 @@ export default { id: 28, key: 'goku_var', value: 'goku_val', - variable_type: 'Var', + variable_type: 'Variable', protected: true, masked: true, environment_scope: 'staging', @@ -114,7 +114,7 @@ export default { id: 25, key: 'test_var_4', value: 'test_val_4', - variable_type: 'Var', + variable_type: 'Variable', protected: false, masked: false, environment_scope: 'production', @@ -134,7 +134,7 @@ export default { id: 24, key: 'test_var_3', value: 'test_val_3', - variable_type: 'Var', + variable_type: 'Variable', protected: false, masked: false, environment_scope: 'All (default)', @@ -144,7 +144,7 @@ export default { id: 26, key: 'test_var_5', value: 'test_val_5', - variable_type: 'Var', + variable_type: 'Variable', protected: false, masked: false, environment_scope: 'production', diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js index 8652359f3df..ce0792d0353 100644 --- a/spec/frontend/ci_variable_list/store/mutations_spec.js +++ b/spec/frontend/ci_variable_list/store/mutations_spec.js @@ -47,7 +47,7 @@ describe('CI variable list mutations', () => { describe('CLEAR_MODAL', () => { it('should clear modal state ', () => { const modalState = { - variable_type: 'Var', + variable_type: 'Variable', key: '', secret_value: '', protected: false, diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 658ad37d7f2..f4d4122bd5a 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -221,4 +221,37 @@ describe('IDE services', () => { }); }); }); + + describe('getFiles', () => { + let mock; + let relativeUrlRoot; + const TEST_RELATIVE_URL_ROOT = 'blah-blah'; + + beforeEach(() => { + jest.spyOn(axios, 'get'); + relativeUrlRoot = gon.relative_url_root; + gon.relative_url_root = TEST_RELATIVE_URL_ROOT; + + mock = new MockAdapter(axios); + + mock + .onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`) + .reply(200, [TEST_FILE_PATH]); + }); + + afterEach(() => { + mock.restore(); + gon.relative_url_root = relativeUrlRoot; + }); + + it('initates the api call based on the passed path and commit hash', () => { + return services.getFiles(TEST_PROJECT_ID, TEST_COMMIT_SHA).then(({ data }) => { + expect(axios.get).toHaveBeenCalledWith( + `${gon.relative_url_root}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`, + expect.any(Object), + ); + expect(data).toEqual([TEST_FILE_PATH]); + }); + }); + }); }); diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/frontend/image_diff/helpers/badge_helper_spec.js index b3001d45e3c..c970ccc535d 100644 --- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js +++ b/spec/frontend/image_diff/helpers/badge_helper_spec.js @@ -66,7 +66,7 @@ describe('badge helper', () => { }); it('should set the badge text', () => { - expect(buttonEl.innerText).toEqual(badgeText); + expect(buttonEl.textContent).toEqual(badgeText); }); it('should set the button coordinates', () => { @@ -120,7 +120,7 @@ describe('badge helper', () => { }); it('should update badge number', () => { - expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString()); + expect(avatarBadgeEl.textContent).toEqual(badgeNumber.toString()); }); it('should remove hidden class', () => { diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js index 8e3e7f1222e..395bb7de362 100644 --- a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js +++ b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js @@ -128,8 +128,8 @@ describe('commentIndicatorHelper', () => { currentTarget: containerEl.querySelector('button'), }; - spyOn(event, 'stopPropagation'); - spyOn(textAreaEl, 'focus'); + jest.spyOn(event, 'stopPropagation').mockImplementation(() => {}); + jest.spyOn(textAreaEl, 'focus').mockImplementation(() => {}); commentIndicatorHelper.commentIndicatorOnClick(event); }); diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/frontend/image_diff/helpers/dom_helper_spec.js index ffe712af2dd..9357d626bbe 100644 --- a/spec/javascripts/image_diff/helpers/dom_helper_spec.js +++ b/spec/frontend/image_diff/helpers/dom_helper_spec.js @@ -44,7 +44,7 @@ describe('domHelper', () => { }); it('should update avatar badge number', () => { - expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString()); }); }); @@ -60,7 +60,7 @@ describe('domHelper', () => { }); it('should update discussion badge number', () => { - expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString()); }); }); diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/frontend/image_diff/helpers/utils_helper_spec.js index 3b6378be883..3b6378be883 100644 --- a/spec/javascripts/image_diff/helpers/utils_helper_spec.js +++ b/spec/frontend/image_diff/helpers/utils_helper_spec.js diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/frontend/image_diff/image_badge_spec.js index a1589d7b7a0..a11b50ead47 100644 --- a/spec/javascripts/image_diff/image_badge_spec.js +++ b/spec/frontend/image_diff/image_badge_spec.js @@ -71,7 +71,7 @@ describe('ImageBadge', () => { describe('imageEl property is provided and not browser property', () => { beforeEach(() => { - spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true); + jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(true); }); it('should generate browser property', () => { diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js index 21e7b8e2e9b..c15718b5106 100644 --- a/spec/javascripts/image_diff/image_diff_spec.js +++ b/spec/frontend/image_diff/image_diff_spec.js @@ -75,7 +75,7 @@ describe('ImageDiff', () => { describe('init', () => { beforeEach(() => { - spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {}); imageDiff = new ImageDiff(element); imageDiff.init(); }); @@ -97,19 +97,19 @@ describe('ImageDiff', () => { let imageEl; beforeEach(() => { - spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {}); - spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {}); - spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {}); - spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {}); - spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {}); - spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {}); - spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + jest.spyOn(imageDiffHelper, 'toggleCollapsed').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'commentIndicatorOnClick').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockImplementation(() => {}); + jest.spyOn(ImageDiff.prototype, 'imageClicked').mockImplementation(() => {}); + jest.spyOn(ImageDiff.prototype, 'addBadge').mockImplementation(() => {}); + jest.spyOn(ImageDiff.prototype, 'removeBadge').mockImplementation(() => {}); + jest.spyOn(ImageDiff.prototype, 'renderBadges').mockImplementation(() => {}); imageEl = element.querySelector('.diff-file .js-image-frame img'); }); describe('default', () => { beforeEach(() => { - spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false); imageDiff = new ImageDiff(element); imageDiff.imageEl = imageEl; imageDiff.bindEvents(); @@ -130,7 +130,7 @@ describe('ImageDiff', () => { describe('image not loaded', () => { beforeEach(() => { - spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false); imageDiff = new ImageDiff(element); imageDiff.imageEl = imageEl; imageDiff.bindEvents(); @@ -146,7 +146,7 @@ describe('ImageDiff', () => { describe('canCreateNote', () => { beforeEach(() => { - spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false); imageDiff = new ImageDiff(element, { canCreateNote: true, }); @@ -185,7 +185,7 @@ describe('ImageDiff', () => { describe('canCreateNote is false', () => { beforeEach(() => { - spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false); imageDiff = new ImageDiff(element); imageDiff.imageEl = imageEl; imageDiff.bindEvents(); @@ -202,12 +202,12 @@ describe('ImageDiff', () => { describe('imageClicked', () => { beforeEach(() => { - spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({ + jest.spyOn(imageDiffHelper, 'getTargetSelection').mockReturnValue({ actual: {}, browser: {}, }); - spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {}); - spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + jest.spyOn(imageDiffHelper, 'setPositionDataAttribute').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {}); imageDiff = new ImageDiff(element); imageDiff.imageClicked({ detail: { @@ -231,7 +231,7 @@ describe('ImageDiff', () => { describe('renderBadges', () => { beforeEach(() => { - spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {}); + jest.spyOn(ImageDiff.prototype, 'renderBadge').mockImplementation(() => {}); imageDiff = new ImageDiff(element); imageDiff.renderBadges(); }); @@ -239,7 +239,7 @@ describe('ImageDiff', () => { it('should call renderBadge for each discussionEl', () => { const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); - expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length); + expect(imageDiff.renderBadge.mock.calls.length).toEqual(discussionEls.length); }); }); @@ -247,9 +247,9 @@ describe('ImageDiff', () => { let discussionEls; beforeEach(() => { - spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); - spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {}); - spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({ + jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'addImageCommentBadge').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').mockReturnValue({ browser: {}, noteId: 'noteId', }); @@ -282,9 +282,9 @@ describe('ImageDiff', () => { describe('addBadge', () => { beforeEach(() => { - spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); - spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {}); - spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'addAvatarBadge').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {}); imageDiff = new ImageDiff(element); imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); imageDiff.addBadge({ @@ -320,8 +320,8 @@ describe('ImageDiff', () => { beforeEach(() => { const { imageMeta } = mockData; - spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); - spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {}); + jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {}); + jest.spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').mockImplementation(() => {}); imageDiff = new ImageDiff(element); imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta]; imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); @@ -336,8 +336,8 @@ describe('ImageDiff', () => { it('should update next imageBadgeEl value', () => { const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge'); - expect(imageBadgeEls[0].innerText).toEqual('1'); - expect(imageBadgeEls[1].innerText).toEqual('2'); + expect(imageBadgeEls[0].textContent).toEqual('1'); + expect(imageBadgeEls[1].textContent).toEqual('2'); expect(imageBadgeEls.length).toEqual(2); }); diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/frontend/image_diff/mock_data.js index a0d1732dd0a..a0d1732dd0a 100644 --- a/spec/javascripts/image_diff/mock_data.js +++ b/spec/frontend/image_diff/mock_data.js diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js index 62e7c8b6c6a..f2a7b7f8406 100644 --- a/spec/javascripts/image_diff/replaced_image_diff_spec.js +++ b/spec/frontend/image_diff/replaced_image_diff_spec.js @@ -76,8 +76,8 @@ describe('ReplacedImageDiff', () => { describe('init', () => { beforeEach(() => { - spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {}); + jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {}); + jest.spyOn(ReplacedImageDiff.prototype, 'generateImageEls').mockImplementation(() => {}); replacedImageDiff = new ReplacedImageDiff(element); replacedImageDiff.init(); @@ -140,7 +140,7 @@ describe('ReplacedImageDiff', () => { describe('generateImageEls', () => { beforeEach(() => { - spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {}); replacedImageDiff = new ReplacedImageDiff(element, { canCreateNote: false, @@ -163,7 +163,7 @@ describe('ReplacedImageDiff', () => { describe('bindEvents', () => { beforeEach(() => { - spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {}); replacedImageDiff = new ReplacedImageDiff(element); setupViewModesEls(); @@ -176,7 +176,7 @@ describe('ReplacedImageDiff', () => { }); it('should register click eventlistener to 2-up view mode', done => { - spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => { + jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => { expect(viewMode).toEqual(viewTypes.TWO_UP); done(); }); @@ -186,7 +186,7 @@ describe('ReplacedImageDiff', () => { }); it('should register click eventlistener to swipe view mode', done => { - spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => { + jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => { expect(viewMode).toEqual(viewTypes.SWIPE); done(); }); @@ -196,7 +196,7 @@ describe('ReplacedImageDiff', () => { }); it('should register click eventlistener to onion skin view mode', done => { - spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => { + jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => { expect(viewMode).toEqual(viewTypes.SWIPE); done(); }); @@ -247,7 +247,7 @@ describe('ReplacedImageDiff', () => { describe('changeView', () => { beforeEach(() => { replacedImageDiff = new ReplacedImageDiff(element); - spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({ + jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockReturnValue({ removed: false, }); setupImageFrameEls(); @@ -265,13 +265,12 @@ describe('ReplacedImageDiff', () => { describe('valid viewType', () => { beforeEach(() => { - jasmine.clock().install(); - spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {}); + jest.spyOn(ReplacedImageDiff.prototype, 'renderNewView').mockImplementation(() => {}); replacedImageDiff.changeView(viewTypes.ONION_SKIN); }); afterEach(() => { - jasmine.clock().uninstall(); + jest.clearAllTimers(); }); it('should call removeCommentIndicator', () => { @@ -287,7 +286,7 @@ describe('ReplacedImageDiff', () => { }); it('should call renderNewView', () => { - jasmine.clock().tick(251); + jest.advanceTimersByTime(251); expect(replacedImageDiff.renderNewView).toHaveBeenCalled(); }); @@ -300,7 +299,7 @@ describe('ReplacedImageDiff', () => { }); it('should call renderBadges', () => { - spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + jest.spyOn(ReplacedImageDiff.prototype, 'renderBadges').mockImplementation(() => {}); replacedImageDiff.renderNewView({ removed: false, @@ -326,14 +325,16 @@ describe('ReplacedImageDiff', () => { }); it('should pass showCommentIndicator normalized indicator values', done => { - spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); - spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => { - expect(meta.x).toEqual(indicator.x); - expect(meta.y).toEqual(indicator.y); - expect(meta.width).toEqual(indicator.image.width); - expect(meta.height).toEqual(indicator.image.height); - done(); - }); + jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {}); + jest + .spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement') + .mockImplementation((imageEl, meta) => { + expect(meta.x).toEqual(indicator.x); + expect(meta.y).toEqual(indicator.y); + expect(meta.width).toEqual(indicator.image.width); + expect(meta.height).toEqual(indicator.image.height); + done(); + }); replacedImageDiff.renderNewView(indicator); }); @@ -341,13 +342,13 @@ describe('ReplacedImageDiff', () => { const normalized = { normalized: true, }; - spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized); - spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake( - (imageFrameEl, normalizedIndicator) => { + jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(normalized); + jest + .spyOn(imageDiffHelper, 'showCommentIndicator') + .mockImplementation((imageFrameEl, normalizedIndicator) => { expect(normalizedIndicator).toEqual(normalized); done(); - }, - ); + }); replacedImageDiff.renderNewView(indicator); }); }); diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js index 367e07d3ad3..f656bb0b60d 100644 --- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js +++ b/spec/frontend/vue_shared/components/ci_badge_link_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 ciBadge from '~/vue_shared/components/ci_badge_link.vue'; describe('CI Badge Link Component', () => { diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js index 9486d7d4f23..63afe631063 100644 --- a/spec/javascripts/vue_shared/components/ci_icon_spec.js +++ b/spec/frontend/vue_shared/components/ci_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 ciIcon from '~/vue_shared/components/ci_icon.vue'; describe('CI Icon component', () => { diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js index fbe9337ecf4..b0563f2f6de 100644 --- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js +++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import waitForPromises from 'spec/helpers/wait_for_promises'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants'; import axios from '~/lib/utils/axios_utils'; import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; @@ -96,7 +96,7 @@ describe('ContentViewer', () => { it('markdown preview receives the file path as a parameter', done => { mock = new MockAdapter(axios); - spyOn(axios, 'post').and.callThrough(); + jest.spyOn(axios, 'post'); mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, { body: '<b>testing</b>', }); @@ -114,7 +114,7 @@ describe('ContentViewer', () => { expect(axios.post).toHaveBeenCalledWith( `${gon.relative_url_root}/testproject/preview_markdown`, { path: 'foo/test.md', text: '* Test' }, - jasmine.any(Object), + expect.any(Object), ); }) .then(done) diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js index a8acecdd3fc..636508be6b6 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; @@ -32,7 +32,7 @@ describe('DiffViewer', () => { createComponent({ ...requiredProps, projectPath: '' }); - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe( `//-/raw/DEF/${RED_BOX_IMAGE_URL}`, ); @@ -53,7 +53,7 @@ describe('DiffViewer', () => { oldPath: 'testold.abc', }); - setTimeout(() => { + setImmediate(() => { expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain( 'testold.abc', ); diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js index b00fa785a0e..892a96b76fd 100644 --- a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; +import { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue'; const defaultLabel = 'Select'; @@ -74,7 +74,7 @@ describe('DropdownButtonComponent', () => { }, ); - expect(vm.$el).not.toContainElement('.dropdown-toggle-text'); + expect(vm.$el.querySelector('.dropdown-toggle-text')).toBeNull(); expect(vm.$el).toHaveText('Lorem Ipsum Dolar'); }); }); diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js index 402de2a8788..30b8e869aab 100644 --- a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import { mockLabels } from './mock_data'; diff --git a/spec/javascripts/vue_shared/components/dropdown/mock_data.js b/spec/frontend/vue_shared/components/dropdown/mock_data.js index b09d42da401..b09d42da401 100644 --- a/spec/javascripts/vue_shared/components/dropdown/mock_data.js +++ b/spec/frontend/vue_shared/components/dropdown/mock_data.js diff --git a/spec/javascripts/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js index e18d0a46223..63f2614106d 100644 --- a/spec/javascripts/vue_shared/components/file_finder/item_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import { file } from 'spec/ide/helpers'; +import { file } from 'jest/ide/helpers'; import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; -import createComponent from '../../../helpers/vue_mount_component_helper'; +import createComponent from 'helpers/vue_mount_component_helper'; describe('File finder item spec', () => { const Component = Vue.extend(ItemComponent); @@ -75,7 +75,7 @@ describe('File finder item spec', () => { }); it('emits event when clicked', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.$el.click(); diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js index 0bb4a04557b..87cafa0bb8c 100644 --- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_dropdown_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 component from '~/vue_shared/components/filtered_search_dropdown.vue'; describe('Filtered search dropdown', () => { @@ -125,7 +125,7 @@ describe('Filtered search dropdown', () => { describe('on click create button', () => { it('emits createItem event with the filter', done => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.$nextTick(() => { vm.$el.querySelector('.js-dropdown-create-button').click(); diff --git a/spec/javascripts/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js index 929ffe219f4..365c9fad478 100644 --- a/spec/javascripts/vue_shared/components/gl_countdown_spec.js +++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js @@ -1,4 +1,4 @@ -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import Vue from 'vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; @@ -8,13 +8,12 @@ describe('GlCountdown', () => { let now = '2000-01-01T00:00:00Z'; beforeEach(() => { - spyOn(Date, 'now').and.callFake(() => new Date(now).getTime()); - jasmine.clock().install(); + jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime()); }); afterEach(() => { vm.$destroy(); - jasmine.clock().uninstall(); + jest.clearAllTimers(); }); describe('when there is time remaining', () => { @@ -29,16 +28,16 @@ describe('GlCountdown', () => { }); it('displays remaining time', () => { - expect(vm.$el).toContainText('01:02:03'); + expect(vm.$el.textContent).toContain('01:02:03'); }); it('updates remaining time', done => { now = '2000-01-01T00:00:01Z'; - jasmine.clock().tick(1000); + jest.advanceTimersByTime(1000); Vue.nextTick() .then(() => { - expect(vm.$el).toContainText('01:02:02'); + expect(vm.$el.textContent).toContain('01:02:02'); done(); }) .catch(done.fail); @@ -57,19 +56,26 @@ describe('GlCountdown', () => { }); it('displays 00:00:00', () => { - expect(vm.$el).toContainText('00:00:00'); + expect(vm.$el.textContent).toContain('00:00:00'); }); }); describe('when an invalid date is passed', () => { + beforeEach(() => { + Vue.config.warnHandler = jest.fn(); + }); + + afterEach(() => { + Vue.config.warnHandler = null; + }); + it('throws a validation error', () => { - spyOn(Vue.config, 'warnHandler').and.stub(); vm = mountComponent(Component, { endDateString: 'this is invalid', }); expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1); - const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0); + const [errorMessage] = Vue.config.warnHandler.mock.calls[0]; expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/); }); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js index b1abc972e1d..216563165d6 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; +import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; import headerCi from '~/vue_shared/components/header_ci_component.vue'; describe('Header CI Component', () => { diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js index a87998aa72f..e7c31014bfc 100644 --- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_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 toolbar from '~/vue_shared/components/markdown/toolbar.vue'; describe('toolbar', () => { diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js index beb980a6556..561456d614e 100644 --- a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js +++ b/spec/frontend/vue_shared/components/navigation_tabs_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 navigationTabs from '~/vue_shared/components/navigation_tabs.vue'; describe('navigation tabs component', () => { @@ -56,7 +56,7 @@ describe('navigation tabs component', () => { }); it('should trigger onTabClick', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.$el.querySelector('.js-pipelines-tab-pending').click(); expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending'); diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js index b787ba7596f..867bf88ff50 100644 --- a/spec/javascripts/vue_shared/components/pikaday_spec.js +++ b/spec/frontend/vue_shared/components/pikaday_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 datePicker from '~/vue_shared/components/pikaday.vue'; describe('datePicker', () => { @@ -20,7 +20,7 @@ describe('datePicker', () => { }); it('should toggle when dropdown is clicked', () => { - const hidePicker = jasmine.createSpy(); + const hidePicker = jest.fn(); vm.$on('hidePicker', hidePicker); vm.$el.querySelector('.dropdown-menu-toggle').click(); diff --git a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js index 2ec19ebf80e..090f8b69213 100644 --- a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js +++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { projectData } from 'spec/ide/mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { projectData } from 'jest/ide/mock_data'; import { TEST_HOST } from 'spec/test_constants'; import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility'; import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue'; @@ -48,8 +48,8 @@ describe('ProjectAvatarDefault component', () => { vm.$nextTick() .then(() => { - expect(vm.$el).toContainElement('.avatar'); - expect(vm.$el).not.toContainElement('.identicon'); + expect(vm.$el.querySelector('.avatar')).not.toBeNull(); + expect(vm.$el.querySelector('.identicon')).toBeNull(); expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl); }) .then(done) diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js index e73fb97b741..eb1d9e93634 100644 --- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -1,5 +1,5 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { trimText } from 'spec/helpers/text_helper'; +import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; const localVue = createLocalVue(); @@ -9,7 +9,7 @@ describe('ProjectListItem component', () => { let wrapper; let vm; let options; - loadJSONFixtures('static/projects.json'); + const project = getJSONFixture('static/projects.json')[0]; beforeEach(() => { @@ -44,7 +44,7 @@ describe('ProjectListItem component', () => { wrapper = shallowMount(Component, options); ({ vm } = wrapper); - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); wrapper.vm.onClick(); expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); @@ -95,7 +95,7 @@ describe('ProjectListItem component', () => { }); it('prevents search query and project name XSS', () => { - const alertSpy = spyOn(window, 'alert'); + const alertSpy = jest.spyOn(window, 'alert'); options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject"; options.propsData.matcher = "pro<script>alert('XSS');</script>"; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js index d90fafb6bf7..9db86fa775f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js @@ -4,10 +4,7 @@ import { shallowMount } from '@vue/test-utils'; import LabelsSelect from '~/labels_select'; import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; -import { - mockConfig, - mockLabels, -} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; +import { mockConfig, mockLabels } from './mock_data'; const createComponent = (config = mockConfig) => shallowMount(BaseComponent, { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js index a4121448492..d02d924bd2b 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -3,10 +3,7 @@ import Vue from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue'; -import { - mockConfig, - mockLabels, -} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; +import { mockConfig, mockLabels } from './mock_data'; const componentConfig = { ...mockConfig, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js index d0299523137..edec3b138b3 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue'; -import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; +import { mockSuggestedColors } from './mock_data'; const createComponent = headerTitle => { const Component = Vue.extend(dropdownCreateLabelComponent); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js index 784bbaf8e6a..7e9e242a4f5 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue'; -import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; +import { mockConfig } from './mock_data'; const createComponent = ( labelsWebUrl = mockConfig.labelsWebUrl, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 887c04268d1..e09f0006359 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; -import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; +import { mockLabels } from './mock_data'; const createComponent = (labels = mockLabels) => { const Component = Vue.extend(dropdownValueCollapsedComponent); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 06355c0dd65..c33cffb421d 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -2,10 +2,7 @@ import { mount } from '@vue/test-utils'; import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; import { GlLabel } from '@gitlab/ui'; -import { - mockConfig, - mockLabels, -} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; +import { mockConfig, mockLabels } from './mock_data'; const createComponent = ( labels = mockLabels, diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js index 6564c012e67..6564c012e67 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js index 3e044f47a3f..bc86ee5a0c6 100644 --- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js +++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; const createComponent = config => { diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/frontend/vue_shared/components/tabs/tab_spec.js index 8437fe37738..8cf07a9177c 100644 --- a/spec/javascripts/vue_shared/components/tabs/tab_spec.js +++ b/spec/frontend/vue_shared/components/tabs/tab_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 Tab from '~/vue_shared/components/tabs/tab.vue'; describe('Tab component', () => { diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/frontend/vue_shared/components/tabs/tabs_spec.js index 50ba18cd338..49d92094b34 100644 --- a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js +++ b/spec/frontend/vue_shared/components/tabs/tabs_spec.js @@ -5,28 +5,23 @@ import Tab from '~/vue_shared/components/tabs/tab.vue'; describe('Tabs component', () => { let vm; - beforeEach(done => { + beforeEach(() => { vm = new Vue({ components: { Tabs, Tab, }, - template: ` - <div> - <tabs> - <tab title="Testing" active> - First tab - </tab> - <tab> - <template slot="title">Test slot</template> - Second tab - </tab> - </tabs> - </div> - `, + render(h) { + return h('div', [ + h('tabs', [ + h('tab', { attrs: { title: 'Testing', active: true } }, 'First tab'), + h('tab', [h('template', { slot: 'title' }, 'Test slot'), 'Second tab']), + ]), + ]); + }, }).$mount(); - setTimeout(done); + return vm.$nextTick(); }); describe('tab links', () => { @@ -46,14 +41,12 @@ describe('Tabs component', () => { expect(vm.$el.querySelector('a').classList).toContain('active'); }); - it('updates active class on click', done => { + it('updates active class on click', () => { vm.$el.querySelectorAll('a')[1].click(); - setTimeout(() => { + return vm.$nextTick(() => { expect(vm.$el.querySelector('a').classList).not.toContain('active'); expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active'); - - done(); }); }); }); diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js index ea0a89a3ab5..83bbb37a89a 100644 --- a/spec/javascripts/vue_shared/components/toggle_button_spec.js +++ b/spec/frontend/vue_shared/components/toggle_button_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 toggleButton from '~/vue_shared/components/toggle_button.vue'; describe('Toggle Button', () => { @@ -42,7 +42,7 @@ describe('Toggle Button', () => { value: true, }); - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); }); it('renders is checked class', () => { @@ -72,7 +72,7 @@ describe('Toggle Button', () => { value: true, disabledInput: true, }); - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); }); it('renders disabled button', () => { diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js new file mode 100644 index 00000000000..ee6c2e2cc46 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js @@ -0,0 +1,27 @@ +import { shallowMount } from '@vue/test-utils'; +import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; + +describe('User Avatar Svg Component', () => { + describe('Initialization', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(UserAvatarSvg, { + propsData: { + size: 99, + svg: + '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.707 15.707C1.077z"/></svg>', + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should have <svg> as a child element', () => { + expect(wrapper.element.tagName).toEqual('svg'); + expect(wrapper.html()).toContain('<path'); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index fabe44ce333..2201a3b4b57 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -30,6 +30,7 @@ describe('Multi-file store tree actions', () => { store.state.currentBranchId = 'master'; store.state.projects.abcproject = { web_url: '', + path_with_namespace: 'foo/abcproject', }; }); @@ -57,7 +58,7 @@ describe('Multi-file store tree actions', () => { store .dispatch('getFiles', basicCallParameters) .then(() => { - expect(service.getFiles).toHaveBeenCalledWith('', '12345678'); + expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678'); done(); }) diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js deleted file mode 100644 index 31644416439..00000000000 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import Vue from 'vue'; -import avatarSvg from 'icons/_icon_random.svg'; -import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; - -const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg); - -describe('User Avatar Svg Component', function() { - describe('Initialization', function() { - beforeEach(function() { - this.propsData = { - size: 99, - svg: avatarSvg, - }; - - this.userAvatarSvg = new UserAvatarSvgComponent({ - propsData: this.propsData, - }).$mount(); - }); - - it('should return a defined Vue component', function() { - expect(this.userAvatarSvg).toBeDefined(); - }); - - it('should have <svg> as a child element', function() { - expect(this.userAvatarSvg.$el.tagName).toEqual('svg'); - expect(this.userAvatarSvg.$el.innerHTML).toContain('<path'); - }); - }); -}); diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index 94b7a086e59..45db4acd3ac 100644 --- a/spec/lib/gitlab/git/attributes_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -75,6 +75,14 @@ describe Gitlab::Git::AttributesParser, :seed_helper do expect(subject.attributes('test.foo')).to eq({}) end end + + context 'when attributes data has binary data' do + let(:data) { "\xFF\xFE*\u0000.\u0000c\u0000s".b } + + it 'returns an empty Hash' do + expect(subject.attributes('test.foo')).to eq({}) + end + end end describe '#patterns' do diff --git a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb index 85a5b1dacc7..11cf14523c2 100644 --- a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb @@ -19,6 +19,20 @@ describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection do it_behaves_like 'connection with paged nodes' do let(:paged_nodes_size) { values.size } end + + context 'when after or before is specified, they are ignored' do + # after and before are not used to filter the array, as they + # were already used to directly fetch the external array + it_behaves_like 'connection with paged nodes' do + let(:arguments) { { after: next_cursor } } + let(:paged_nodes_size) { values.size } + end + + it_behaves_like 'connection with paged nodes' do + let(:arguments) { { before: prev_cursor } } + let(:paged_nodes_size) { values.size } + end + end end describe '#start_cursor' do diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb index 555b7f04f86..95782c1f674 100644 --- a/spec/models/design_management/design_spec.rb +++ b/spec/models/design_management/design_spec.rb @@ -380,15 +380,8 @@ describe DesignManagement::Design do end end - # TODO these tests are being temporarily skipped unless run in EE, - # as we are in the process of moving Design Management to FOSS in 13.0 - # in steps. In the current step the routes have not yet been moved. - # - # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. describe '#note_etag_key' do it 'returns a correct etag key' do - skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? - design = create(:design) expect(design.note_etag_key).to eq( diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index cbbd0dae2eb..4fe612c1d0e 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -852,4 +852,20 @@ describe 'project routing' do it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/serverless", "/gitlab/gitlabhq/-/serverless" end end + + describe Projects::DesignManagement::Designs::RawImagesController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/-/design_management/designs/1/raw_image')).to route_to('projects/design_management/designs/raw_images#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1') + expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/raw_image')).to route_to('projects/design_management/designs/raw_images#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', sha: 'c6f00aa50b80887ada30a6fe517670be9f8f9ece') + end + end + + describe Projects::DesignManagement::Designs::ResizedImageController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/-/design_management/designs/1/resized_image/v432x230')).to route_to('projects/design_management/designs/resized_image#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', id: 'v432x230') + expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/v432x230')).to route_to('projects/design_management/designs/resized_image#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', sha: 'c6f00aa50b80887ada30a6fe517670be9f8f9ece', id: 'v432x230') + expect(get('/gitlab/gitlabhq/-/design_management/designs/1/invalid/resized_image/v432x230')).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/-/design_management/designs/1/invalid/resized_image/v432x230') + expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/small')).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/small') + end + end end |