Commit 6d7569d3 authored by Peter Hegman's avatar Peter Hegman

Merge branch '26732-update-qa-tests' into 'master'

Update New Project page dropdown and QA tests

See merge request gitlab-org/gitlab!71085
parents 0ea3c377 8e79c244
...@@ -14,6 +14,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; ...@@ -14,6 +14,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql'; import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
...@@ -41,15 +42,28 @@ export default { ...@@ -41,15 +42,28 @@ export default {
debounce: DEBOUNCE_DELAY, debounce: DEBOUNCE_DELAY,
}, },
}, },
inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel'], inject: [
'namespaceFullPath',
'namespaceId',
'rootUrl',
'trackLabel',
'userNamespaceFullPath',
'userNamespaceId',
],
data() { data() {
return { return {
currentUser: {}, currentUser: {},
groupToFilterBy: undefined,
search: '', search: '',
selectedNamespace: { selectedNamespace: this.namespaceId
id: this.namespaceId, ? {
fullPath: this.namespaceFullPath, id: this.namespaceId,
}, fullPath: this.namespaceFullPath,
}
: {
id: this.userNamespaceId,
fullPath: this.userNamespaceFullPath,
},
}; };
}, },
computed: { computed: {
...@@ -59,21 +73,43 @@ export default { ...@@ -59,21 +73,43 @@ export default {
userNamespace() { userNamespace() {
return this.currentUser.namespace || {}; return this.currentUser.namespace || {};
}, },
filteredGroups() {
return this.groupToFilterBy
? this.userGroups.filter((group) =>
group.fullPath.startsWith(this.groupToFilterBy.fullPath),
)
: this.userGroups;
},
hasGroupMatches() { hasGroupMatches() {
return this.userGroups.length; return this.filteredGroups.length;
}, },
hasNamespaceMatches() { hasNamespaceMatches() {
return this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()); return (
this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) &&
!this.groupToFilterBy
);
}, },
hasNoMatches() { hasNoMatches() {
return !this.hasGroupMatches && !this.hasNamespaceMatches; return !this.hasGroupMatches && !this.hasNamespaceMatches;
}, },
}, },
created() {
eventHub.$on('select-template', this.handleSelectTemplate);
},
beforeDestroy() {
eventHub.$off('select-template', this.handleSelectTemplate);
},
methods: { methods: {
focusInput() { focusInput() {
this.$refs.search.focusInput(); this.$refs.search.focusInput();
}, },
handleClick({ id, fullPath }) { handleSelectTemplate(groupId) {
this.groupToFilterBy = this.userGroups.find(
(group) => getIdFromGraphQLId(group.id) === groupId,
);
this.setNamespace(this.groupToFilterBy);
},
setNamespace({ id, fullPath }) {
this.selectedNamespace = { this.selectedNamespace = {
id: getIdFromGraphQLId(id), id: getIdFromGraphQLId(id),
fullPath, fullPath,
...@@ -84,28 +120,35 @@ export default { ...@@ -84,28 +120,35 @@ export default {
</script> </script>
<template> <template>
<gl-button-group class="gl-w-full"> <gl-button-group class="input-lg">
<gl-button label>{{ rootUrl }}</gl-button> <gl-button class="gl-text-truncate" label :title="rootUrl">{{ rootUrl }}</gl-button>
<gl-dropdown <gl-dropdown
class="gl-w-full"
:text="selectedNamespace.fullPath" :text="selectedNamespace.fullPath"
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!" toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown" data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })" @show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@shown="focusInput" @shown="focusInput"
> >
<gl-search-box-by-type ref="search" v-model.trim="search" /> <gl-search-box-by-type
ref="search"
v-model.trim="search"
data-qa-selector="select_namespace_dropdown_search_field"
/>
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" /> <gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else> <template v-else>
<template v-if="hasGroupMatches"> <template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)"> <gl-dropdown-item
v-for="group of filteredGroups"
:key="group.id"
@click="setNamespace(group)"
>
{{ group.fullPath }} {{ group.fullPath }}
</gl-dropdown-item> </gl-dropdown-item>
</template> </template>
<template v-if="hasNamespaceMatches"> <template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)"> <gl-dropdown-item @click="setNamespace(userNamespace)">
{{ userNamespace.fullPath }} {{ userNamespace.fullPath }}
</gl-dropdown-item> </gl-dropdown-item>
</template> </template>
......
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
...@@ -39,27 +39,32 @@ function initNewProjectCreation() { ...@@ -39,27 +39,32 @@ function initNewProjectCreation() {
} }
function initNewProjectUrlSelect() { function initNewProjectUrlSelect() {
const el = document.querySelector('.js-vue-new-project-url-select'); const elements = document.querySelectorAll('.js-vue-new-project-url-select');
if (!el) { if (!elements.length) {
return undefined; return;
} }
Vue.use(VueApollo); Vue.use(VueApollo);
return new Vue({ elements.forEach(
el, (el) =>
apolloProvider: new VueApollo({ new Vue({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), el,
}), apolloProvider: new VueApollo({
provide: { defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
namespaceFullPath: el.dataset.namespaceFullPath, }),
namespaceId: el.dataset.namespaceId, provide: {
rootUrl: el.dataset.rootUrl, namespaceFullPath: el.dataset.namespaceFullPath,
trackLabel: el.dataset.trackLabel, namespaceId: el.dataset.namespaceId,
}, rootUrl: el.dataset.rootUrl,
render: (createElement) => createElement(NewProjectUrlSelect), trackLabel: el.dataset.trackLabel,
}); userNamespaceFullPath: el.dataset.userNamespaceFullPath,
userNamespaceId: el.dataset.userNamespaceId,
},
render: (createElement) => createElement(NewProjectUrlSelect),
}),
);
} }
initProjectVisibilitySelector(); initProjectVisibilitySelector();
......
...@@ -16,7 +16,12 @@ ...@@ -16,7 +16,12 @@
- if current_user.can_select_namespace? - if current_user.can_select_namespace?
- namespace_id = namespace_id_from(params) - namespace_id = namespace_id_from(params)
- if Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml) - if Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
.js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path, namespace_id: namespace_id, root_url: root_url, track_label: track_label } } .js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path,
namespace_id: namespace_id,
root_url: root_url,
track_label: track_label,
user_namespace_full_path: current_user.namespace.full_path,
user_namespace_id: current_user.namespace.id } }
- else - else
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url } .input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text .input-group-text
......
import $ from 'jquery'; import $ from 'jquery';
import { Rails } from '~/lib/utils/rails_ujs'; import { Rails } from '~/lib/utils/rails_ujs';
import eventHub from '~/pages/projects/new/event_hub';
import projectNew from '~/projects/project_new'; import projectNew from '~/projects/project_new';
const bindEvents = () => { const bindEvents = () => {
...@@ -30,7 +31,7 @@ const bindEvents = () => { ...@@ -30,7 +31,7 @@ const bindEvents = () => {
function hideNonRootParentPathOptions() { function hideNonRootParentPathOptions() {
const rootParent = `/${ const rootParent = `/${
$namespaceSelect.find('option:selected').data('show-path').split('/')[1] $namespaceSelect.find('option:selected').data('show-path')?.split('/')[1]
}`; }`;
$namespaceSelect $namespaceSelect
...@@ -56,6 +57,8 @@ const bindEvents = () => { ...@@ -56,6 +57,8 @@ const bindEvents = () => {
const templateName = $(this).data('template-name'); const templateName = $(this).data('template-name');
if (subgroupId) { if (subgroupId) {
eventHub.$emit('select-template', groupId);
$subgroupWithTemplatesIdInput.val(subgroupId); $subgroupWithTemplatesIdInput.val(subgroupId);
$namespaceSelect.val(groupId).trigger('change'); $namespaceSelect.val(groupId).trigger('change');
......
...@@ -5,10 +5,9 @@ module QA ...@@ -5,10 +5,9 @@ module QA
module Project module Project
module Import module Import
class RepoByURL < Page::Base class RepoByURL < Page::Base
include Page::Component::Select2 view 'app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue' do
view 'app/views/projects/_new_project_fields.html.haml' do
element :select_namespace_dropdown element :select_namespace_dropdown
element :select_namespace_dropdown_search_field
end end
def import!(gitlab_repo_path, name) def import!(gitlab_repo_path, name)
...@@ -33,8 +32,15 @@ module QA ...@@ -33,8 +32,15 @@ module QA
end end
def choose_test_namespace def choose_test_namespace
find('.js-select-namespace').click choose_namespace(Runtime::Namespace.path)
search_and_select(Runtime::Namespace.path) end
def choose_namespace(namespace)
retry_on_exception do
click_element :select_namespace_dropdown
fill_element :select_namespace_dropdown_search_field, namespace
click_button namespace
end
end end
def click_create_button def click_create_button
......
...@@ -5,7 +5,6 @@ module QA ...@@ -5,7 +5,6 @@ module QA
module Project module Project
class New < Page::Base class New < Page::Base
include Page::Component::Project::Templates include Page::Component::Project::Templates
include Page::Component::Select2
include Page::Component::VisibilitySetting include Page::Component::VisibilitySetting
include Layout::Flash include Layout::Flash
...@@ -14,7 +13,6 @@ module QA ...@@ -14,7 +13,6 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox element :initialize_with_readme_checkbox
element :project_namespace_select
element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
...@@ -28,6 +26,11 @@ module QA ...@@ -28,6 +26,11 @@ module QA
element :template_option_row element :template_option_row
end end
view 'app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue' do
element :select_namespace_dropdown
element :select_namespace_dropdown_search_field
end
view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do
element :panel_link element :panel_link
end end
...@@ -46,8 +49,9 @@ module QA ...@@ -46,8 +49,9 @@ module QA
def choose_namespace(namespace) def choose_namespace(namespace)
retry_on_exception do retry_on_exception do
click_element :project_namespace_select unless dropdown_open? click_element :select_namespace_dropdown
search_and_select(namespace) fill_element :select_namespace_dropdown_search_field, namespace
click_button namespace
end end
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module QA module QA
RSpec.describe 'Manage', :smoke do RSpec.describe 'Manage', :smoke do
describe 'Project' do describe 'Project', :requires_admin do
shared_examples 'successful project creation' do shared_examples 'successful project creation' do
it 'creates a new project' do it 'creates a new project' do
Page::Project::Show.perform do |project| Page::Project::Show.perform do |project|
...@@ -17,6 +17,7 @@ module QA ...@@ -17,6 +17,7 @@ module QA
end end
before do before do
Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
Flow::Login.sign_in Flow::Login.sign_in
project project
end end
......
...@@ -20,6 +20,7 @@ module QA ...@@ -20,6 +20,7 @@ module QA
end end
before do before do
Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
sign_in sign_in
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module QA module QA
RSpec.describe 'Manage' do RSpec.describe 'Manage' do
describe 'Project templates' do describe 'Project templates', :requires_admin do
include Support::API include Support::API
before(:all) do before(:all) do
...@@ -36,6 +36,10 @@ module QA ...@@ -36,6 +36,10 @@ module QA
end end
end end
before do
Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
end
context 'built-in', :requires_admin do context 'built-in', :requires_admin do
before do before do
Flow::Login.sign_in_as_admin Flow::Login.sign_in_as_admin
......
...@@ -10,6 +10,7 @@ import VueApollo from 'vue-apollo'; ...@@ -10,6 +10,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/pages/projects/new/event_hub';
import NewProjectUrlSelect from '~/pages/projects/new/components/new_project_url_select.vue'; import NewProjectUrlSelect from '~/pages/projects/new/components/new_project_url_select.vue';
import searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; import searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
...@@ -28,6 +29,10 @@ describe('NewProjectUrlSelect component', () => { ...@@ -28,6 +29,10 @@ describe('NewProjectUrlSelect component', () => {
id: 'gid://gitlab/Group/28', id: 'gid://gitlab/Group/28',
fullPath: 'h5bp', fullPath: 'h5bp',
}, },
{
id: 'gid://gitlab/Group/30',
fullPath: 'h5bp/subgroup',
},
], ],
}, },
namespace: { namespace: {
...@@ -40,14 +45,21 @@ describe('NewProjectUrlSelect component', () => { ...@@ -40,14 +45,21 @@ describe('NewProjectUrlSelect component', () => {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const provide = { const defaultProvide = {
namespaceFullPath: 'h5bp', namespaceFullPath: 'h5bp',
namespaceId: '28', namespaceId: '28',
rootUrl: 'https://gitlab.com/', rootUrl: 'https://gitlab.com/',
trackLabel: 'blank_project', trackLabel: 'blank_project',
userNamespaceFullPath: 'root',
userNamespaceId: '1',
}; };
const mountComponent = ({ search = '', queryResponse = data, mountFn = shallowMount } = {}) => { const mountComponent = ({
search = '',
queryResponse = data,
provide = defaultProvide,
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]]; const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]];
const apolloProvider = createMockApollo(requestHandlers); const apolloProvider = createMockApollo(requestHandlers);
...@@ -75,20 +87,42 @@ describe('NewProjectUrlSelect component', () => { ...@@ -75,20 +87,42 @@ describe('NewProjectUrlSelect component', () => {
it('renders the root url as a label', () => { it('renders the root url as a label', () => {
wrapper = mountComponent(); wrapper = mountComponent();
expect(findButtonLabel().text()).toBe(provide.rootUrl); expect(findButtonLabel().text()).toBe(defaultProvide.rootUrl);
expect(findButtonLabel().props('label')).toBe(true); expect(findButtonLabel().props('label')).toBe(true);
}); });
it('renders a dropdown with the initial namespace full path as the text', () => { describe('when namespaceId is provided', () => {
wrapper = mountComponent(); beforeEach(() => {
wrapper = mountComponent();
});
expect(findDropdown().props('text')).toBe(provide.namespaceFullPath); it('renders a dropdown with the given namespace full path as the text', () => {
expect(findDropdown().props('text')).toBe(defaultProvide.namespaceFullPath);
});
it('renders a dropdown with the given namespace id in the hidden input', () => {
expect(findHiddenInput().attributes('value')).toBe(defaultProvide.namespaceId);
});
}); });
it('renders a dropdown with the initial namespace id in the hidden input', () => { describe('when namespaceId is not provided', () => {
wrapper = mountComponent(); const provide = {
...defaultProvide,
namespaceFullPath: undefined,
namespaceId: undefined,
};
beforeEach(() => {
wrapper = mountComponent({ provide });
});
it("renders a dropdown with the user's namespace full path as the text", () => {
expect(findDropdown().props('text')).toBe(defaultProvide.userNamespaceFullPath);
});
expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId); it("renders a dropdown with the user's namespace id in the hidden input", () => {
expect(findHiddenInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
});
}); });
it('focuses on the input when the dropdown is opened', async () => { it('focuses on the input when the dropdown is opened', async () => {
...@@ -112,11 +146,39 @@ describe('NewProjectUrlSelect component', () => { ...@@ -112,11 +146,39 @@ describe('NewProjectUrlSelect component', () => {
const listItems = wrapper.findAll('li'); const listItems = wrapper.findAll('li');
expect(listItems).toHaveLength(6);
expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups'); expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath); expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath);
expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath); expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
expect(listItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('Users'); expect(listItems.at(3).text()).toBe(data.currentUser.groups.nodes[2].fullPath);
expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath); expect(listItems.at(4).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
});
describe('when selecting from a group template', () => {
const groupId = getIdFromGraphQLId(data.currentUser.groups.nodes[1].id);
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
eventHub.$emit('select-template', groupId);
});
it('filters the dropdown items to the selected group and children', async () => {
const listItems = wrapper.findAll('li');
expect(listItems).toHaveLength(3);
expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[2].fullPath);
});
it('sets the selection to the group', async () => {
expect(findDropdown().props('text')).toBe(data.currentUser.groups.nodes[1].fullPath);
});
}); });
it('renders `No matches found` when there are no matching dropdown items', async () => { it('renders `No matches found` when there are no matching dropdown items', async () => {
...@@ -164,7 +226,7 @@ describe('NewProjectUrlSelect component', () => { ...@@ -164,7 +226,7 @@ describe('NewProjectUrlSelect component', () => {
findDropdown().vm.$emit('show'); findDropdown().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', { expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
label: provide.trackLabel, label: defaultProvide.trackLabel,
property: 'project_path', property: 'project_path',
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment