Commit 8e79c244 authored by Coung Ngo's avatar Coung Ngo Committed by Peter Hegman

Update new project page QA tests

Update QA tests now that the Project URL select2 dropdown has been
replaced by a GlDropdown Vue component behind the feature flag
`paginatable_namespace_drop_down_for_project_creation`

https://gitlab.com/gitlab-org/gitlab/-/issues/26732
parent 0c57db90
...@@ -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