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';
import Tracking from '~/tracking';
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 eventHub from '../event_hub';
export default {
components: {
......@@ -41,15 +42,28 @@ export default {
debounce: DEBOUNCE_DELAY,
},
},
inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel'],
inject: [
'namespaceFullPath',
'namespaceId',
'rootUrl',
'trackLabel',
'userNamespaceFullPath',
'userNamespaceId',
],
data() {
return {
currentUser: {},
groupToFilterBy: undefined,
search: '',
selectedNamespace: {
id: this.namespaceId,
fullPath: this.namespaceFullPath,
},
selectedNamespace: this.namespaceId
? {
id: this.namespaceId,
fullPath: this.namespaceFullPath,
}
: {
id: this.userNamespaceId,
fullPath: this.userNamespaceFullPath,
},
};
},
computed: {
......@@ -59,21 +73,43 @@ export default {
userNamespace() {
return this.currentUser.namespace || {};
},
filteredGroups() {
return this.groupToFilterBy
? this.userGroups.filter((group) =>
group.fullPath.startsWith(this.groupToFilterBy.fullPath),
)
: this.userGroups;
},
hasGroupMatches() {
return this.userGroups.length;
return this.filteredGroups.length;
},
hasNamespaceMatches() {
return this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase());
return (
this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) &&
!this.groupToFilterBy
);
},
hasNoMatches() {
return !this.hasGroupMatches && !this.hasNamespaceMatches;
},
},
created() {
eventHub.$on('select-template', this.handleSelectTemplate);
},
beforeDestroy() {
eventHub.$off('select-template', this.handleSelectTemplate);
},
methods: {
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 = {
id: getIdFromGraphQLId(id),
fullPath,
......@@ -84,28 +120,35 @@ export default {
</script>
<template>
<gl-button-group class="gl-w-full">
<gl-button label>{{ rootUrl }}</gl-button>
<gl-button-group class="input-lg">
<gl-button class="gl-text-truncate" label :title="rootUrl">{{ rootUrl }}</gl-button>
<gl-dropdown
class="gl-w-full"
: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"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@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" />
<template v-else>
<template v-if="hasGroupMatches">
<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 }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)">
<gl-dropdown-item @click="setNamespace(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
......
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
......@@ -39,27 +39,32 @@ function initNewProjectCreation() {
}
function initNewProjectUrlSelect() {
const el = document.querySelector('.js-vue-new-project-url-select');
const elements = document.querySelectorAll('.js-vue-new-project-url-select');
if (!el) {
return undefined;
if (!elements.length) {
return;
}
Vue.use(VueApollo);
return new Vue({
el,
apolloProvider: new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
}),
provide: {
namespaceFullPath: el.dataset.namespaceFullPath,
namespaceId: el.dataset.namespaceId,
rootUrl: el.dataset.rootUrl,
trackLabel: el.dataset.trackLabel,
},
render: (createElement) => createElement(NewProjectUrlSelect),
});
elements.forEach(
(el) =>
new Vue({
el,
apolloProvider: new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
}),
provide: {
namespaceFullPath: el.dataset.namespaceFullPath,
namespaceId: el.dataset.namespaceId,
rootUrl: el.dataset.rootUrl,
trackLabel: el.dataset.trackLabel,
userNamespaceFullPath: el.dataset.userNamespaceFullPath,
userNamespaceId: el.dataset.userNamespaceId,
},
render: (createElement) => createElement(NewProjectUrlSelect),
}),
);
}
initProjectVisibilitySelector();
......
......@@ -16,7 +16,12 @@
- if current_user.can_select_namespace?
- namespace_id = namespace_id_from(params)
- 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
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text
......
import $ from 'jquery';
import { Rails } from '~/lib/utils/rails_ujs';
import eventHub from '~/pages/projects/new/event_hub';
import projectNew from '~/projects/project_new';
const bindEvents = () => {
......@@ -30,7 +31,7 @@ const bindEvents = () => {
function hideNonRootParentPathOptions() {
const rootParent = `/${
$namespaceSelect.find('option:selected').data('show-path').split('/')[1]
$namespaceSelect.find('option:selected').data('show-path')?.split('/')[1]
}`;
$namespaceSelect
......@@ -56,6 +57,8 @@ const bindEvents = () => {
const templateName = $(this).data('template-name');
if (subgroupId) {
eventHub.$emit('select-template', groupId);
$subgroupWithTemplatesIdInput.val(subgroupId);
$namespaceSelect.val(groupId).trigger('change');
......
......@@ -5,10 +5,9 @@ module QA
module Project
module Import
class RepoByURL < Page::Base
include Page::Component::Select2
view 'app/views/projects/_new_project_fields.html.haml' do
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
def import!(gitlab_repo_path, name)
......@@ -33,8 +32,15 @@ module QA
end
def choose_test_namespace
find('.js-select-namespace').click
search_and_select(Runtime::Namespace.path)
choose_namespace(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
def click_create_button
......
......@@ -5,7 +5,6 @@ module QA
module Project
class New < Page::Base
include Page::Component::Project::Templates
include Page::Component::Select2
include Page::Component::VisibilitySetting
include Layout::Flash
......@@ -14,7 +13,6 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
element :project_namespace_select
element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
......@@ -28,6 +26,11 @@ module QA
element :template_option_row
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
element :panel_link
end
......@@ -46,8 +49,9 @@ module QA
def choose_namespace(namespace)
retry_on_exception do
click_element :project_namespace_select unless dropdown_open?
search_and_select(namespace)
click_element :select_namespace_dropdown
fill_element :select_namespace_dropdown_search_field, namespace
click_button namespace
end
end
......
......@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', :smoke do
describe 'Project' do
describe 'Project', :requires_admin do
shared_examples 'successful project creation' do
it 'creates a new project' do
Page::Project::Show.perform do |project|
......@@ -17,6 +17,7 @@ module QA
end
before do
Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
Flow::Login.sign_in
project
end
......
......@@ -20,6 +20,7 @@ module QA
end
before do
Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
sign_in
end
......
......@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
describe 'Project templates' do
describe 'Project templates', :requires_admin do
include Support::API
before(:all) do
......@@ -36,6 +36,10 @@ module QA
end
end
before do
Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
end
context 'built-in', :requires_admin do
before do
Flow::Login.sign_in_as_admin
......
......@@ -10,6 +10,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
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 searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
......@@ -28,6 +29,10 @@ describe('NewProjectUrlSelect component', () => {
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
},
{
id: 'gid://gitlab/Group/30',
fullPath: 'h5bp/subgroup',
},
],
},
namespace: {
......@@ -40,14 +45,21 @@ describe('NewProjectUrlSelect component', () => {
const localVue = createLocalVue();
localVue.use(VueApollo);
const provide = {
const defaultProvide = {
namespaceFullPath: 'h5bp',
namespaceId: '28',
rootUrl: 'https://gitlab.com/',
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 apolloProvider = createMockApollo(requestHandlers);
......@@ -75,20 +87,42 @@ describe('NewProjectUrlSelect component', () => {
it('renders the root url as a label', () => {
wrapper = mountComponent();
expect(findButtonLabel().text()).toBe(provide.rootUrl);
expect(findButtonLabel().text()).toBe(defaultProvide.rootUrl);
expect(findButtonLabel().props('label')).toBe(true);
});
it('renders a dropdown with the initial namespace full path as the text', () => {
wrapper = mountComponent();
describe('when namespaceId is provided', () => {
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', () => {
wrapper = mountComponent();
describe('when namespaceId is not provided', () => {
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 () => {
......@@ -112,11 +146,39 @@ describe('NewProjectUrlSelect component', () => {
const listItems = wrapper.findAll('li');
expect(listItems).toHaveLength(6);
expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
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(3).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath);
expect(listItems.at(3).text()).toBe(data.currentUser.groups.nodes[2].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 () => {
......@@ -164,7 +226,7 @@ describe('NewProjectUrlSelect component', () => {
findDropdown().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
label: provide.trackLabel,
label: defaultProvide.trackLabel,
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