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 getProjectsQueryResponse from 'test_fixtures/graphql/projects/access_tokens/get_projects.query.graphql.json'; 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 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) }, ]); }); }); });