diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-14 09:09:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-14 09:09:02 +0000 |
commit | 846a84f2e9d6149b00c63ccae2850421f6766bac (patch) | |
tree | a23327af14cdd705bdde5ae2ed1b8750ba04755e /spec | |
parent | 8e42824b115f56679b9c791570b27d6184fecad9 (diff) | |
download | gitlab-ce-846a84f2e9d6149b00c63ccae2850421f6766bac.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/factories/namespaces.rb | 30 | ||||
-rw-r--r-- | spec/frontend/design_management/utils/tracking_spec.js | 28 | ||||
-rw-r--r-- | spec/frontend/design_management_new/utils/tracking_spec.js | 28 | ||||
-rw-r--r-- | spec/frontend/ref/components/ref_selector_spec.js | 532 | ||||
-rw-r--r-- | spec/models/namespace/traversal_hierarchy_spec.rb | 63 | ||||
-rw-r--r-- | spec/support/shared_examples/namespaces/hierarchy_examples.rb | 21 |
6 files changed, 680 insertions, 22 deletions
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb index 09dbe16ef9e..f4d5848e878 100644 --- a/spec/factories/namespaces.rb +++ b/spec/factories/namespaces.rb @@ -29,5 +29,35 @@ FactoryBot.define do trait :with_root_storage_statistics do association :root_storage_statistics, factory: :namespace_root_storage_statistics end + + # Construct a hierarchy underneath the namespace. + # Each namespace will have `children` amount of children, + # and `depth` levels of descendants. + trait :with_hierarchy do + transient do + children { 4 } + depth { 4 } + end + + after(:create) do |namespace, evaluator| + def create_graph(parent: nil, children: 4, depth: 4) + return unless depth > 1 + + children.times do + factory_name = parent.model_name.singular + child = FactoryBot.create(factory_name, parent: parent) + create_graph(parent: child, children: children, depth: depth - 1) + end + + parent + end + + create_graph( + parent: namespace, + children: evaluator.children, + depth: evaluator.depth + ) + end + end end end diff --git a/spec/frontend/design_management/utils/tracking_spec.js b/spec/frontend/design_management/utils/tracking_spec.js index 9fa5eae55b3..0549fb44956 100644 --- a/spec/frontend/design_management/utils/tracking_spec.js +++ b/spec/frontend/design_management/utils/tracking_spec.js @@ -8,7 +8,7 @@ function getTrackingSpy(key) { describe('Tracking Events', () => { describe('trackDesignDetailView', () => { const eventKey = 'projects:issues:design'; - const eventName = 'design_viewed'; + const eventName = 'view_design'; it('trackDesignDetailView fires a tracking event when called', () => { const trackingSpy = getTrackingSpy(eventKey); @@ -20,11 +20,14 @@ describe('Tracking Events', () => { eventName, expect.objectContaining({ label: eventName, - value: { - 'internal-object-refrerer': '', - 'design-collection-owner': '', - 'design-version-number': 1, - 'design-is-current-version': false, + context: { + schema: expect.any(String), + data: { + 'design-version-number': 1, + 'design-is-current-version': false, + 'internal-object-referrer': '', + 'design-collection-owner': '', + }, }, }), ); @@ -40,11 +43,14 @@ describe('Tracking Events', () => { eventName, expect.objectContaining({ label: eventName, - value: { - 'internal-object-refrerer': 'from-a-test', - 'design-collection-owner': 'test', - 'design-version-number': 100, - 'design-is-current-version': true, + context: { + schema: expect.any(String), + data: { + 'design-version-number': 100, + 'design-is-current-version': true, + 'internal-object-referrer': 'from-a-test', + 'design-collection-owner': 'test', + }, }, }), ); diff --git a/spec/frontend/design_management_new/utils/tracking_spec.js b/spec/frontend/design_management_new/utils/tracking_spec.js index 073cc0df255..ac7267642cb 100644 --- a/spec/frontend/design_management_new/utils/tracking_spec.js +++ b/spec/frontend/design_management_new/utils/tracking_spec.js @@ -8,7 +8,7 @@ function getTrackingSpy(key) { describe('Tracking Events', () => { describe('trackDesignDetailView', () => { const eventKey = 'projects:issues:design'; - const eventName = 'design_viewed'; + const eventName = 'view_design'; it('trackDesignDetailView fires a tracking event when called', () => { const trackingSpy = getTrackingSpy(eventKey); @@ -20,11 +20,14 @@ describe('Tracking Events', () => { eventName, expect.objectContaining({ label: eventName, - value: { - 'internal-object-refrerer': '', - 'design-collection-owner': '', - 'design-version-number': 1, - 'design-is-current-version': false, + context: { + schema: expect.any(String), + data: { + 'design-version-number': 1, + 'design-is-current-version': false, + 'internal-object-referrer': '', + 'design-collection-owner': '', + }, }, }), ); @@ -40,11 +43,14 @@ describe('Tracking Events', () => { eventName, expect.objectContaining({ label: eventName, - value: { - 'internal-object-refrerer': 'from-a-test', - 'design-collection-owner': 'test', - 'design-version-number': 100, - 'design-is-current-version': true, + context: { + schema: expect.any(String), + data: { + 'design-version-number': 100, + 'design-is-current-version': true, + 'internal-object-referrer': 'from-a-test', + 'design-collection-owner': 'test', + }, }, }), ); diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js new file mode 100644 index 00000000000..2688e4b3428 --- /dev/null +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -0,0 +1,532 @@ +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { GlLoadingIcon, GlSearchBoxByType, GlNewDropdownItem, GlIcon } from '@gitlab/ui'; +import { trimText } from 'helpers/text_helper'; +import { sprintf } from '~/locale'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants'; +import createStore from '~/ref/stores/'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Ref selector component', () => { + const fixtures = { + branches: getJSONFixture('api/branches/branches.json'), + tags: getJSONFixture('api/tags/tags.json'), + commit: getJSONFixture('api/commits/commit.json'), + }; + + const projectId = '8'; + + let wrapper; + let branchesApiCallSpy; + let tagsApiCallSpy; + let commitApiCallSpy; + + const createComponent = () => { + wrapper = mount(RefSelector, { + propsData: { + projectId, + value: '', + }, + listeners: { + // simulate a parent component v-model binding + input: selectedRef => { + wrapper.setProps({ value: selectedRef }); + }, + }, + stubs: { + GlSearchBoxByType: true, + }, + localVue, + store: createStore(), + }); + }; + + beforeEach(() => { + const mock = new MockAdapter(axios); + gon.api_version = 'v4'; + + branchesApiCallSpy = jest + .fn() + .mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]); + tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]); + commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]); + + mock + .onGet(`/api/v4/projects/${projectId}/repository/branches`) + .reply(config => branchesApiCallSpy(config)); + mock + .onGet(`/api/v4/projects/${projectId}/repository/tags`) + .reply(config => tagsApiCallSpy(config)); + mock + .onGet(new RegExp(`/api/v4/projects/${projectId}/repository/commits/.*`)) + .reply(config => commitApiCallSpy(config)); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + // + // Finders + // + const findButtonContent = () => wrapper.find('[data-testid="button-content"]'); + + const findNoResults = () => wrapper.find('[data-testid="no-results"]'); + + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + + const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]'); + const findBranchDropdownItems = () => findBranchesSection().findAll(GlNewDropdownItem); + const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0); + + const findTagsSection = () => wrapper.find('[data-testid="tags-section"]'); + const findTagDropdownItems = () => findTagsSection().findAll(GlNewDropdownItem); + const findFirstTagDropdownItem = () => findTagDropdownItems().at(0); + + const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]'); + const findCommitDropdownItems = () => findCommitsSection().findAll(GlNewDropdownItem); + const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0); + + // + // Expecters + // + const branchesSectionContainsErrorMessage = () => { + const branchesSection = findBranchesSection(); + + return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage); + }; + + const tagsSectionContainsErrorMessage = () => { + const tagsSection = findTagsSection(); + + return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage); + }; + + const commitsSectionContainsErrorMessage = () => { + const commitsSection = findCommitsSection(); + + return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage); + }; + + // + // Convenience methods + // + const updateQuery = newQuery => { + wrapper.find(GlSearchBoxByType).vm.$emit('input', newQuery); + }; + + const selectFirstBranch = () => { + findFirstBranchDropdownItem().vm.$emit('click'); + }; + + const selectFirstTag = () => { + findFirstTagDropdownItem().vm.$emit('click'); + }; + + const selectFirstCommit = () => { + findFirstCommitDropdownItem().vm.$emit('click'); + }; + + const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) => + axios.waitForAll().then(() => { + if (andClearMocks) { + branchesApiCallSpy.mockClear(); + tagsApiCallSpy.mockClear(); + commitApiCallSpy.mockClear(); + } + }); + + describe('initialization behavior', () => { + beforeEach(createComponent); + + it('initializes the dropdown with branches and tags when mounted', () => { + return waitForRequests().then(() => { + expect(branchesApiCallSpy).toHaveBeenCalledTimes(1); + expect(tagsApiCallSpy).toHaveBeenCalledTimes(1); + expect(commitApiCallSpy).not.toHaveBeenCalled(); + }); + }); + + it('shows a spinner while network requests are in progress', () => { + expect(findLoadingIcon().exists()).toBe(true); + + return waitForRequests().then(() => { + expect(findLoadingIcon().exists()).toBe(false); + }); + }); + }); + + describe('post-initialization behavior', () => { + describe('when the search query is updated', () => { + beforeEach(() => { + createComponent(); + + return waitForRequests({ andClearMocks: true }); + }); + + it('requeries the endpoints when the search query is updated', () => { + updateQuery('v1.2.3'); + + return waitForRequests().then(() => { + expect(branchesApiCallSpy).toHaveBeenCalledTimes(1); + expect(tagsApiCallSpy).toHaveBeenCalledTimes(1); + }); + }); + + it("does not make a call to the commit endpoint if the query doesn't look like a SHA", () => { + updateQuery('not a sha'); + + return waitForRequests().then(() => { + expect(commitApiCallSpy).not.toHaveBeenCalled(); + }); + }); + + it('searches for a commit if the query could potentially be a SHA', () => { + updateQuery('abcdef'); + + return waitForRequests().then(() => { + expect(commitApiCallSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('when no results are found', () => { + beforeEach(() => { + branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + commitApiCallSpy = jest.fn().mockReturnValue([404]); + + createComponent(); + + return waitForRequests(); + }); + + describe('when the search query is empty', () => { + it('renders a "no results" message', () => { + expect(findNoResults().text()).toBe(DEFAULT_I18N.noResults); + }); + }); + + describe('when the search query is not empty', () => { + const query = 'hello'; + + beforeEach(() => { + updateQuery(query); + + return waitForRequests(); + }); + + it('renders a "no results" message that includes the search query', () => { + expect(findNoResults().text()).toBe(sprintf(DEFAULT_I18N.noResultsWithQuery, { query })); + }); + }); + }); + + describe('branches', () => { + describe('when the branches search returns results', () => { + beforeEach(() => { + createComponent(); + + return waitForRequests(); + }); + + it('renders the branches section in the dropdown', () => { + expect(findBranchesSection().exists()).toBe(true); + }); + + it('renders the "Branches" heading with a total number indicator', () => { + expect( + findBranchesSection() + .find('[data-testid="section-header"]') + .text(), + ).toBe('Branches 123'); + }); + + it("does not render an error message in the branches section's body", () => { + expect(branchesSectionContainsErrorMessage()).toBe(false); + }); + + it('renders each non-default branch as a selectable item', () => { + const dropdownItems = findBranchDropdownItems(); + + fixtures.branches.forEach((b, i) => { + if (!b.default) { + expect(dropdownItems.at(i).text()).toBe(b.name); + } + }); + }); + + it('renders the default branch as a selectable item with a "default" badge', () => { + const dropdownItems = findBranchDropdownItems(); + + const defaultBranch = fixtures.branches.find(b => b.default); + const defaultBranchIndex = fixtures.branches.indexOf(defaultBranch); + + expect(trimText(dropdownItems.at(defaultBranchIndex).text())).toBe( + `${defaultBranch.name} default`, + ); + }); + }); + + describe('when the branches search returns no results', () => { + beforeEach(() => { + branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + + createComponent(); + + return waitForRequests(); + }); + + it('does not render the branches section in the dropdown', () => { + expect(findBranchesSection().exists()).toBe(false); + }); + }); + + describe('when the branches search returns an error', () => { + beforeEach(() => { + branchesApiCallSpy = jest.fn().mockReturnValue([500]); + + createComponent(); + + return waitForRequests(); + }); + + it('renders the branches section in the dropdown', () => { + expect(findBranchesSection().exists()).toBe(true); + }); + + it("renders an error message in the branches section's body", () => { + expect(branchesSectionContainsErrorMessage()).toBe(true); + }); + }); + }); + + describe('tags', () => { + describe('when the tags search returns results', () => { + beforeEach(() => { + createComponent(); + + return waitForRequests(); + }); + + it('renders the tags section in the dropdown', () => { + expect(findTagsSection().exists()).toBe(true); + }); + + it('renders the "Tags" heading with a total number indicator', () => { + expect( + findTagsSection() + .find('[data-testid="section-header"]') + .text(), + ).toBe('Tags 456'); + }); + + it("does not render an error message in the tags section's body", () => { + expect(tagsSectionContainsErrorMessage()).toBe(false); + }); + + it('renders each tag as a selectable item', () => { + const dropdownItems = findTagDropdownItems(); + + fixtures.tags.forEach((t, i) => { + expect(dropdownItems.at(i).text()).toBe(t.name); + }); + }); + }); + + describe('when the tags search returns no results', () => { + beforeEach(() => { + tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + + createComponent(); + + return waitForRequests(); + }); + + it('does not render the tags section in the dropdown', () => { + expect(findTagsSection().exists()).toBe(false); + }); + }); + + describe('when the tags search returns an error', () => { + beforeEach(() => { + tagsApiCallSpy = jest.fn().mockReturnValue([500]); + + createComponent(); + + return waitForRequests(); + }); + + it('renders the tags section in the dropdown', () => { + expect(findTagsSection().exists()).toBe(true); + }); + + it("renders an error message in the tags section's body", () => { + expect(tagsSectionContainsErrorMessage()).toBe(true); + }); + }); + }); + + describe('commits', () => { + describe('when the commit search returns results', () => { + beforeEach(() => { + createComponent(); + + updateQuery('abcd1234'); + + return waitForRequests(); + }); + + it('renders the commit section in the dropdown', () => { + expect(findCommitsSection().exists()).toBe(true); + }); + + it('renders the "Commits" heading with a total number indicator', () => { + expect( + findCommitsSection() + .find('[data-testid="section-header"]') + .text(), + ).toBe('Commits 1'); + }); + + it("does not render an error message in the comits section's body", () => { + expect(commitsSectionContainsErrorMessage()).toBe(false); + }); + + it('renders each commit as a selectable item with the short SHA and commit title', () => { + const dropdownItems = findCommitDropdownItems(); + + const { commit } = fixtures; + + expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`); + }); + }); + + describe('when the commit search returns no results (i.e. a 404)', () => { + beforeEach(() => { + commitApiCallSpy = jest.fn().mockReturnValue([404]); + + createComponent(); + + updateQuery('abcd1234'); + + return waitForRequests(); + }); + + it('does not render the commits section in the dropdown', () => { + expect(findCommitsSection().exists()).toBe(false); + }); + }); + + describe('when the commit search returns an error (other than a 404)', () => { + beforeEach(() => { + commitApiCallSpy = jest.fn().mockReturnValue([500]); + + createComponent(); + + updateQuery('abcd1234'); + + return waitForRequests(); + }); + + it('renders the commits section in the dropdown', () => { + expect(findCommitsSection().exists()).toBe(true); + }); + + it("renders an error message in the commits section's body", () => { + expect(commitsSectionContainsErrorMessage()).toBe(true); + }); + }); + }); + + describe('selection', () => { + beforeEach(() => { + createComponent(); + + updateQuery(fixtures.commit.short_id); + + return waitForRequests(); + }); + + it('renders a checkmark by the selected item', () => { + expect(findFirstBranchDropdownItem().find(GlIcon).element).toHaveClass( + 'gl-visibility-hidden', + ); + + selectFirstBranch(); + + return localVue.nextTick().then(() => { + expect(findFirstBranchDropdownItem().find(GlIcon).element).not.toHaveClass( + 'gl-visibility-hidden', + ); + }); + }); + + describe('when a branch is seleceted', () => { + it("displays the branch name in the dropdown's button", () => { + expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); + + selectFirstBranch(); + + return localVue.nextTick().then(() => { + expect(findButtonContent().text()).toBe(fixtures.branches[0].name); + }); + }); + + it("updates the v-model binding with the branch's name", () => { + expect(wrapper.vm.value).toEqual(''); + + selectFirstBranch(); + + expect(wrapper.vm.value).toEqual(fixtures.branches[0].name); + }); + }); + + describe('when a tag is seleceted', () => { + it("displays the tag name in the dropdown's button", () => { + expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); + + selectFirstTag(); + + return localVue.nextTick().then(() => { + expect(findButtonContent().text()).toBe(fixtures.tags[0].name); + }); + }); + + it("updates the v-model binding with the tag's name", () => { + expect(wrapper.vm.value).toEqual(''); + + selectFirstTag(); + + expect(wrapper.vm.value).toEqual(fixtures.tags[0].name); + }); + }); + + describe('when a commit is selected', () => { + it("displays the full SHA in the dropdown's button", () => { + expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); + + selectFirstCommit(); + + return localVue.nextTick().then(() => { + expect(findButtonContent().text()).toBe(fixtures.commit.id); + }); + }); + + it("updates the v-model binding with the commit's full SHA", () => { + expect(wrapper.vm.value).toEqual(''); + + selectFirstCommit(); + + expect(wrapper.vm.value).toEqual(fixtures.commit.id); + }); + }); + }); + }); +}); diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb new file mode 100644 index 00000000000..71b0e974106 --- /dev/null +++ b/spec/models/namespace/traversal_hierarchy_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Namespace::TraversalHierarchy, type: :model do + let_it_be(:root, reload: true) { create(:namespace, :with_hierarchy) } + + describe '.for_namespace' do + let(:hierarchy) { described_class.for_namespace(namespace) } + + context 'with root group' do + let(:namespace) { root } + + it { expect(hierarchy.root).to eq root } + end + + context 'with child group' do + let(:namespace) { root.children.first.children.first } + + it { expect(hierarchy.root).to eq root } + end + + context 'with group outside of hierarchy' do + let(:namespace) { create(:namespace) } + + it { expect(hierarchy.root).not_to eq root } + end + end + + describe '.new' do + let(:hierarchy) { described_class.new(namespace) } + + context 'with root group' do + let(:namespace) { root } + + it { expect(hierarchy.root).to eq root } + end + + context 'with child group' do + let(:namespace) { root.children.first } + + it { expect { hierarchy }.to raise_error(StandardError, 'Must specify a root node') } + end + end + + describe '#incorrect_traversal_ids' do + subject { described_class.new(root).incorrect_traversal_ids } + + it { is_expected.to match_array Namespace.all } + end + + describe '#sync_traversal_ids!' do + let(:hierarchy) { described_class.new(root) } + + before do + hierarchy.sync_traversal_ids! + root.reload + end + + it_behaves_like 'hierarchy with traversal_ids' + it { expect(hierarchy.incorrect_traversal_ids).to be_empty } + end +end diff --git a/spec/support/shared_examples/namespaces/hierarchy_examples.rb b/spec/support/shared_examples/namespaces/hierarchy_examples.rb new file mode 100644 index 00000000000..d5754f47be2 --- /dev/null +++ b/spec/support/shared_examples/namespaces/hierarchy_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'hierarchy with traversal_ids' do + # A convenient null node to represent the parent of root. + let(:null_node) { double(traversal_ids: []) } + + # Walk the tree to assert that the current_node's traversal_id is always + # present and equal to it's parent's traversal_ids plus it's own ID. + def validate_traversal_ids(current_node, parent = null_node) + expect(current_node.traversal_ids).to be_present + expect(current_node.traversal_ids).to eq parent.traversal_ids + [current_node.id] + + current_node.children.each do |child| + validate_traversal_ids(child, current_node) + end + end + + it 'will be valid' do + validate_traversal_ids(root) + end +end |