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';
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