diff options
Diffstat (limited to 'spec/frontend/access_tokens/components/projects_token_selector_spec.js')
-rw-r--r-- | spec/frontend/access_tokens/components/projects_token_selector_spec.js | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js new file mode 100644 index 00000000000..09f52fe9a5f --- /dev/null +++ b/spec/frontend/access_tokens/components/projects_token_selector_spec.js @@ -0,0 +1,269 @@ +import { + GlAvatar, + GlAvatarLabeled, + GlIntersectionObserver, + GlToken, + GlTokenSelector, + GlLoadingIcon, +} from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import produce from 'immer'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; + +import { getJSONFixture } from 'helpers/fixtures'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; +import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +describe('ProjectsTokenSelector', () => { + const getProjectsQueryResponse = getJSONFixture( + 'graphql/projects/access_tokens/get_projects.query.graphql.json', + ); + const getProjectsQueryResponsePage2 = produce( + getProjectsQueryResponse, + (getProjectsQueryResponseDraft) => { + /* eslint-disable no-param-reassign */ + getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false; + getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null; + getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1); + getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100'; + /* eslint-enable no-param-reassign */ + }, + ); + + const runDebounce = () => jest.runAllTimers(); + + const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects; + const project1 = projects[0]; + const project2 = projects[1]; + + let wrapper; + + let resolveGetProjectsQuery; + let resolveGetInitialProjectsQuery; + const getProjectsQueryRequestHandler = jest.fn( + ({ ids }) => + new Promise((resolve) => { + if (ids) { + resolveGetInitialProjectsQuery = resolve; + } else { + resolveGetProjectsQuery = resolve; + } + }), + ); + + const createComponent = ({ + propsData = {}, + apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]), + resolveQueries = true, + } = {}) => { + Vue.use(VueApollo); + + wrapper = extendedWrapper( + mount(ProjectsTokenSelector, { + apolloProvider, + propsData: { + selectedProjects: [], + initialProjectIds: [], + ...propsData, + }, + stubs: ['gl-intersection-observer'], + }), + ); + + runDebounce(); + + if (resolveQueries) { + resolveGetProjectsQuery(getProjectsQueryResponse); + + return waitForPromises(); + } + + return Promise.resolve(); + }; + + const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); + const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]'); + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + + it('renders dropdown items with project avatars', async () => { + await createComponent(); + + wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => { + const project = projects[index]; + + expect(avatarLabeledWrapper.attributes()).toEqual( + expect.objectContaining({ + 'entity-id': `${getIdFromGraphQLId(project.id)}`, + 'entity-name': project.name, + ...(project.avatarUrl && { src: project.avatarUrl }), + }), + ); + + expect(avatarLabeledWrapper.props()).toEqual( + expect.objectContaining({ + label: project.name, + subLabel: project.nameWithNamespace, + }), + ); + }); + }); + + it('renders tokens with project avatars', () => { + createComponent({ + propsData: { + selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }], + }, + }); + + const token = wrapper.findComponent(GlToken); + const avatar = token.findComponent(GlAvatar); + + expect(token.text()).toContain(project2.nameWithNamespace); + expect(avatar.attributes('src')).toBe(project2.avatarUrl); + expect(avatar.props()).toEqual( + expect.objectContaining({ + entityId: getIdFromGraphQLId(project2.id), + entityName: project2.name, + }), + ); + }); + + describe('when `enter` key is pressed', () => { + it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => { + createComponent(); + + const event = { + preventDefault: jest.fn(), + }; + + findTokenSelectorInput().trigger('keydown.enter', event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('when text input is typed in', () => { + const searchTerm = 'foo bar'; + + beforeEach(async () => { + await createComponent(); + + await findTokenSelectorInput().setValue(searchTerm); + runDebounce(); + }); + + it('makes GraphQL request with `search` variable set', async () => { + expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ + search: searchTerm, + after: null, + first: 20, + ids: null, + }); + }); + + it('sets loading state while waiting for GraphQL request to resolve', async () => { + expect(findTokenSelector().props('loading')).toBe(true); + + resolveGetProjectsQuery(getProjectsQueryResponse); + await waitForPromises(); + + expect(findTokenSelector().props('loading')).toBe(false); + }); + }); + + describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => { + beforeEach(async () => { + await createComponent(); + + findIntersectionObserver().vm.$emit('appear'); + }); + + it('makes GraphQL request with `after` variable set', async () => { + expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ + after: pageInfo.endCursor, + first: 20, + search: '', + ids: null, + }); + }); + + it('displays loading icon while waiting for GraphQL request to resolve', async () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + + resolveGetProjectsQuery(getProjectsQueryResponsePage2); + await waitForPromises(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + }); + }); + + describe('when there is not a next page of projects', () => { + it('does not render `GlIntersectionObserver`', async () => { + createComponent({ resolveQueries: false }); + + resolveGetProjectsQuery(getProjectsQueryResponsePage2); + await waitForPromises(); + + expect(findIntersectionObserver().exists()).toBe(false); + }); + }); + + describe('when `GlTokenSelector` emits `input` event', () => { + it('emits `input` event used by `v-model`', () => { + findTokenSelector().vm.$emit('input', project1); + + expect(wrapper.emitted('input')[0]).toEqual([project1]); + }); + }); + + describe('when `GlTokenSelector` emits `focus` event', () => { + it('emits `focus` event', () => { + const event = { fakeEvent: 'foo' }; + findTokenSelector().vm.$emit('focus', event); + + expect(wrapper.emitted('focus')[0]).toEqual([event]); + }); + }); + + describe('when `initialProjectIds` is an empty array', () => { + it('does not request initial projects', async () => { + await createComponent(); + + expect(getProjectsQueryRequestHandler).toHaveBeenCalledTimes(1); + expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + ids: null, + }), + ); + }); + }); + + describe('when `initialProjectIds` is an array of project IDs', () => { + it('requests those projects and emits `input` event with result', async () => { + await createComponent({ + propsData: { + initialProjectIds: [getIdFromGraphQLId(project1.id), getIdFromGraphQLId(project2.id)], + }, + }); + + resolveGetInitialProjectsQuery(getProjectsQueryResponse); + await waitForPromises(); + + expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith({ + after: '', + first: null, + search: '', + ids: [project1.id, project2.id], + }); + expect(wrapper.emitted('input')[0][0]).toEqual([ + { ...project1, id: getIdFromGraphQLId(project1.id) }, + { ...project2, id: getIdFromGraphQLId(project2.id) }, + ]); + }); + }); +}); |