diff options
22 files changed, 350 insertions, 144 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 41b15e2edd3..62907acfd57 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -227,6 +227,8 @@ _ee:super-sidebar-nav: QA_SUPER_SIDEBAR_ENABLED: "true" QA_ALLURE_RESULTS_DIRECTORY: tmp/allure-results-super-sidebar GITLAB_QA_OPTS: --set-feature-flags super_sidebar_nav=enabled + RSPEC_REPORT_OPTS: "--format documentation" + SKIP_REPORT_IN_ISSUES: "true" allow_failure: true rules: - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH diff --git a/.rubocop.yml b/.rubocop.yml index 3489c2e2534..8f9a02bbadd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -30,6 +30,7 @@ inherit_mode: merge: - Include - Exclude + - AllowedPatterns AllCops: # Target the current Ruby version. For example, "2.7" or "3.0". @@ -97,6 +98,9 @@ InternalAffairs/DeprecateCopHelper: Include: - spec/rubocop/**/*.rb +Layout/LineLength: + AllowedPatterns: ['^RSpec\.describe\s.*\sdo'] + Lint/LastKeywordArgument: Safe: false @@ -514,7 +514,7 @@ gem 'kas-grpc', '~> 0.0.2' gem 'grpc', '~> 1.42.0' -gem 'google-protobuf', '~> 3.22' +gem 'google-protobuf', '~> 3.22', '>= 3.22.1' gem 'toml-rb', '~> 2.2.0' diff --git a/Gemfile.checksum b/Gemfile.checksum index 9bb151adb66..579849182bc 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -235,15 +235,15 @@ {"name":"google-cloud-env","version":"1.6.0","platform":"ruby","checksum":"6179acb946975892c7908748df5722a4ebadfc8cf5bb7b0d8d933ca67183fa15"}, {"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"}, {"name":"google-cloud-storage","version":"1.44.0","platform":"ruby","checksum":"299a1e055c9277c8120f7c10d21d37e4d8c17c7b963350c0e0bff7e9d9a570ea"}, -{"name":"google-protobuf","version":"3.22.0","platform":"arm64-darwin","checksum":"e47680f8cf46d5e0bf573052047276260d785caf7b586719af407767e96e535c"}, -{"name":"google-protobuf","version":"3.22.0","platform":"java","checksum":"24e55a0665113f60af35e27dd4c1fcc9b08d16f9da7605456634e505c95890b3"}, -{"name":"google-protobuf","version":"3.22.0","platform":"ruby","checksum":"58db86f65c686ef4b9389569faa176bada384850751675f0c3736ef76cdcae90"}, -{"name":"google-protobuf","version":"3.22.0","platform":"x64-mingw-ucrt","checksum":"43ce8f98fdfa06a81397577bb502230d545178afd592ef5e7d2daddacdda8eb6"}, -{"name":"google-protobuf","version":"3.22.0","platform":"x64-mingw32","checksum":"8255654f5a4a7fff0d430357fa9fe2f23db16eb17ed1067b46a1aab7fd9fcbab"}, -{"name":"google-protobuf","version":"3.22.0","platform":"x86-linux","checksum":"2794f32ecfbeec0fd7baa4a61292fa220f5aad07b656ba656ecf134b1f7e8425"}, -{"name":"google-protobuf","version":"3.22.0","platform":"x86-mingw32","checksum":"a037a5ed2d0a1faa556466c34f7177fee1f3aee8282c2195aed804fead65c65d"}, -{"name":"google-protobuf","version":"3.22.0","platform":"x86_64-darwin","checksum":"bff2987e4bf1a934a4555aea2020df18b557393b188c849fb9deacec9ed0f3d9"}, -{"name":"google-protobuf","version":"3.22.0","platform":"x86_64-linux","checksum":"a0ea6aa03602e9e4f11c1506a114777204aade2e04adc2051945536c35d88bfe"}, +{"name":"google-protobuf","version":"3.22.1","platform":"arm64-darwin","checksum":"bd904af849bd1f143c4d9eb74e0622f9efca7ed20719b00baa77fcb1a5fc2aa7"}, +{"name":"google-protobuf","version":"3.22.1","platform":"java","checksum":"268359af055709c5796b1c726f839ef0fe356702c7abdb136367eb37cf9662e3"}, +{"name":"google-protobuf","version":"3.22.1","platform":"ruby","checksum":"2fc99aa4f0c2fdd33baf7c7fafb045118d1775893ca8823254658429a23ba0f2"}, +{"name":"google-protobuf","version":"3.22.1","platform":"x64-mingw-ucrt","checksum":"351cb7743ea748156fea56ace3f5613ce5a5f1c4c59b18ff28930ef7c53de5f6"}, +{"name":"google-protobuf","version":"3.22.1","platform":"x64-mingw32","checksum":"3808b954ee3240ef97d019037149dde2afa877585420b30f7ac7be37cee7bb4e"}, +{"name":"google-protobuf","version":"3.22.1","platform":"x86-linux","checksum":"6d8ee9928dbe564916b37ede264b1b3492915b818a0daf9c8b4b572eda7baa30"}, +{"name":"google-protobuf","version":"3.22.1","platform":"x86-mingw32","checksum":"a8ab1fda2a208dc238f16b2796d5a3383e6a273f8ef88a23ca90e0c2846594ee"}, +{"name":"google-protobuf","version":"3.22.1","platform":"x86_64-darwin","checksum":"a425ce701b8c4f9aeb7cac0e7f1b36496ff8f6bf86dc7fe9d06af534d25e45d8"}, +{"name":"google-protobuf","version":"3.22.1","platform":"x86_64-linux","checksum":"a62f7d472c346cea6c6141c70b1caba5b7e75319c56cf44a24b62bad6c1adf10"}, {"name":"googleapis-common-protos-types","version":"1.3.0","platform":"ruby","checksum":"c5411f3197cc3e02547ded1858303b1f830b4dc89c588c142ad6c8a231050671"}, {"name":"googleauth","version":"1.3.0","platform":"ruby","checksum":"51dd7362353cf1e90a2d01e1fb94321ae3926c776d4dc4a79db65230217ffcc2"}, {"name":"gpgme","version":"2.0.22","platform":"ruby","checksum":"7c6904952afdd0bf2c7c3ed6de98a5143f86c6b7390dbcd9d7012bddfa3ec862"}, diff --git a/Gemfile.lock b/Gemfile.lock index dd8b77c40cc..89c13820b12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -664,7 +664,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - google-protobuf (3.22.0) + google-protobuf (3.22.1) googleapis-common-protos-types (1.3.0) google-protobuf (~> 3.14) googleauth (1.3.0) @@ -1704,7 +1704,7 @@ DEPENDENCIES google-apis-serviceusage_v1 (~> 0.28.0) google-apis-sqladmin_v1beta4 (~> 0.41.0) google-cloud-storage (~> 1.44.0) - google-protobuf (~> 3.22) + google-protobuf (~> 3.22, >= 3.22.1) gpgme (~> 2.0.22) grape (~> 1.5.2) grape-entity (~> 0.10.0) diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index 3af2b7892be..cd89234a231 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -13,6 +13,7 @@ import { mergeById, isSidebarDirty, getAggregationsUrl, + prepareSearchAggregations, } from './utils'; export const fetchGroups = ({ commit }, search) => { @@ -135,12 +136,12 @@ export const fetchSidebarCount = ({ commit, state }) => { return Promise.all(promises); }; -export const fetchLanguageAggregation = ({ commit }) => { +export const fetchLanguageAggregation = ({ commit, state }) => { commit(types.REQUEST_AGGREGATIONS); return axios .get(getAggregationsUrl()) .then(({ data }) => { - commit(types.RECEIVE_AGGREGATIONS_SUCCESS, data); + commit(types.RECEIVE_AGGREGATIONS_SUCCESS, prepareSearchAggregations(state, data)); }) .catch((e) => { logError(e); diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js index 1e6619ca6d5..8e484e69646 100644 --- a/app/assets/javascripts/search/store/utils.js +++ b/app/assets/javascripts/search/store/utils.js @@ -1,7 +1,8 @@ -import { isEqual } from 'lodash'; +import { isEqual, orderBy } from 'lodash'; import AccessorUtilities from '~/lib/utils/accessor'; import { formatNumber } from '~/locale'; import { joinPaths } from '~/lib/utils/url_utility'; +import { languageFilterData } from '~/search/sidebar/constants/language_filter_data'; import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY, @@ -9,6 +10,8 @@ import { NUMBER_FORMATING_OPTIONS, } from './constants'; +const LANGUAGE_AGGREGATION_NAME = languageFilterData.filterParam; + function extractKeys(object, keyList) { return Object.fromEntries(keyList.map((key) => [key, object[key]])); } @@ -117,3 +120,27 @@ export const getAggregationsUrl = () => { currentUrl.pathname = joinPaths('/search', 'aggregations'); return currentUrl.toString(); }; + +const sortLanguages = (state, entries) => { + const queriedLanguages = state.query?.[LANGUAGE_AGGREGATION_NAME] || []; + + if (!Array.isArray(queriedLanguages) || !queriedLanguages.length) { + return entries; + } + + const queriedLanguagesSet = new Set(queriedLanguages); + + return orderBy(entries, [({ key }) => queriedLanguagesSet.has(key), 'count'], ['desc', 'desc']); +}; + +export const prepareSearchAggregations = (state, aggregationData) => + aggregationData.map((item) => { + if (item?.name === LANGUAGE_AGGREGATION_NAME) { + return { + ...item, + buckets: sortLanguages(state, item.buckets), + }; + } + + return item; + }); diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index 3225d3f6e49..1d3319dbc90 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -87,6 +87,9 @@ export default { hasAdminPermission() { return this.note.userPermissions.adminNote; }, + hasAwardEmojiPermission() { + return this.note.userPermissions.awardEmoji; + }, }, methods: { showReplyForm() { @@ -159,10 +162,13 @@ export default { </note-header> <div class="gl-display-inline-flex"> <note-actions + :show-award-emoji="hasAwardEmojiPermission" :show-reply="showReply" :show-edit="hasAdminPermission" + :note-id="note.id" @startReplying="showReplyForm" @startEditing="startEditing" + @error="($event) => $emit('error', $event)" /> <!-- v-if condition should be moved to "delete" dropdown item as soon as we implement copying the link --> <gl-dropdown diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue index c17e855e527..6bea7953698 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue @@ -1,7 +1,10 @@ <script> -import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { __, s__ } from '~/locale'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import addAwardEmojiMutation from '../../graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; export default { name: 'WorkItemNoteActions', @@ -10,11 +13,14 @@ export default { }, components: { GlButton, + GlIcon, ReplyButton, + EmojiPicker: () => import('~/emoji/components/picker.vue'), }, directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagsMixin()], props: { showReply: { type: Boolean, @@ -24,12 +30,63 @@ export default { type: Boolean, required: true, }, + noteId: { + type: String, + required: true, + }, + showAwardEmoji: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + async setAwardEmoji(name) { + try { + const { + data: { + awardEmojiAdd: { errors = [] }, + }, + } = await this.$apollo.mutate({ + mutation: addAwardEmojiMutation, + variables: { + awardableId: this.noteId, + name, + }, + }); + + if (errors.length > 0) { + throw new Error(errors[0].message); + } + } catch (error) { + this.$emit('error', s__('WorkItem|Failed to award emoji')); + Sentry.captureException(error); + } + }, }, }; </script> <template> <div class="note-actions"> + <emoji-picker + v-if="showAwardEmoji && glFeatures.workItemsMvc2" + toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary" + data-testid="note-emoji-button" + @click="setAwardEmoji" + > + <template #button-content> + <gl-icon class="award-control-icon-neutral gl-button-icon gl-icon" name="slight-smile" /> + <gl-icon + class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!" + name="smiley" + /> + <gl-icon + class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!" + name="smile" + /> + </template> + </emoji-picker> <reply-button v-if="showReply" ref="replyButton" @startReplying="$emit('startReplying')" /> <gl-button v-if="showEdit" diff --git a/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql new file mode 100644 index 00000000000..dc51c53428b --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql @@ -0,0 +1,17 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" + +mutation workItemNoteAddAwardEmoji($awardableId: AwardableID!, $name: String!) { + awardEmojiAdd(input: { awardableId: $awardableId, name: $name }) { + awardEmoji { + name + description + unicode + emoji + unicodeVersion + user { + ...User + } + } + errors + } +} diff --git a/app/models/note.rb b/app/models/note.rb index 54e7389136f..b9b884b88c5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -172,7 +172,6 @@ class Note < ApplicationRecord project: [:project_members, :namespace, { group: [:group_members] }]) end scope :with_metadata, -> { includes(:system_note_metadata) } - scope :with_web_entity_associations, -> { preload(:project, :author, :noteable) } scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) } scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) } @@ -291,6 +290,10 @@ class Note < ApplicationRecord def cherry_picked_merge_requests(shas) where(noteable_type: 'MergeRequest', commit_id: shas).select(:noteable_id) end + + def with_web_entity_associations + preload(:project, :author, :noteable) + end end # rubocop: disable CodeReuse/ServiceClass diff --git a/config/feature_flags/development/user_search_simple_query_string.yml b/config/feature_flags/development/user_search_simple_query_string.yml deleted file mode 100644 index 78230c0fef9..00000000000 --- a/config/feature_flags/development/user_search_simple_query_string.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: user_search_simple_query_string -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110623 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391955 -milestone: '15.10' -type: development -group: group::global search -default_enabled: false diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md index ac4c3c62746..5273ea47f58 100644 --- a/doc/user/search/advanced_search.md +++ b/doc/user/search/advanced_search.md @@ -55,12 +55,6 @@ Advanced search uses [Elasticsearch syntax](https://www.elastic.co/guide/en/elas ### Refining user search -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/388409) in GitLab 15.10 [with a flag](../../administration/feature_flags.md) named `user_search_simple_query_string`. Disabled by default. - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `user_search_simple_query_string`. -On GitLab.com, this feature is available. - In user search, a [fuzzy query](https://www.elastic.co/guide/en/elasticsearch/reference/7.2/query-dsl-fuzzy-query.html) is used by default. You can refine your search with [Elasticsearch syntax](#syntax). ### Code search diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0d921596def..48797d7a384 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -29713,6 +29713,9 @@ msgstr "" msgid "OnDemandScans|Only project owners and maintainers can select runner tags." msgstr "" +msgid "OnDemandScans|Pre-scan verification for %{profile_name} completed with" +msgstr "" + msgid "OnDemandScans|Repeats" msgstr "" @@ -49001,6 +49004,9 @@ msgstr "" msgid "WorkItem|Existing task" msgstr "" +msgid "WorkItem|Failed to award emoji" +msgstr "" + msgid "WorkItem|Health status" msgstr "" diff --git a/package.json b/package.json index a993fcafa15..c4d738429d3 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", "@gitlab/svgs": "3.24.0", - "@gitlab/ui": "56.2.0", + "@gitlab/ui": "56.2.1", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230223005157", "@mattiasbuelens/web-streams-adapter": "^0.1.0", diff --git a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb index a10e95a860c..ecf8717be87 100644 --- a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true module QA - # https://github.com/gitlab-qa-github/import-test <- project under test - # - RSpec.describe 'Manage', product_group: :import do + RSpec.describe 'Manage', product_group: :import, quarantine: { + type: :stale, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/394994' + } do describe 'GitHub import' do include_context 'with github import' diff --git a/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb index b5a8df15ddc..6cb777ba7f4 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', product_group: :import do + RSpec.describe 'Manage', product_group: :import, quarantine: { + type: :stale, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/394994' + } do describe 'GitHub import' do include_context 'with github import' diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js index 46740958090..47e09bb38bc 100644 --- a/spec/frontend/blob/components/blob_header_spec.js +++ b/spec/frontend/blob/components/blob_header_spec.js @@ -1,9 +1,14 @@ import { shallowMount, mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import BlobHeader from '~/blob/components/blob_header.vue'; import DefaultActions from '~/blob/components/blob_header_default_actions.vue'; import BlobFilepath from '~/blob/components/blob_header_filepath.vue'; import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue'; +import { + RICH_BLOB_VIEWER_TITLE, + SIMPLE_BLOB_VIEWER, + SIMPLE_BLOB_VIEWER_TITLE, +} from '~/blob/components/constants'; import TableContents from '~/blob/components/table_contents.vue'; import { Blob } from './mock_data'; @@ -11,12 +16,26 @@ import { Blob } from './mock_data'; describe('Blob Header Default Actions', () => { let wrapper; - function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) { - const method = shouldMount ? mount : shallowMount; - const blobHash = 'foo-bar'; - wrapper = method.call(this, BlobHeader, { + const defaultProvide = { + blobHash: 'foo-bar', + }; + + const findDefaultActions = () => wrapper.findComponent(DefaultActions); + const findTableContents = () => wrapper.findComponent(TableContents); + const findViewSwitcher = () => wrapper.findComponent(ViewerSwitcher); + const findBlobFilePath = () => wrapper.findComponent(BlobFilepath); + const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE); + const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE); + + function createComponent({ + blobProps = {}, + options = {}, + propsData = {}, + mountFn = shallowMount, + } = {}) { + wrapper = mountFn(BlobHeader, { provide: { - blobHash, + ...defaultProvide, }, propsData: { blob: { ...Blob, ...blobProps }, @@ -26,143 +45,123 @@ describe('Blob Header Default Actions', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { - const findDefaultActions = () => wrapper.findComponent(DefaultActions); - - const slots = { - prepend: 'Foo Prepend', - actions: 'Actions Bar', - }; - it('matches the snapshot', () => { createComponent(); expect(wrapper.element).toMatchSnapshot(); }); - it('renders all components', () => { - createComponent(); - expect(wrapper.findComponent(TableContents).exists()).toBe(true); - expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true); - expect(findDefaultActions().exists()).toBe(true); - expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true); + describe('default render', () => { + it.each` + findComponent | componentName + ${findTableContents} | ${'TableContents'} + ${findViewSwitcher} | ${'ViewSwitcher'} + ${findDefaultActions} | ${'DefaultActions'} + ${findBlobFilePath} | ${'BlobFilePath'} + `('renders $componentName component by default', ({ findComponent }) => { + createComponent(); + + expect(findComponent().exists()).toBe(true); + }); }); it('does not render viewer switcher if the blob has only the simple viewer', () => { createComponent({ - richViewer: null, + blobProps: { + richViewer: null, + }, }); - expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false); + expect(findViewSwitcher().exists()).toBe(false); }); it('does not render viewer switcher if a corresponding prop is passed', () => { - createComponent( - {}, - {}, - { + createComponent({ + propsData: { hideViewerSwitcher: true, }, - ); - expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false); + }); + expect(findViewSwitcher().exists()).toBe(false); }); it('does not render default actions is corresponding prop is passed', () => { - createComponent( - {}, - {}, - { + createComponent({ + propsData: { hideDefaultActions: true, }, - ); - expect(wrapper.findComponent(DefaultActions).exists()).toBe(false); + }); + expect(findDefaultActions().exists()).toBe(false); }); - Object.keys(slots).forEach((slot) => { - it('renders the slots', () => { - const slotContent = slots[slot]; - createComponent( - {}, - { - scopedSlots: { - [slot]: `<span>${slotContent}</span>`, - }, + it.each` + slotContent | key + ${'Foo Prepend'} | ${'prepend'} + ${'Actions Bar'} | ${'actions'} + `('renders the slot $key', ({ key, slotContent }) => { + createComponent({ + options: { + scopedSlots: { + [key]: `<span>${slotContent}</span>`, }, - {}, - true, - ); - expect(wrapper.text()).toContain(slotContent); + }, + mountFn: mount, }); + expect(wrapper.text()).toContain(slotContent); }); it('passes information about render error down to default actions', () => { - createComponent( - {}, - {}, - { + createComponent({ + propsData: { hasRenderError: true, }, - ); + }); expect(findDefaultActions().props('hasRenderError')).toBe(true); }); it('passes the correct isBinary value to default actions when viewing a binary file', () => { - createComponent({}, {}, { isBinary: true }); + createComponent({ propsData: { isBinary: true } }); expect(findDefaultActions().props('isBinary')).toBe(true); }); }); describe('functionality', () => { - const newViewer = 'Foo Bar'; - const activeViewerType = 'Alpha Beta'; - const factory = (hideViewerSwitcher = false) => { - createComponent( - {}, - {}, - { - activeViewerType, + createComponent({ + propsData: { + activeViewerType: SIMPLE_BLOB_VIEWER, hideViewerSwitcher, }, - ); + mountFn: mountExtended, + }); }; - it('by default sets viewer data based on activeViewerType', () => { + it('shows the correctly selected view by default', () => { factory(); - expect(wrapper.vm.viewer).toBe(activeViewerType); + + expect(findViewSwitcher().exists()).toBe(true); + expect(findRichTextEditorBtn().props().selected).toBe(false); + expect(findSimpleTextEditorBtn().props().selected).toBe(true); }); - it('sets viewer to null if the viewer switcher should be hidden', () => { + it('Does not show the viewer switcher should be hidden', () => { factory(true); - expect(wrapper.vm.viewer).toBe(null); + + expect(findViewSwitcher().exists()).toBe(false); }); it('watches the changes in viewer data and emits event when the change is registered', async () => { factory(); - jest.spyOn(wrapper.vm, '$emit'); - wrapper.vm.viewer = newViewer; - await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer); - }); - - it('does not emit event if the switcher is not rendered', async () => { - factory(true); - - expect(wrapper.vm.showViewerSwitcher).toBe(false); - jest.spyOn(wrapper.vm, '$emit'); - wrapper.vm.viewer = newViewer; + await findRichTextEditorBtn().trigger('click'); - await nextTick(); - expect(wrapper.vm.$emit).not.toHaveBeenCalled(); + expect(wrapper.emitted('viewer-changed')).toBeDefined(); }); it('sets different icons depending on the blob file type', async () => { factory(); - expect(wrapper.vm.blobSwitcherDocIcon).toBe('document'); + + expect(findViewSwitcher().props('docIcon')).toBe('document'); + await wrapper.setProps({ blob: { ...Blob, @@ -172,7 +171,8 @@ describe('Blob Header Default Actions', () => { }, }, }); - expect(wrapper.vm.blobSwitcherDocIcon).toBe('table'); + + expect(findViewSwitcher().props('docIcon')).toBe('table'); }); }); }); diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js index fb9c0a93907..0aa4f0e1c84 100644 --- a/spec/frontend/search/mock_data.js +++ b/spec/frontend/search/mock_data.js @@ -653,3 +653,10 @@ export const TEST_FILTER_DATA = { JSON: { label: 'JSON', value: 'JSON', count: 15 }, }, }; + +export const SMALL_MOCK_AGGREGATIONS = [ + { + name: 'language', + buckets: TEST_RAW_BUCKETS, + }, +]; diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js index 8c4a17f0a5d..dfe4e801f11 100644 --- a/spec/frontend/search/store/utils_spec.js +++ b/spec/frontend/search/store/utils_spec.js @@ -7,6 +7,7 @@ import { isSidebarDirty, formatSearchResultCount, getAggregationsUrl, + prepareSearchAggregations, } from '~/search/store/utils'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { @@ -15,6 +16,9 @@ import { MOCK_INFLATED_DATA, FRESH_STORED_DATA, STALE_STORED_DATA, + MOCK_AGGREGATIONS, + SMALL_MOCK_AGGREGATIONS, + TEST_RAW_BUCKETS, } from '../mock_data'; const PREV_TIME = new Date().getTime() - 1; @@ -266,4 +270,22 @@ describe('Global Search Store Utils', () => { expect(getAggregationsUrl()).toStrictEqual(`${testURL}search/aggregations`); }); }); + + const TEST_LANGUAGE_QUERY = ['Markdown', 'JSON']; + const TEST_EXPECTED_ORDERED_BUCKETS = [ + TEST_RAW_BUCKETS.find((x) => x.key === 'Markdown'), + TEST_RAW_BUCKETS.find((x) => x.key === 'JSON'), + ...TEST_RAW_BUCKETS.filter((x) => !TEST_LANGUAGE_QUERY.includes(x.key)), + ]; + + describe('prepareSearchAggregations', () => { + it.each` + description | query | data | result + ${'has no query'} | ${undefined} | ${MOCK_AGGREGATIONS} | ${MOCK_AGGREGATIONS} + ${'has query'} | ${{ language: TEST_LANGUAGE_QUERY }} | ${SMALL_MOCK_AGGREGATIONS} | ${[{ ...SMALL_MOCK_AGGREGATIONS[0], buckets: TEST_EXPECTED_ORDERED_BUCKETS }]} + ${'has bad query'} | ${{ language: ['sdf', 'wrt'] }} | ${SMALL_MOCK_AGGREGATIONS} | ${SMALL_MOCK_AGGREGATIONS} + `('$description', ({ query, data, result }) => { + expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result); + }); + }); }); diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js index d85cd46c1c3..b293127b6af 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js @@ -1,52 +1,116 @@ import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import EmojiPicker from '~/emoji/components/picker.vue'; +import waitForPromises from 'helpers/wait_for_promises'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue'; +import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; + +Vue.use(VueApollo); describe('Work Item Note Actions', () => { let wrapper; + const noteId = '1'; const findReplyButton = () => wrapper.findComponent(ReplyButton); const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]'); + const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]'); + + const addEmojiMutationResolver = jest.fn().mockResolvedValue({ + data: { + errors: [], + }, + }); + + const EmojiPickerStub = { + props: EmojiPicker.props, + template: '<div></div>', + }; - const createComponent = ({ showReply = true, showEdit = true } = {}) => { + const createComponent = ({ showReply = true, showEdit = true, showAwardEmoji = true } = {}) => { wrapper = shallowMount(WorkItemNoteActions, { propsData: { showReply, showEdit, + noteId, + showAwardEmoji, + }, + provide: { + glFeatures: { + workItemsMvc2: true, + }, }, + stubs: { + EmojiPicker: EmojiPickerStub, + }, + apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]), }); }; - describe('Default', () => { - it('Should show the reply button by default', () => { + describe('reply button', () => { + it('is visible by default', () => { createComponent(); + expect(findReplyButton().exists()).toBe(true); }); - }); - describe('When the reply button needs to be hidden', () => { - it('Should show the reply button by default', () => { + it('is hidden when showReply false', () => { createComponent({ showReply: false }); + expect(findReplyButton().exists()).toBe(false); }); }); - it('shows edit button when `showEdit` prop is true', () => { - createComponent(); + describe('edit button', () => { + it('is visible when `showEdit` prop is true', () => { + createComponent(); - expect(findEditButton().exists()).toBe(true); - }); + expect(findEditButton().exists()).toBe(true); + }); + + it('is hidden when `showEdit` prop is false', () => { + createComponent({ showEdit: false }); + + expect(findEditButton().exists()).toBe(false); + }); - it('does not show edit button when `showEdit` prop is false', () => { - createComponent({ showEdit: false }); + it('emits `startEditing` event when clicked', () => { + createComponent(); + findEditButton().vm.$emit('click'); - expect(findEditButton().exists()).toBe(false); + expect(wrapper.emitted('startEditing')).toEqual([[]]); + }); }); - it('emits `startEditing` event when edit button is clicked', () => { - createComponent(); - findEditButton().vm.$emit('click'); + describe('emoji picker', () => { + it('is visible when `showAwardEmoji` prop is true', () => { + createComponent(); + + expect(findEmojiButton().exists()).toBe(true); + }); + + it('is hidden when `showAwardEmoji` prop is false', () => { + createComponent({ showAwardEmoji: false }); - expect(wrapper.emitted('startEditing')).toEqual([[]]); + expect(findEmojiButton().exists()).toBe(false); + }); + + it('commits mutation on click', async () => { + const awardName = 'carrot'; + + createComponent(); + + findEmojiButton().vm.$emit('click', awardName); + + await waitForPromises(); + + expect(findEmojiButton().emitted('errors')).toEqual(undefined); + expect(addEmojiMutationResolver).toHaveBeenCalledWith({ + awardableId: noteId, + name: awardName, + }); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 7adc8295ebb..1dc075a3e62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1226,14 +1226,14 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.24.0.tgz#bc8265919aa04b06cd08be91637471bad195936d" integrity sha512-R4s5qJUFUIbPflknpw1aI/PchiNq65vY7LVsJZnQkY+vi+AgmsETdut/AdferbGWmeWMU0q2wuVu9phE8lDUgA== -"@gitlab/ui@56.2.0": - version "56.2.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-56.2.0.tgz#7163dbb161b995fe69b5c4436c85fd3dbd127f12" - integrity sha512-dhhtgK2oq69zVkMnKj8SS/zqVy3VBdT2kT+Y+iTPxtHLmqr8SRgUvG0Yf247h/Inza5Facaa5qzweCXEK062Gw== +"@gitlab/ui@56.2.1": + version "56.2.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-56.2.1.tgz#ca503f5b70a912c98da6bb30f9e22b6fff710d72" + integrity sha512-WeTNZT+Bo3jZ2gv7efuTyu4fm5Ks6KbpBLaJmyFtDMe38+o0RpgRgMjc+Zh0nLP3RuejhBUPkYsK9WxhdVmiog== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.20.1" - dompurify "^2.4.4" + dompurify "^2.4.5" echarts "^5.3.2" iframe-resizer "^4.3.2" lodash "^4.17.20" |