diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-07 09:09:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-07 09:09:21 +0300 |
commit | 97815325b875a7bde0793cb0777e36b275ae1c6c (patch) | |
tree | 7f5e56fe3b69c472a78922adc6f82fd29217eebc /spec | |
parent | 923a8c950c009e65fd17ae2ab6c5e245a66175b5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
6 files changed, 125 insertions, 97 deletions
diff --git a/spec/frontend/behaviors/toasts_spec.js b/spec/frontend/behaviors/toasts_spec.js new file mode 100644 index 00000000000..bad04758ea1 --- /dev/null +++ b/spec/frontend/behaviors/toasts_spec.js @@ -0,0 +1,59 @@ +import { initToastMessages } from '~/behaviors/toasts'; +import { setHTMLFixture } from 'helpers/fixtures'; +import showToast from '~/vue_shared/plugins/global_toast'; + +jest.mock('~/vue_shared/plugins/global_toast'); + +describe('initToastMessages', () => { + describe('when there are no messages', () => { + beforeEach(() => { + setHTMLFixture('<div></div>'); + + initToastMessages(); + }); + + it('does not display any toasts', () => { + expect(showToast).not.toHaveBeenCalled(); + }); + }); + + describe('when there is a message', () => { + const expectedMessage = 'toast with jam is great'; + + beforeEach(() => { + setHTMLFixture( + `<div> + <div class="js-toast-message" data-message="${expectedMessage}"></div> + </div>`, + ); + + initToastMessages(); + }); + + it('displays the message', () => { + expect(showToast).toHaveBeenCalledTimes(1); + expect(showToast).toHaveBeenCalledWith(expectedMessage); + }); + }); + + describe('when there are multiple messages', () => { + beforeEach(() => { + setHTMLFixture( + `<div> + <div class="js-toast-message" data-message="foo"></div> + <div class="js-toast-message" data-message="bar"></div> + <div class="js-toast-message" data-message="baz"></div> + </div>`, + ); + + initToastMessages(); + }); + + it('displays the messages', () => { + expect(showToast).toHaveBeenCalledTimes(3); + expect(showToast).toHaveBeenCalledWith('foo'); + expect(showToast).toHaveBeenCalledWith('bar'); + expect(showToast).toHaveBeenCalledWith('baz'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js index 01089422376..98cd87cb07b 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js @@ -1,6 +1,6 @@ import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { useFakeDate } from 'helpers/fake_date'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -21,7 +21,6 @@ import { ROOT_IMAGE_TOOLTIP, } from '~/packages_and_registries/container_registry/explorer/constants'; import getContainerRepositoryMetadata from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql'; -import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { containerRepositoryMock, imageTagsCountMock } from '../../mock_data'; describe('Details Header', () => { @@ -44,12 +43,6 @@ describe('Details Header', () => { const findMenu = () => wrapper.findComponent(GlDropdown); const findSize = () => wrapper.findByTestId('image-size'); - const waitForMetadataItems = async () => { - // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available - await nextTick(); - await nextTick(); - }; - const mountComponent = ({ propsData = { image: defaultImage }, resolver = jest.fn().mockResolvedValue(imageTagsCountMock()), @@ -66,7 +59,6 @@ describe('Details Header', () => { GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { - TitleArea, GlDropdown, GlDropdownItem, }, @@ -169,11 +161,9 @@ describe('Details Header', () => { describe('metadata items', () => { describe('tags count', () => { - it('displays "-- tags" while loading', async () => { + it('displays "-- tags" while loading', () => { mountComponent(); - await waitForMetadataItems(); - expect(findTagsCount().props('text')).toBe('-- tags'); }); @@ -181,7 +171,6 @@ describe('Details Header', () => { mountComponent(); await waitForPromises(); - await waitForMetadataItems(); expect(findTagsCount().props('text')).toBe('13 tags'); }); @@ -192,23 +181,20 @@ describe('Details Header', () => { }); await waitForPromises(); - await waitForMetadataItems(); expect(findTagsCount().props('text')).toBe('1 tag'); }); - it('has the correct icon', async () => { + it('has the correct icon', () => { mountComponent(); - await waitForMetadataItems(); expect(findTagsCount().props('icon')).toBe('tag'); }); }); describe('size metadata item', () => { - it('when size is not returned, it hides the item', async () => { + it('when size is not returned, it hides the item', () => { mountComponent(); - await waitForMetadataItems(); expect(findSize().exists()).toBe(false); }); @@ -220,7 +206,6 @@ describe('Details Header', () => { }); await waitForPromises(); - await waitForMetadataItems(); expect(findSize().props()).toMatchObject({ icon: 'disk', @@ -230,18 +215,11 @@ describe('Details Header', () => { }); describe('cleanup metadata item', () => { - it('has the correct icon', async () => { - mountComponent(); - await waitForMetadataItems(); - - expect(findCleanup().props('icon')).toBe('expire'); - }); - - it('when cleanup is not scheduled', async () => { + it('when cleanup is not scheduled has the right icon and props', () => { mountComponent(); - await waitForMetadataItems(); expect(findCleanup().props()).toMatchObject({ + icon: 'expire', text: CLEANUP_DISABLED_TEXT, textTooltip: CLEANUP_DISABLED_TOOLTIP, }); @@ -255,7 +233,7 @@ describe('Details Header', () => { ${UNFINISHED_STATUS} | ${'Cleanup incomplete'} | ${CLEANUP_UNFINISHED_TOOLTIP} `( 'when the status is $status the text is $text and the tooltip is $tooltip', - async ({ status, text, tooltip }) => { + ({ status, text, tooltip }) => { mountComponent({ propsData: { image: { @@ -267,7 +245,6 @@ describe('Details Header', () => { }, }, }); - await waitForMetadataItems(); expect(findCleanup().props()).toMatchObject({ text, @@ -278,25 +255,22 @@ describe('Details Header', () => { }); describe('visibility and created at', () => { - it('has created text', async () => { + it('has created text', () => { mountComponent(); - await waitForMetadataItems(); expect(findCreatedAndVisibility().props('text')).toBe('Created Nov 3, 2020 13:29'); }); describe('visibility icon', () => { - it('shows an eye when the project is public', async () => { + it('shows an eye when the project is public', () => { mountComponent(); - await waitForMetadataItems(); expect(findCreatedAndVisibility().props('icon')).toBe('eye'); }); - it('shows an eye slashed when the project is not public', async () => { + it('shows an eye slashed when the project is not public', () => { mountComponent({ propsData: { image: { ...defaultImage, project: { visibility: 'private' } } }, }); - await waitForMetadataItems(); expect(findCreatedAndVisibility().props('icon')).toBe('eye-slash'); }); diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js index ec1451de470..d6705bd1e88 100644 --- a/spec/frontend/vue_shared/components/registry/title_area_spec.js +++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js @@ -6,16 +6,12 @@ import component from '~/vue_shared/components/registry/title_area.vue'; describe('title area', () => { let wrapper; - const DYNAMIC_SLOT = 'metadata-dynamic-slot'; - const findSubHeaderSlot = () => wrapper.findByTestId('sub-header'); const findRightActionsSlot = () => wrapper.findByTestId('right-actions'); const findMetadataSlot = (name) => wrapper.findByTestId(name); const findTitle = () => wrapper.findByTestId('title'); const findAvatar = () => wrapper.findComponent(GlAvatar); const findInfoMessages = () => wrapper.findAllByTestId('info-message'); - const findDynamicSlot = () => wrapper.findByTestId(DYNAMIC_SLOT); - const findSlotOrderElements = () => wrapper.findAll('[slot-test]'); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => { @@ -132,50 +128,6 @@ describe('title area', () => { }); }); - describe('dynamic slots', () => { - const createDynamicSlot = () => { - return wrapper.vm.$createElement('div', { - attrs: { - 'data-testid': DYNAMIC_SLOT, - 'slot-test': true, - }, - }); - }; - - it('shows dynamic slots', async () => { - mountComponent(); - // we manually add a new slot to simulate dynamic slots being evaluated after the initial mount - wrapper.vm.$slots[DYNAMIC_SLOT] = createDynamicSlot(); - - // updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered - wrapper.vm.$forceUpdate(); - await nextTick(); - - expect(findDynamicSlot().exists()).toBe(true); - }); - - it('preserve the order of the slots', async () => { - mountComponent({ - slots: { - 'metadata-foo': '<div slot-test data-testid="metadata-foo"></div>', - }, - }); - - // rewrite slot putting dynamic slot as first - wrapper.vm.$slots = { - 'metadata-dynamic-slot': createDynamicSlot(), - 'metadata-foo': wrapper.vm.$slots['metadata-foo'], - }; - - // updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered - wrapper.vm.$forceUpdate(); - await nextTick(); - - expect(findSlotOrderElements().at(0).attributes('data-testid')).toBe(DYNAMIC_SLOT); - expect(findSlotOrderElements().at(1).attributes('data-testid')).toBe('metadata-foo'); - }); - }); - describe('info-messages', () => { it('shows a message when the props contains one', () => { mountComponent({ propsData: { infoMessages: [{ text: 'foo foo bar bar' }] } }); diff --git a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb index 52fbf6d2f9b..02b84085cc4 100644 --- a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb +++ b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb @@ -80,12 +80,16 @@ RSpec.describe Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValida it { expect(described_class.constraint_type_exists?).to be_truthy } it 'always asks the database' do - control = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do + control1 = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do described_class.constraint_type_exists? end - expect(control.count).to be >= 1 - expect { described_class.constraint_type_exists? }.to issue_same_number_of_queries_as(control) + control2 = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do + described_class.constraint_type_exists? + end + + expect(control1.count).to eq(1) + expect(control2.count).to eq(1) end end diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb index 955b6e53686..e6a281d8d59 100644 --- a/spec/requests/projects/merge_requests_controller_spec.rb +++ b/spec/requests/projects/merge_requests_controller_spec.rb @@ -132,4 +132,44 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code end end end + + describe 'GET #pipelines.json' do + before do + login_as(user) + end + + it 'avoids N+1 queries', :use_sql_query_cache do + create_pipeline + + # warm up + get pipelines_project_merge_request_path(project, merge_request, format: :json) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get pipelines_project_merge_request_path(project, merge_request, format: :json) + end + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)['count']['all']).to eq(1) + + create_pipeline + + expect do + get pipelines_project_merge_request_path(project, merge_request, format: :json) + end.to issue_same_number_of_queries_as(control) + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)['count']['all']).to eq(2) + end + + private + + def create_pipeline + create( + :ci_pipeline, :with_job, :success, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha + ) + end + end end diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb index 29ebe5a3918..cc912d8de66 100644 --- a/spec/support/matchers/exceed_query_limit.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -271,15 +271,11 @@ RSpec::Matchers.define :issue_fewer_queries_than do end end -RSpec::Matchers.define :issue_same_number_of_queries_as do +RSpec::Matchers.define :issue_same_number_of_queries_as do |expected| supports_block_expectations include ExceedQueryLimitHelpers - def control - block_arg - end - chain :or_fewer do @or_fewer = true end @@ -288,12 +284,15 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do @skip_cached = true end - def control_recorder - @control_recorder ||= ActiveRecord::QueryRecorder.new(&control) - end - def expected_count - control_recorder.count + # Some tests pass a query recorder, others pass a block that executes an action. + # Maybe, we need to clear the block usage and only accept query recorders. + + @expected_count ||= if expected.is_a?(ActiveRecord::QueryRecorder) + query_recorder_count(expected) + else + ActiveRecord::QueryRecorder.new(&block_arg).count + end end def verify_count(&block) |