From 8754d20bbb9e573d48e80d7f6aed1ded40a40263 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 11 Aug 2022 03:09:21 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- spec/factories/users/project_user_callouts.rb | 10 +++ spec/frontend/__helpers__/vue_test_utils_helper.js | 19 +++++- .../__helpers__/vue_test_utils_helper_spec.js | 57 ++++++++++++++-- .../runner/components/runner_bulk_delete_spec.js | 5 +- spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/project_spec.rb | 1 + spec/models/user_spec.rb | 79 ++++++++++++++++++++++ spec/models/users/project_callout_spec.rb | 23 +++++++ spec/requests/users/project_callouts_spec.rb | 58 ++++++++++++++++ .../lib/glfm/update_example_snapshots_spec.rb | 12 +++- .../users/dismiss_project_callout_service_spec.rb | 25 +++++++ 11 files changed, 278 insertions(+), 12 deletions(-) create mode 100644 spec/factories/users/project_user_callouts.rb create mode 100644 spec/models/users/project_callout_spec.rb create mode 100644 spec/requests/users/project_callouts_spec.rb create mode 100644 spec/services/users/dismiss_project_callout_service_spec.rb (limited to 'spec') diff --git a/spec/factories/users/project_user_callouts.rb b/spec/factories/users/project_user_callouts.rb new file mode 100644 index 00000000000..50e85315bb9 --- /dev/null +++ b/spec/factories/users/project_user_callouts.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :project_callout, class: 'Users::ProjectCallout' do + feature_name { :awaiting_members_banner } + + user + project + end +end diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js index 2aae91f8a39..75bd5df8cbf 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper.js @@ -6,6 +6,20 @@ const vNodeContainsText = (vnode, text) => (vnode.text && vnode.text.includes(text)) || (vnode.children && vnode.children.filter((child) => vNodeContainsText(child, text)).length); +/** + * Create a VTU wrapper from an element. + * + * If a Vue instance manages the element, the wrapper is created + * with that Vue instance. + * + * @param {HTMLElement} element + * @param {Object} options + * @returns VTU wrapper + */ +const createWrapperFromElement = (element, options) => + // eslint-disable-next-line no-underscore-dangle + createWrapper(element.__vue__ || element, options || {}); + /** * Determines whether a `shallowMount` Wrapper contains text * within one of it's slots. This will also work on Wrappers @@ -85,8 +99,7 @@ export const extendedWrapper = (wrapper) => { if (!elements.length) { return new ErrorWrapper(query); } - - return createWrapper(elements[0], this.options || {}); + return createWrapperFromElement(elements[0], this.options); }, }, }; @@ -104,7 +117,7 @@ export const extendedWrapper = (wrapper) => { ); const wrappers = elements.map((element) => { - const elementWrapper = createWrapper(element, this.options || {}); + const elementWrapper = createWrapperFromElement(element, this.options); elementWrapper.selector = text; return elementWrapper; diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js index 3bb228f94b8..ae180c3b49d 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js @@ -6,6 +6,7 @@ import { WrapperArray as VTUWrapperArray, ErrorWrapper as VTUErrorWrapper, } from '@vue/test-utils'; +import Vue from 'vue'; import { extendedWrapper, shallowMountExtended, @@ -139,9 +140,12 @@ describe('Vue test utils helpers', () => { const text = 'foo bar'; const options = { selector: 'div' }; const mockDiv = document.createElement('div'); + const mockVm = new Vue({ render: (h) => h('div') }).$mount(); let wrapper; beforeEach(() => { + jest.spyOn(vtu, 'createWrapper'); + wrapper = extendedWrapper( shallowMount({ template: `
foo bar
`, @@ -164,7 +168,6 @@ describe('Vue test utils helpers', () => { describe('when element is found', () => { beforeEach(() => { jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]); - jest.spyOn(vtu, 'createWrapper'); }); it('returns a VTU wrapper', () => { @@ -172,14 +175,27 @@ describe('Vue test utils helpers', () => { expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeUndefined(); }); }); + describe('when a Vue instance element is found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockVm.$el]); + }); + + it('returns a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeInstanceOf(Vue); + }); + }); describe('when multiple elements are found', () => { beforeEach(() => { const mockSpan = document.createElement('span'); jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]); - jest.spyOn(vtu, 'createWrapper'); }); it('returns the first element as a VTU wrapper', () => { @@ -187,6 +203,24 @@ describe('Vue test utils helpers', () => { expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeUndefined(); + }); + }); + + describe('when multiple Vue instances are found', () => { + beforeEach(() => { + const mockVm2 = new Vue({ render: (h) => h('span') }).$mount(); + jest + .spyOn(testingLibrary, expectedQuery) + .mockImplementation(() => [mockVm.$el, mockVm2.$el]); + }); + + it('returns the first element as a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeInstanceOf(Vue); }); }); @@ -211,12 +245,17 @@ describe('Vue test utils helpers', () => { ${'findAllByAltText'} | ${'queryAllByAltText'} `('$findMethod', ({ findMethod, expectedQuery }) => { const text = 'foo bar'; - const options = { selector: 'div' }; + const options = { selector: 'li' }; const mockElements = [ document.createElement('li'), document.createElement('li'), document.createElement('li'), ]; + const mockVms = [ + new Vue({ render: (h) => h('li') }).$mount(), + new Vue({ render: (h) => h('li') }).$mount(), + new Vue({ render: (h) => h('li') }).$mount(), + ]; let wrapper; beforeEach(() => { @@ -245,9 +284,13 @@ describe('Vue test utils helpers', () => { ); }); - describe('when elements are found', () => { + describe.each` + case | mockResult | isVueInstance + ${'HTMLElements'} | ${mockElements} | ${false} + ${'Vue instance elements'} | ${mockVms} | ${true} + `('when $case are found', ({ mockResult, isVueInstance }) => { beforeEach(() => { - jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements); + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockResult); }); it('returns a VTU wrapper array', () => { @@ -257,7 +300,9 @@ describe('Vue test utils helpers', () => { expect( result.wrappers.every( (resultWrapper) => - resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options, + resultWrapper instanceof VTUWrapper && + resultWrapper.vm instanceof Vue === isVueInstance && + resultWrapper.options === wrapper.options, ), ).toBe(true); expect(result.length).toBe(3); diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js index f5b56396cf1..cc679a52b34 100644 --- a/spec/frontend/runner/components/runner_bulk_delete_spec.js +++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import { GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -17,8 +18,8 @@ describe('RunnerBulkDelete', () => { let mockState; let mockCheckedRunnerIds; - const findClearBtn = () => wrapper.findByTestId('clear-btn'); - const findDeleteBtn = () => wrapper.findByTestId('delete-btn'); + const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection')); + const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected')); const createComponent = () => { const { cacheConfig, localMutations } = mockState; diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a324fd70424..4ffde842819 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -627,6 +627,7 @@ project: - security_trainings - vulnerability_reads - build_artifacts_size_refresh +- project_callouts award_emoji: - awardable - user diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index afb321c0777..2920278679f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -147,6 +147,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:build_trace_chunks).through(:builds).dependent(:restrict_with_error) } it { is_expected.to have_many(:secure_files).class_name('Ci::SecureFile').dependent(:restrict_with_error) } it { is_expected.to have_one(:build_artifacts_size_refresh).class_name('Projects::BuildArtifactsSizeRefresh') } + it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout').with_foreign_key(:project_id) } # GitLab Pages it { is_expected.to have_many(:pages_domains) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 12029b4151d..01b6b36db77 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -137,6 +137,7 @@ RSpec.describe User do it { is_expected.to have_many(:callouts).class_name('Users::Callout') } it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') } it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') } + it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout') } describe '#user_detail' do it 'does not persist `user_detail` by default' do @@ -6671,6 +6672,40 @@ RSpec.describe User do end end + describe '#dismissed_callout_for_project?' do + let_it_be(:user, refind: true) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + context 'when no callout dismissal record exists' do + it 'returns false when no ignore_dismissal_earlier_than provided' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project)).to eq false + end + end + + context 'when dismissed callout exists' do + before_all do + create(:project_callout, + user: user, + project_id: project.id, + feature_name: feature_name, + dismissed_at: 4.months.ago) + end + + it 'returns true when no ignore_dismissal_earlier_than provided' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project)).to eq true + end + + it 'returns true when ignore_dismissal_earlier_than is earlier than dismissed_at' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project, ignore_dismissal_earlier_than: 6.months.ago)).to eq true + end + + it 'returns false when ignore_dismissal_earlier_than is later than dismissed_at' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project, ignore_dismissal_earlier_than: 3.months.ago)).to eq false + end + end + end + describe '#find_or_initialize_group_callout' do let_it_be(:user, refind: true) { create(:user) } let_it_be(:group) { create(:group) } @@ -6715,6 +6750,50 @@ RSpec.describe User do end end + describe '#find_or_initialize_project_callout' do + let_it_be(:user, refind: true) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + subject(:callout_with_source) do + user.find_or_initialize_project_callout(feature_name, project.id) + end + + context 'when callout exists' do + let!(:callout) do + create(:project_callout, user: user, feature_name: feature_name, project_id: project.id) + end + + it 'returns existing callout' do + expect(callout_with_source).to eq(callout) + end + end + + context 'when callout does not exist' do + context 'when feature name is valid' do + it 'initializes a new callout' do + expect(callout_with_source).to be_a_new(Users::ProjectCallout) + end + + it 'is valid' do + expect(callout_with_source).to be_valid + end + end + + context 'when feature name is not valid' do + let(:feature_name) { 'notvalid' } + + it 'initializes a new callout' do + expect(callout_with_source).to be_a_new(Users::ProjectCallout) + end + + it 'is not valid' do + expect(callout_with_source).not_to be_valid + end + end + end + end + describe '#hook_attrs' do let(:user) { create(:user) } let(:user_attributes) do diff --git a/spec/models/users/project_callout_spec.rb b/spec/models/users/project_callout_spec.rb new file mode 100644 index 00000000000..214568b4de3 --- /dev/null +++ b/spec/models/users/project_callout_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::ProjectCallout do + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create_default(:project) } + let_it_be(:callout) { create(:project_callout) } + + it_behaves_like 'having unique enum values' + + describe 'relationships' do + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:feature_name) } + it { + is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id, :project_id).ignoring_case_sensitivity + } + end +end diff --git a/spec/requests/users/project_callouts_spec.rb b/spec/requests/users/project_callouts_spec.rb new file mode 100644 index 00000000000..98c00fef052 --- /dev/null +++ b/spec/requests/users/project_callouts_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project callouts' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + before do + sign_in(user) + end + + describe 'POST /-/users/project_callouts' do + let(:params) { { feature_name: feature_name, project_id: project.id } } + + subject { post project_callouts_path, params: params, headers: { 'ACCEPT' => 'application/json' } } + + context 'with valid feature name and project' do + let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + context 'when callout entry does not exist' do + it 'creates a callout entry with dismissed state' do + expect { subject }.to change { Users::ProjectCallout.count }.by(1) + end + + it 'returns success' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when callout entry already exists' do + let!(:callout) do + create(:project_callout, + feature_name: Users::ProjectCallout.feature_names.each_key.first, + user: user, + project: project) + end + + it 'returns success', :aggregate_failures do + expect { subject }.not_to change { Users::ProjectCallout.count } + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with invalid feature name' do + let(:feature_name) { 'bogus_feature_name' } + + it 'returns bad request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb index 149a384d31e..fe815aa6f1e 100644 --- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb +++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb @@ -65,13 +65,19 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do ## Strong + This example doesn't have an extension after the `example` keyword, so its + `source_specification` will be `commonmark`. + ```````````````````````````````` example __bold__ .

bold

```````````````````````````````` - ```````````````````````````````` example strong + This example has an extension after the `example` keyword, so its + `source_specification` will be `github`. + + ```````````````````````````````` example some_extension_name __bold with more text__ .

bold with more text

@@ -132,6 +138,10 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do ## Strong but with HTML + This example has the `gitlab` keyword after the `example` keyword, so its + `source_specification` will be `gitlab`. + + ```````````````````````````````` example gitlab strong bold diff --git a/spec/services/users/dismiss_project_callout_service_spec.rb b/spec/services/users/dismiss_project_callout_service_spec.rb new file mode 100644 index 00000000000..73e50a4c37d --- /dev/null +++ b/spec/services/users/dismiss_project_callout_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::DismissProjectCalloutService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:params) { { feature_name: feature_name, project_id: project.id } } + let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + subject(:execute) do + described_class.new( + container: nil, current_user: user, params: params + ).execute + end + + it_behaves_like 'dismissing user callout', Users::ProjectCallout + + it 'sets the project_id' do + expect(execute.project_id).to eq(project.id) + end + end +end -- cgit v1.2.3