diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-22 13:39:34 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-22 13:39:34 +0000 |
commit | e23c409e66b47a970a3cb83ac15d2ff906e75ce0 (patch) | |
tree | b1d580cd64c5d67a81a9445da42e82ceeefa96c5 /spec | |
parent | 2fa173410ad24b37aba6450ae4530ec231844d86 (diff) | |
download | gitlab-ce-e23c409e66b47a970a3cb83ac15d2ff906e75ce0.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
27 files changed, 569 insertions, 294 deletions
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index 2cf56f93cf9..cb9188cf171 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'Dashboard > User filters projects' do let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) } before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/groups/labels/sort_labels_spec.rb b/spec/features/groups/labels/sort_labels_spec.rb index fba166449f8..af4d39bc6fa 100644 --- a/spec/features/groups/labels/sort_labels_spec.rb +++ b/spec/features/groups/labels/sort_labels_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'Sort labels', :js do let!(:label2) { create(:group_label, title: 'Bar', description: 'Fusce consequat', group: group) } before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) + group.add_maintainer(user) sign_in(user) diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb index 22d7ff91d41..631aa940270 100644 --- a/spec/features/groups/milestones_sorting_spec.rb +++ b/spec/features/groups/milestones_sorting_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Milestones sorting', :js do let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user } before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) sign_in(user) end diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb index e64c19d4708..a9c014a9408 100644 --- a/spec/features/help_dropdown_spec.rb +++ b/spec/features/help_dropdown_spec.rb @@ -59,6 +59,10 @@ RSpec.describe "Help Dropdown", :js do expect(page).to have_text('Your GitLab Version') expect(page).to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}") expect(page).to have_selector('.version-check-badge') + expect(page).to have_selector( + 'a[data-testid="gitlab-version-container"][href="/help/update/index"]' + ) + expect(page).to have_selector('.version-check-badge[href="/help/update/index"]') expect(page).to have_text(ui_text) end end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 727f9aa486e..361a07ebd0b 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe 'Branches' do - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } let(:repository) { project.repository } context 'logged in as developer' do @@ -175,7 +175,7 @@ RSpec.describe 'Branches' do search_for_branch('fix') expect(page).not_to have_content('fix') - expect(all('.all-branches').last).to have_selector('li', count: 0) + expect(all('.all-branches', wait: false).last).to have_selector('li', count: 0) end end @@ -233,7 +233,7 @@ RSpec.describe 'Branches' do end context 'with one or more pipeline', :js do - let(:project) { create(:project, :public, :empty_repo) } + let_it_be(:project) { create(:project, :public, :empty_repo) } before do sha = create_file(branch_name: "branch") diff --git a/spec/features/projects/labels/sort_labels_spec.rb b/spec/features/projects/labels/sort_labels_spec.rb index ecbc4b524dc..6a16f474056 100644 --- a/spec/features/projects/labels/sort_labels_spec.rb +++ b/spec/features/projects/labels/sort_labels_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'Sort labels', :js do let!(:label2) { create(:label, title: 'Bar', description: 'Fusce consequat', project: project) } before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb index c47350fb663..5c379ac1034 100644 --- a/spec/features/projects/milestones/milestones_sorting_spec.rb +++ b/spec/features/projects/milestones/milestones_sorting_spec.rb @@ -21,6 +21,7 @@ RSpec.describe 'Milestones sorting', :js do end before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) create(:milestone, start_date: 7.days.from_now, due_date: 10.days.from_now, title: "a", project: project) create(:milestone, start_date: 6.days.from_now, due_date: 11.days.from_now, title: "c", project: project) create(:milestone, start_date: 5.days.from_now, due_date: 12.days.from_now, title: "b", project: project) diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb index b9b28398279..15f7dae502d 100644 --- a/spec/features/projects/user_sorts_projects_spec.rb +++ b/spec/features/projects/user_sorts_projects_spec.rb @@ -44,6 +44,7 @@ RSpec.describe 'User sorts projects and order persists' do context "from explore projects" do before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) sign_in(user) visit(explore_projects_path) find('#sort-projects-dropdown').click @@ -55,6 +56,7 @@ RSpec.describe 'User sorts projects and order persists' do context 'from dashboard projects' do before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) sign_in(user) visit(dashboard_projects_path) find('#sort-projects-dropdown').click @@ -66,6 +68,7 @@ RSpec.describe 'User sorts projects and order persists' do context 'from group homepage', :js do before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) stub_feature_flags(group_overview_tabs_vue: false) sign_in(user) visit(group_canonical_path(group)) @@ -80,6 +83,7 @@ RSpec.describe 'User sorts projects and order persists' do context 'from group details', :js do before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) stub_feature_flags(group_overview_tabs_vue: false) sign_in(user) visit(details_group_path(group)) diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb index 879ffd2932b..8ac17413df3 100644 --- a/spec/features/projects/wikis_spec.rb +++ b/spec/features/projects/wikis_spec.rb @@ -3,6 +3,10 @@ require "spec_helper" RSpec.describe 'Project wikis', :js do + before do + stub_feature_flags(gl_listbox_for_sort_dropdowns: false) + end + let_it_be(:user) { create(:user) } let(:wiki) { create(:project_wiki, user: user, project: project) } diff --git a/spec/fixtures/packages/rubygems/package.gemspec b/spec/fixtures/packages/rubygems/package.gemspec index ea03414cc6f..60acd078fad 100644 --- a/spec/fixtures/packages/rubygems/package.gemspec +++ b/spec/fixtures/packages/rubygems/package.gemspec @@ -30,7 +30,7 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.post_install_message = 'Installed, thank you!' s.rdoc_options = ['--main', 'README.md'] - s.required_ruby_version = '>= 2.7.0' + s.required_ruby_version = '>= 2.7.0' # rubocop:disable Gemspec/RequiredRubyVersion s.required_rubygems_version = '>= 1.8.11' s.requirements = 'A high powered server or calculator' s.rubygems_version = '1.8.09' diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js index 07c6cca535a..c973960e683 100644 --- a/spec/frontend/listbox/index_spec.js +++ b/spec/frontend/listbox/index_spec.js @@ -1,6 +1,6 @@ import { nextTick } from 'vue'; -import { getAllByRole, getByRole } from '@testing-library/dom'; -import { GlDropdown } from '@gitlab/ui'; +import { getAllByRole, getByRole, getByTestId } from '@testing-library/dom'; +import { GlDropdown, GlListbox } from '@gitlab/ui'; import { createWrapper } from '@vue/test-utils'; import { initListbox, parseAttributes } from '~/listbox'; import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; @@ -28,20 +28,6 @@ describe('initListbox', () => { instance = initListbox(...args); }; - // TODO: Rewrite these finders to use better semantics once the - // implementation is switched to GlListbox - // https://gitlab.com/gitlab-org/gitlab/-/issues/348738 - const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle'); - const findItem = (text) => getByRole(document.body, 'menuitem', { name: text }); - const findItems = () => getAllByRole(document.body, 'menuitem'); - const findSelectedItems = () => - findItems().filter( - (menuitem) => - !menuitem - .querySelector('.gl-new-dropdown-item-check-icon') - .classList.contains('gl-visibility-hidden'), - ); - it('returns null given no element', () => { setup(); @@ -53,63 +39,141 @@ describe('initListbox', () => { }); describe('given a valid element', () => { - let onChangeSpy; + describe('when `glListboxForSortDropdowns` FF is enabled', () => { + let onChangeSpy; - beforeEach(async () => { - setHTMLFixture(fixture); - onChangeSpy = jest.fn(); - setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy }); + const listbox = () => createWrapper(instance).findComponent(GlListbox); + const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle'); + const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true }); - await nextTick(); - }); + beforeEach(async () => { + window.gon.features = { glListboxForSortDropdowns: true }; + setHTMLFixture(fixture); + onChangeSpy = jest.fn(); + setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy }); - afterEach(() => { - resetHTMLFixture(); - }); + await nextTick(); + }); - it('returns an instance', () => { - expect(instance).not.toBe(null); - }); + afterEach(() => { + resetHTMLFixture(); + }); - it('renders button with selected item text', () => { - expect(findToggleButton().textContent.trim()).toBe('Bar'); - }); + it('returns an instance', () => { + expect(instance).not.toBe(null); + }); - it('has the correct item selected', () => { - const selectedItems = findSelectedItems(); - expect(selectedItems).toHaveLength(1); - expect(selectedItems[0].textContent.trim()).toBe('Bar'); - }); + it('renders button with selected item text', () => { + expect(findToggleButton().textContent.trim()).toBe('Bar'); + }); - it('applies additional classes from the original element', () => { - expect(instance.$el.classList).toContain('test-class-1', 'test-class-2'); + it('has the correct item selected', () => { + const selectedItems = findSelectedItems(); + expect(selectedItems).toHaveLength(1); + expect(selectedItems[0].textContent.trim()).toBe('Bar'); + }); + + it('applies additional classes from the original element', () => { + expect(instance.$el.classList).toContain('test-class-1', 'test-class-2'); + }); + + describe.each(parsedAttributes.items)('selecting an item', (item) => { + beforeEach(async () => { + listbox().vm.$emit('select', item.value); + await nextTick(); + }); + + it('calls the onChange callback with the item', () => { + expect(onChangeSpy).toHaveBeenCalledWith(item); + }); + + it('updates the toggle button text', () => { + expect(findToggleButton().textContent.trim()).toBe(item.text); + }); + + it('marks the item as selected', () => { + const selectedItems = findSelectedItems(); + expect(selectedItems).toHaveLength(1); + expect(selectedItems[0].textContent.trim()).toBe(item.text); + }); + }); + + it('passes the "right" prop through to the underlying component', () => { + expect(listbox().props('right')).toBe(parsedAttributes.right); + }); }); - describe.each(parsedAttributes.items)('clicking on an item', (item) => { + describe('when `glListboxForSortDropdowns` FF is disabled', () => { + let onChangeSpy; + + const ITEM_ROLE = 'menuitem'; + const dropdown = () => createWrapper(instance).findComponent(GlDropdown); + + const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle'); + const findItem = (text) => getByRole(document.body, ITEM_ROLE, { name: text }); + const findItems = () => getAllByRole(document.body, ITEM_ROLE); + const findSelectedItems = () => + findItems().filter( + (item) => + !item + .querySelector('.gl-new-dropdown-item-check-icon') + .classList.contains('gl-visibility-hidden'), + ); beforeEach(async () => { - findItem(item.text).click(); + window.gon.features = { glListboxForSortDropdowns: false }; + setHTMLFixture(fixture); + onChangeSpy = jest.fn(); + setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy }); await nextTick(); }); - it('calls the onChange callback with the item', () => { - expect(onChangeSpy).toHaveBeenCalledWith(item); + afterEach(() => { + resetHTMLFixture(); + }); + + it('returns an instance', () => { + expect(instance).not.toBe(null); }); - it('updates the toggle button text', () => { - expect(findToggleButton().textContent.trim()).toBe(item.text); + it('renders button with selected item text', () => { + expect(findToggleButton().textContent.trim()).toBe('Bar'); }); - it('marks the item as selected', () => { + it('has the correct item selected', () => { const selectedItems = findSelectedItems(); expect(selectedItems).toHaveLength(1); - expect(selectedItems[0].textContent.trim()).toBe(item.text); + expect(selectedItems[0].textContent.trim()).toBe('Bar'); + }); + + it('applies additional classes from the original element', () => { + expect(instance.$el.classList).toContain('test-class-1', 'test-class-2'); }); - }); - it('passes the "right" prop through to the underlying component', () => { - const wrapper = createWrapper(instance).findComponent(GlDropdown); - expect(wrapper.props('right')).toBe(parsedAttributes.right); + describe.each(parsedAttributes.items)('selecting an item', (item) => { + beforeEach(async () => { + findItem(item.text).click(); + await nextTick(); + }); + + it('calls the onChange callback with the item', () => { + expect(onChangeSpy).toHaveBeenCalledWith(item); + }); + + it('updates the toggle button text', () => { + expect(findToggleButton().textContent.trim()).toBe(item.text); + }); + + it('marks the item as selected', () => { + const selectedItems = findSelectedItems(); + expect(selectedItems).toHaveLength(1); + expect(selectedItems[0].textContent.trim()).toBe(item.text); + }); + }); + + it('passes the "right" prop through to the underlying component', () => { + expect(dropdown().props('right')).toBe(parsedAttributes.right); + }); }); }); }); diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js index 6da1861ea7c..0d52542397f 100644 --- a/spec/frontend/repository/components/blob_controls_spec.js +++ b/spec/frontend/repository/components/blob_controls_spec.js @@ -8,9 +8,13 @@ import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql' import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createRouter from '~/repository/router'; import { updateElementsVisibility } from '~/repository/utils/dom'; +import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; +import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import { blobControlsDataMock, refMock } from '../mock_data'; jest.mock('~/repository/utils/dom'); +jest.mock('~/behaviors/shortcuts/shortcuts_blob'); +jest.mock('~/blob/blob_line_permalink_updater'); let router; let wrapper; @@ -82,4 +86,12 @@ describe('Blob controls component', () => { expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true); }, ); + + it('loads the ShortcutsBlob', () => { + expect(ShortcutsBlob).toHaveBeenCalled(); + }); + + it('loads the BlobLinePermalinkUpdater', () => { + expect(BlobLinePermalinkUpdater).toHaveBeenCalled(); + }); }); diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js index 6699ae5fb69..f6bb8f5de6c 100644 --- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js +++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js @@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; +import { helpPagePath } from '~/helpers/help_page_helper'; import axios from '~/lib/utils/axios_utils'; import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue'; @@ -9,6 +10,8 @@ describe('GitlabVersionCheck', () => { let wrapper; let mock; + const UPGRADE_DOCS_URL = helpPagePath('update/index'); + const defaultResponse = { code: 200, res: { severity: 'success' }, @@ -102,6 +105,10 @@ describe('GitlabVersionCheck', () => { it(`variant is ${expectedUI.variant}`, () => { expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant); }); + + it(`link is ${UPGRADE_DOCS_URL}`, () => { + expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL); + }); }); }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index b047e0dc8d7..b5111fdcae4 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -19,6 +19,7 @@ import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql'; import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; +import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import { temporaryConfig } from '~/graphql_shared/issuable_client'; @@ -29,6 +30,7 @@ import { workItemResponseFactory, workItemTitleSubscriptionResponse, workItemWeightSubscriptionResponse, + workItemAssigneesSubscriptionResponse, } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -46,6 +48,9 @@ describe('WorkItemDetail component', () => { const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const assigneesSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemAssigneesSubscriptionResponse); const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); const findAlert = () => wrapper.findComponent(GlAlert); @@ -80,6 +85,7 @@ describe('WorkItemDetail component', () => { [workItemQuery, handler], [workItemTitleSubscription, subscriptionHandler], [workItemDatesSubscription, datesSubscriptionHandler], + [workItemAssigneesSubscription, assigneesSubscriptionHandler], confidentialityMock, ]; @@ -413,6 +419,30 @@ describe('WorkItemDetail component', () => { }); }); + describe('assignees subscription', () => { + describe('when the assignees widget exists', () => { + it('calls the assignees subscription', async () => { + createComponent(); + await waitForPromises(); + + expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({ + issuableId: workItemQueryResponse.data.workItem.id, + }); + }); + }); + + describe('when the assignees widget does not exist', () => { + it('does not call the assignees subscription', async () => { + const response = workItemResponseFactory({ assigneesWidgetPresent: false }); + const handler = jest.fn().mockResolvedValue(response); + createComponent({ handler, workItemsMvc2Enabled: true }); + await waitForPromises(); + + expect(assigneesSubscriptionHandler).not.toHaveBeenCalled(); + }); + }); + }); + describe('dates subscription', () => { describe('when the due date widget exists', () => { it('calls the dates subscription', async () => { diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index e1bc8d2f6b7..d1108a57e23 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -444,6 +444,22 @@ export const workItemWeightSubscriptionResponse = { }, }; +export const workItemAssigneesSubscriptionResponse = { + data: { + issuableAssigneesUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemAssigneesWeight', + assignees: { + nodes: [mockAssignees[0]], + }, + }, + ], + }, + }, +}; + export const workItemHierarchyEmptyResponse = { data: { workItem: { diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index ab370e2ca8b..39e8eb837c0 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo'; import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import { + workItemAssigneesSubscriptionResponse, workItemDatesSubscriptionResponse, workItemResponseFactory, workItemTitleSubscriptionResponse, @@ -13,6 +14,7 @@ import App from '~/work_items/components/app.vue'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql'; import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; +import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import { createRouter } from '~/work_items/router'; @@ -26,6 +28,9 @@ describe('Work items router', () => { const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); + const assigneesSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemAssigneesSubscriptionResponse); const createComponent = async (routeArg) => { const router = createRouter('/work_item'); @@ -37,6 +42,7 @@ describe('Work items router', () => { [workItemQuery, workItemQueryHandler], [workItemDatesSubscription, datesSubscriptionHandler], [workItemTitleSubscription, titleSubscriptionHandler], + [workItemAssigneesSubscription, assigneesSubscriptionHandler], ]; if (IS_EE) { diff --git a/spec/graphql/mutations/work_items/update_widgets_spec.rb b/spec/graphql/mutations/work_items/update_widgets_spec.rb deleted file mode 100644 index 2e54b81b5c7..00000000000 --- a/spec/graphql/mutations/work_items/update_widgets_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Mutations::WorkItems::UpdateWidgets do - include GraphqlHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } - - let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } - - describe '#resolve' do - before do - stub_spam_services - end - - context 'when no work item matches the given id' do - let(:current_user) { developer } - let(:gid) { global_id_of(id: non_existing_record_id, model_name: WorkItem.name) } - - it 'raises an error' do - expect { mutation.resolve(id: gid, resolve: true) }.to raise_error( - Gitlab::Graphql::Errors::ResourceNotAvailable, - Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR - ) - end - end - - context 'when user can access the requested work item', :aggregate_failures do - let(:current_user) { developer } - let(:args) { {} } - - let_it_be(:work_item) { create(:work_item, project: project) } - - subject { mutation.resolve(id: work_item.to_global_id, **args) } - - context 'when `:work_items` is disabled for a project' do - let_it_be(:project2) { create(:project) } - - it 'returns an error' do - stub_feature_flags(work_items: project2) # only enable `work_item` for project2 - - expect(subject[:errors]).to contain_exactly('`work_items` feature flag disabled for this project') - end - end - - context 'when resolved with an input for description widget' do - let(:args) { { description_widget: { description: "updated description" } } } - - it 'returns the updated work item' do - expect(subject[:work_item].description).to eq("updated description") - expect(subject[:errors]).to be_empty - end - end - end - end -end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 00e620832b3..0b6e51c26f5 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -388,22 +388,30 @@ RSpec.describe GroupsHelper do end describe '#show_thanks_for_purchase_alert?' do - subject { helper.show_thanks_for_purchase_alert? } + subject { helper.show_thanks_for_purchase_alert?(quantity) } - it 'returns true with purchased_quantity present in params' do - allow(controller).to receive(:params) { { purchased_quantity: '1' } } + context 'with quantity present' do + let(:quantity) { 1 } - is_expected.to be_truthy + it 'returns true' do + is_expected.to be_truthy + end end - it 'returns false with purchased_quantity not present in params' do - is_expected.to be_falsey + context 'with quantity not present' do + let(:quantity) { nil } + + it 'returns false' do + is_expected.to be_falsey + end end - it 'returns false with purchased_quantity is empty in params' do - allow(controller).to receive(:params) { { purchased_quantity: '' } } + context 'with quantity empty' do + let(:quantity) { '' } - is_expected.to be_falsey + it 'returns false' do + is_expected.to be_falsey + end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7ee381b29ea..190227080cd 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -5362,7 +5362,7 @@ RSpec.describe Ci::Build do end describe '#clone' do - let_it_be(:user) { FactoryBot.build(:user) } + let_it_be(:user) { create(:user) } context 'when given new job variables' do context 'when the cloned build has an action' do diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb index df3ee69621b..eba393da2b7 100644 --- a/spec/presenters/commit_presenter_spec.rb +++ b/spec/presenters/commit_presenter_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' RSpec.describe CommitPresenter do - let(:project) { create(:project, :repository) } let(:commit) { project.commit } - let(:user) { create(:user) } let(:presenter) { described_class.new(commit, current_user: user) } + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:project) { create(:project, :repository) } + describe '#web_path' do it { expect(presenter.web_path).to eq("/#{project.full_path}/-/commit/#{commit.sha}") } end diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb index 05e5a9d4f1d..ca7f96b567d 100644 --- a/spec/presenters/projects/security/configuration_presenter_spec.rb +++ b/spec/presenters/projects/security/configuration_presenter_spec.rb @@ -6,9 +6,8 @@ RSpec.describe Projects::Security::ConfigurationPresenter do include Gitlab::Routing.url_helpers using RSpec::Parameterized::TableSyntax - let(:project_with_repo) { create(:project, :repository) } - let(:project_with_no_repo) { create(:project) } - let(:current_user) { create(:user) } + let_it_be(:current_user) { build_stubbed(:user) } + let(:presenter) { described_class.new(project, current_user: current_user) } before do @@ -19,9 +18,9 @@ RSpec.describe Projects::Security::ConfigurationPresenter do subject(:html_data) { presenter.to_html_data_attribute } context 'when latest default branch pipeline`s source is not auto devops' do - let(:project) { project_with_repo } + let_it_be(:project) { create(:project, :repository) } - let(:pipeline) do + let_it_be(:pipeline) do create( :ci_pipeline, project: project, @@ -119,6 +118,16 @@ RSpec.describe Projects::Security::ConfigurationPresenter do context 'when the job has more than one report' do let(:features) { Gitlab::Json.parse(html_data[:features]) } + let(:project) { create(:project, :repository) } + + let(:pipeline) do + create( + :ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha + ) + end let!(:artifacts) do { artifacts: { reports: { other_job: ['gl-other-report.json'], sast: ['gl-sast-report.json'] } } } @@ -161,6 +170,8 @@ RSpec.describe Projects::Security::ConfigurationPresenter do end context "while retrieving information about gitlab ci file" do + let(:project) { create(:project, :repository) } + context 'when a .gitlab-ci.yml file exists' do let!(:ci_config) do project.repository.create_file( @@ -189,7 +200,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do end context 'when the project is empty' do - let(:project) { project_with_no_repo } + let(:project) { create(:project) } it 'includes a blank gitlab_ci history path' do expect(html_data[:gitlab_ci_history_path]).to eq('') @@ -197,7 +208,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do end context 'when the project has no default branch set' do - let(:project) { project_with_repo } + let(:project) { create(:project, :repository) } it 'includes the path to gitlab_ci history' do allow(project).to receive(:default_branch).and_return(nil) @@ -207,9 +218,9 @@ RSpec.describe Projects::Security::ConfigurationPresenter do end context "when the latest default branch pipeline's source is auto devops" do - let(:project) { project_with_repo } + let_it_be(:project) { create(:project, :repository) } - let(:pipeline) do + let_it_be(:pipeline) do create( :ci_pipeline, :auto_devops_source, @@ -256,7 +267,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do end context 'when the project has no default branch pipeline' do - let(:project) { project_with_repo } + let_it_be(:project) { create(:project, :repository) } it 'reports that auto devops is disabled' do expect(html_data[:auto_devops_enabled]).to be_falsy diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb index f6835936418..78e7ec39ee3 100644 --- a/spec/requests/api/graphql/milestone_spec.rb +++ b/spec/requests/api/graphql/milestone_spec.rb @@ -5,8 +5,12 @@ require 'spec_helper' RSpec.describe 'Querying a Milestone' do include GraphqlHelpers + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, group: group) } let_it_be(:guest) { create(:user) } - let_it_be(:project) { create(:project) } + let_it_be(:inherited_guest) { create(:user) } + let_it_be(:inherited_reporter) { create(:user) } + let_it_be(:inherited_developer) { create(:user) } let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:release_a) { create(:release, project: project) } let_it_be(:release_b) { create(:release, project: project) } @@ -14,116 +18,137 @@ RSpec.describe 'Querying a Milestone' do before_all do milestone.releases << [release_a, release_b] project.add_guest(guest) + group.add_guest(inherited_guest) + group.add_reporter(inherited_reporter) + group.add_developer(inherited_developer) end let(:expected_release_nodes) do contain_exactly(a_graphql_entity_for(release_a), a_graphql_entity_for(release_b)) end - context 'when we post the query' do - let(:current_user) { nil } - let(:query) do - graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, all_graphql_fields_for('Milestone')) - end + shared_examples 'returns the milestone successfully' do + it_behaves_like 'a working graphql query' - subject { graphql_data['milestone'] } + it { is_expected.to include('title' => milestone.name) } - before do - post_graphql(query, current_user: current_user) + it 'contains release information' do + is_expected.to include('releases' => include('nodes' => expected_release_nodes)) end + end - context 'when the user has access to the milestone' do - let(:current_user) { guest } - - it_behaves_like 'a working graphql query' - - it { is_expected.to include('title' => milestone.name) } - - it 'contains release information' do - is_expected.to include('releases' => include('nodes' => expected_release_nodes)) + context 'when we post the query' do + context 'and the project is private' do + let(:query) do + graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, all_graphql_fields_for('Milestone')) end - end - context 'when the user does not have access to the milestone' do - it_behaves_like 'a working graphql query' - - it { is_expected.to be_nil } - end + subject { graphql_data['milestone'] } - context 'when ID argument is missing' do - let(:query) do - graphql_query_for('milestone', {}, 'title') + before do + post_graphql(query, current_user: current_user) end - it 'raises an exception' do - expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id")) + context 'when the user is a direct project member' do + context 'and the user is a guest' do + let(:current_user) { guest } + + it_behaves_like 'returns the milestone successfully' + + context 'when there are two milestones' do + let_it_be(:milestone_b) { create(:milestone, project: project) } + + let(:milestone_fields) do + <<~GQL + fragment milestoneFields on Milestone { + #{all_graphql_fields_for('Milestone', max_depth: 1)} + releases { nodes { #{all_graphql_fields_for('Release', max_depth: 1)} } } + } + GQL + end + + let(:single_query) do + <<~GQL + query ($id_a: MilestoneID!) { + a: milestone(id: $id_a) { ...milestoneFields } + } + + #{milestone_fields} + GQL + end + + let(:multi_query) do + <<~GQL + query ($id_a: MilestoneID!, $id_b: MilestoneID!) { + a: milestone(id: $id_a) { ...milestoneFields } + b: milestone(id: $id_b) { ...milestoneFields } + } + #{milestone_fields} + GQL + end + + it 'returns the correct releases associated with each milestone' do + r = run_with_clean_state(multi_query, + context: { current_user: current_user }, + variables: { + id_a: global_id_of(milestone).to_s, + id_b: milestone_b.to_global_id.to_s + }) + + expect(r.to_h['errors']).to be_blank + expect(graphql_dig_at(r.to_h, :data, :a, :releases, :nodes)).to match expected_release_nodes + expect(graphql_dig_at(r.to_h, :data, :b, :releases, :nodes)).to be_empty + end + + it 'does not suffer from N+1 performance issues' do + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(single_query, + context: { current_user: current_user }, + variables: { id_a: milestone.to_global_id.to_s }) + end + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(multi_query, + context: { current_user: current_user }, + variables: { + id_a: milestone.to_global_id.to_s, + id_b: milestone_b.to_global_id.to_s + }) + end + + expect(multi).not_to exceed_query_limit(baseline) + end + end + end end - end - end - context 'when there are two milestones' do - let_it_be(:milestone_b) { create(:milestone, project: project) } - - let(:current_user) { guest } - let(:milestone_fields) do - <<~GQL - fragment milestoneFields on Milestone { - #{all_graphql_fields_for('Milestone', max_depth: 1)} - releases { nodes { #{all_graphql_fields_for('Release', max_depth: 1)} } } - } - GQL - end + context 'when the user is an inherited member from the group' do + where(:user) { [ref(:inherited_guest), ref(:inherited_reporter), ref(:inherited_developer)] } - let(:single_query) do - <<~GQL - query ($id_a: MilestoneID!) { - a: milestone(id: $id_a) { ...milestoneFields } - } + with_them do + let(:current_user) { user } - #{milestone_fields} - GQL - end + it_behaves_like 'returns the milestone successfully' + end + end - let(:multi_query) do - <<~GQL - query ($id_a: MilestoneID!, $id_b: MilestoneID!) { - a: milestone(id: $id_a) { ...milestoneFields } - b: milestone(id: $id_b) { ...milestoneFields } - } - #{milestone_fields} - GQL - end + context 'when unauthenticated' do + let(:current_user) { nil } - it 'produces correct results' do - r = run_with_clean_state(multi_query, - context: { current_user: current_user }, - variables: { - id_a: global_id_of(milestone).to_s, - id_b: milestone_b.to_global_id.to_s - }) - - expect(r.to_h['errors']).to be_blank - expect(graphql_dig_at(r.to_h, :data, :a, :releases, :nodes)).to match expected_release_nodes - expect(graphql_dig_at(r.to_h, :data, :b, :releases, :nodes)).to be_empty - end + it_behaves_like 'a working graphql query' - it 'does not suffer from N+1 performance issues' do - baseline = ActiveRecord::QueryRecorder.new do - run_with_clean_state(single_query, - context: { current_user: current_user }, - variables: { id_a: milestone.to_global_id.to_s }) - end + it { is_expected.to be_nil } - multi = ActiveRecord::QueryRecorder.new do - run_with_clean_state(multi_query, - context: { current_user: current_user }, - variables: { - id_a: milestone.to_global_id.to_s, - id_b: milestone_b.to_global_id.to_s - }) - end + context 'when ID argument is missing' do + let(:query) do + graphql_query_for('milestone', {}, 'title') + end - expect(multi).not_to exceed_query_limit(baseline) + it 'raises an exception' do + expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id")) + end + end + end end end end diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb deleted file mode 100644 index 2a5cb937a2f..00000000000 --- a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Update work item widgets' do - include GraphqlHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } - let_it_be(:work_item, refind: true) { create(:work_item, project: project) } - - let(:input) { { 'descriptionWidget' => { 'description' => 'updated description' } } } - let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) } - let(:mutation) do - graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s), <<~FIELDS) - errors - workItem { - description - widgets { - type - ... on WorkItemWidgetDescription { - description - } - } - } - FIELDS - end - - context 'the user is not allowed to update a work item' do - let(:current_user) { create(:user) } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to update a work item', :aggregate_failures do - let(:current_user) { developer } - - it_behaves_like 'update work item description widget' do - let(:new_description) { 'updated description' } - end - - it_behaves_like 'has spam protection' do - let(:mutation_class) { ::Mutations::WorkItems::UpdateWidgets } - end - - context 'when the work_items feature flag is disabled' do - before do - stub_feature_flags(work_items: false) - end - - it 'does not update the work item and returns and error' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to not_change(work_item, :description) - - expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') - end - end - end -end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 6169bc9b2a2..02d29601ceb 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -505,13 +505,35 @@ RSpec.describe API::Groups do group3.add_maintainer(user2) end - it 'returns an array of groups the user has at least master access' do - get api('/groups', user2), params: { min_access_level: 40 } + context 'with min_access_level parameter' do + it 'returns an array of groups the user has at least master access' do + get api('/groups', user2), params: { min_access_level: 40 } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(response_groups).to contain_exactly(group2.id, group3.id) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(response_groups).to contain_exactly(group2.id, group3.id) + end + + context 'distinct count with present_groups_select_all feature flag' do + subject { get api('/groups', user2), params: { min_access_level: 40 } } + + it 'counts with *' do + count_sql = /#{Regexp.escape('SELECT count(*)')}/i + expect { subject }.to make_queries_matching count_sql + end + + context 'when present_groups_select_all feature flag is disabled' do + before do + stub_feature_flags(present_groups_select_all: false) + end + + it 'counts with count_column' do + count_sql = /#{Regexp.escape('SELECT count(count_column)')}/i + expect { subject }.to make_queries_matching count_sql + end + end + end end end diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb index 7cf25468eb2..582ea202187 100644 --- a/spec/support/matchers/event_store.rb +++ b/spec/support/matchers/event_store.rb @@ -6,7 +6,7 @@ RSpec::Matchers.define :publish_event do |expected_event_class| supports_block_expectations match do |proc| - raise ArgumentError, 'This matcher only supports block expectation' unless proc.respond_to?(:call) + raise ArgumentError, 'publish_event matcher only supports block expectation' unless proc.respond_to?(:call) @events ||= [] @@ -22,6 +22,8 @@ RSpec::Matchers.define :publish_event do |expected_event_class| end def match_data?(actual, expected) + return if actual.blank? || expected.blank? + values_match?(actual.keys, expected.keys) && actual.keys.all? do |key| values_match?(expected[key], actual[key]) @@ -33,7 +35,7 @@ RSpec::Matchers.define :publish_event do |expected_event_class| end failure_message do - message = "expected #{expected_event_class} with #{@expected_data} to be published" + message = "expected #{expected_event_class} with #{@expected_data || 'no data'} to be published" if @events.present? <<~MESSAGE @@ -46,7 +48,7 @@ RSpec::Matchers.define :publish_event do |expected_event_class| end match_when_negated do |proc| - raise ArgumentError, 'This matcher only supports block expectation' unless proc.respond_to?(:call) + raise ArgumentError, 'publish_event matcher only supports block expectation' unless proc.respond_to?(:call) allow(Gitlab::EventStore).to receive(:publish) @@ -57,7 +59,45 @@ RSpec::Matchers.define :publish_event do |expected_event_class| def events_list @events.map do |event| - " - #{event.class.name} #{event.data}" + " - #{event.class.name} with #{event.data}" end.join("\n") end end + +# not_publish_event enables multiple assertions on a single block, for example: +# expect { Model.create(invalid: :attribute) } +# .to not_change(Model, :count) +# .and not_publish_event(ModelCreated) +RSpec::Matchers.define :not_publish_event do |expected_event_class| + include RSpec::Matchers::Composable + + supports_block_expectations + + match do |proc| + raise ArgumentError, 'not_publish_event matcher only supports block expectation' unless proc.respond_to?(:call) + + @events ||= [] + + allow(Gitlab::EventStore).to receive(:publish) do |published_event| + @events << published_event + end + + proc.call + + @events.none? do |event| + event.instance_of?(expected_event_class) + end + end + + failure_message do + "expected #{expected_event_class} not to be published" + end + + chain :with do |_| # rubocop: disable Lint/UnreachableLoop + raise ArgumentError, 'not_publish_event does not permit .with to avoid ambiguity' + end + + match_when_negated do |proc| + raise ArgumentError, 'not_publish_event matcher does not support negation. Use `expect {}.to publish_event` instead' + end +end diff --git a/spec/support_specs/matchers/event_store_spec.rb b/spec/support_specs/matchers/event_store_spec.rb new file mode 100644 index 00000000000..3614d05fde8 --- /dev/null +++ b/spec/support_specs/matchers/event_store_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'json_schemer' + +load File.expand_path('../../../spec/support/matchers/event_store.rb', __dir__) + +RSpec.describe 'event store matchers', :aggregate_errors do + let(:event_type1) do + Class.new(Gitlab::EventStore::Event) do + def schema + { + 'type' => 'object', + 'properties' => { + 'id' => { 'type' => 'integer' } + }, + 'required' => %w[id] + } + end + end + end + + let(:event_type2) do + Class.new(Gitlab::EventStore::Event) do + def schema + { + 'type' => 'object', + 'properties' => { + 'id' => { 'type' => 'integer' } + }, + 'required' => %w[id] + } + end + end + end + + before do + stub_const('FakeEventType1', event_type1) + stub_const('FakeEventType2', event_type2) + end + + def publishing_event(event_type, data = {}) + ::Gitlab::EventStore.publish(event_type.new(data: data)) + end + + describe 'publish_event' do + it 'requires a block matcher' do + matcher = -> { expect(:anything).to publish_event(:anything) } # rubocop: disable RSpec/ExpectActual + + expect(&matcher).to raise_error( + ArgumentError, + 'publish_event matcher only supports block expectation' + ) + end + + it 'validates the event type' do + valid_event_type = -> do + expect { publishing_event(FakeEventType1, { 'id' => 1 }) } + .to publish_event(FakeEventType1).with('id' => 1) + end + + expect(&valid_event_type).not_to raise_error + + invalid_event_type = -> do + expect { publishing_event(FakeEventType1, { 'id' => 1 }) } + .to publish_event(FakeEventType2).with('id' => 1) + end + + expect(&invalid_event_type).to raise_error <<~MESSAGE + expected FakeEventType2 with {"id"=>1} to be published, but only the following events were published: + - FakeEventType1 with {"id"=>1} + MESSAGE + end + + it 'validates the event data' do + missing_data = -> do + expect { publishing_event(FakeEventType1, { 'id' => 1 }) } + .to publish_event(FakeEventType1) + end + + expect(&missing_data).to raise_error <<~MESSAGE + expected FakeEventType1 with no data to be published, but only the following events were published: + - FakeEventType1 with {"id"=>1} + MESSAGE + + different_data = -> do + expect { publishing_event(FakeEventType1, { 'id' => 1 }) } + .to publish_event(FakeEventType1).with({ 'id' => 2 }) + end + + expect(&different_data).to raise_error <<~MESSAGE + expected FakeEventType1 with {"id"=>2} to be published, but only the following events were published: + - FakeEventType1 with {"id"=>1} + MESSAGE + end + end + + describe 'not_publish_event' do + it 'requires a block matcher' do + matcher = -> { expect(:anything).to not_publish_event(:anything) } # rubocop: disable RSpec/ExpectActual + + expect(&matcher) + .to raise_error(ArgumentError, 'not_publish_event matcher only supports block expectation') + end + + it 'does not permit .with' do + matcher = -> do + expect { publishing_event(FakeEventType1, { 'id' => 1 }) } + .to not_publish_event(FakeEventType2).with({ 'id' => 1 }) + end + + expect(&matcher) + .to raise_error(ArgumentError, 'not_publish_event does not permit .with to avoid ambiguity') + end + + it 'validates the event type' do + matcher = -> do + expect { publishing_event(FakeEventType1, { 'id' => 1 }) } + .to not_publish_event(FakeEventType1) + end + + expect(&matcher) + .to raise_error('expected FakeEventType1 not to be published') + end + end +end diff --git a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb index 0e24810f835..2f423c72ca6 100644 --- a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb +++ b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb @@ -12,5 +12,11 @@ RSpec.describe 'layouts/header/_gitlab_version' do it 'renders the version check badge' do expect(rendered).to have_selector('.js-gitlab-version-check') end + + it 'renders the container as a link' do + expect(rendered).to have_selector( + 'a[data-testid="gitlab-version-container"][href="/help/update/index"]' + ) + end end end |