import { merge } from 'lodash'; import { GlLoadingIcon, GlEmptyState, GlPagination, GlModal } from '@gitlab/ui'; import { nextTick } from 'vue'; import responseBody from 'test_fixtures/api/deploy_keys/index.json'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { stubComponent } from 'helpers/stub_component'; import DeployKeysTable from '~/admin/deploy_keys/components/table.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import Api, { DEFAULT_PER_PAGE } from '~/api'; import { createAlert } from '~/flash'; jest.mock('~/api'); jest.mock('~/flash'); jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); describe('DeployKeysTable', () => { let wrapper; const defaultProvide = { createPath: '/admin/deploy_keys/new', deletePath: '/admin/deploy_keys/:id', editPath: '/admin/deploy_keys/:id/edit', emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg', }; const deployKey = responseBody[0]; const deployKey2 = responseBody[1]; const deployKeyWithoutMd5Fingerprint = responseBody[2]; const createComponent = (provide = {}) => { wrapper = mountExtended(DeployKeysTable, { provide: merge({}, defaultProvide, provide), stubs: { GlModal: stubComponent(GlModal, { template: `
`, }), }, }); }; const findEditButton = (index) => wrapper.findAllByLabelText(DeployKeysTable.i18n.edit, { selector: 'a' }).at(index); const findRemoveButton = (index) => wrapper.findAllByLabelText(DeployKeysTable.i18n.delete, { selector: 'button' }).at(index); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findTimeAgoTooltip = (index) => wrapper.findAllComponents(TimeAgoTooltip).at(index); const findPagination = () => wrapper.findComponent(GlPagination); const expectDeployKeyIsRendered = (expectedDeployKey, expectedRowIndex) => { const editButton = findEditButton(expectedRowIndex); const timeAgoTooltip = findTimeAgoTooltip(expectedRowIndex); expect(wrapper.findByText(expectedDeployKey.title).exists()).toBe(true); expect( wrapper.findByText(expectedDeployKey.fingerprint_sha256, { selector: 'span' }).exists(), ).toBe(true); expect(timeAgoTooltip.exists()).toBe(true); expect(timeAgoTooltip.props('time')).toBe(expectedDeployKey.created_at); expect(editButton.exists()).toBe(true); expect(editButton.attributes('href')).toBe(`/admin/deploy_keys/${expectedDeployKey.id}/edit`); expect(findRemoveButton(expectedRowIndex).exists()).toBe(true); }; const expectDeployKeyWithFingerprintIsRendered = (expectedDeployKey, expectedRowIndex) => { expect(wrapper.findByText(expectedDeployKey.fingerprint, { selector: 'span' }).exists()).toBe( true, ); expectDeployKeyIsRendered(expectedDeployKey, expectedRowIndex); }; const itRendersTheEmptyState = () => { it('renders empty state', () => { const emptyState = wrapper.findComponent(GlEmptyState); expect(emptyState.exists()).toBe(true); expect(emptyState.props()).toMatchObject({ svgPath: defaultProvide.emptyStateSvgPath, title: DeployKeysTable.i18n.emptyStateTitle, description: DeployKeysTable.i18n.emptyStateDescription, primaryButtonText: DeployKeysTable.i18n.newDeployKeyButtonText, primaryButtonLink: defaultProvide.createPath, }); }); }; afterEach(() => { wrapper.destroy(); }); it('renders page title', () => { createComponent(); expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true); }); it('renders `New deploy key` button', () => { createComponent(); const newDeployKeyButton = wrapper.findByTestId('new-deploy-key-button'); expect(newDeployKeyButton.exists()).toBe(true); expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath); }); describe('when `/deploy_keys` API request is pending', () => { beforeEach(() => { Api.deployKeys.mockImplementation(() => new Promise(() => {})); }); it('shows loading icon', async () => { createComponent(); await nextTick(); expect(findLoadingIcon().exists()).toBe(true); }); }); describe('when `/deploy_keys` API request is successful', () => { describe('when there are deploy keys', () => { beforeEach(() => { Api.deployKeys.mockResolvedValue({ data: responseBody, headers: { 'x-total': `${responseBody.length}` }, }); createComponent(); }); it('renders deploy keys in table', () => { expectDeployKeyWithFingerprintIsRendered(deployKey, 0); expectDeployKeyWithFingerprintIsRendered(deployKey2, 1); }); it('renders deploy keys that do not have an MD5 fingerprint', () => { expectDeployKeyIsRendered(deployKeyWithoutMd5Fingerprint, 2); }); describe('when delete button is clicked', () => { it('asks user to confirm', async () => { await findRemoveButton(0).trigger('click'); const modal = wrapper.findComponent(GlModal); const form = modal.find('form'); const submitSpy = jest.spyOn(form.element, 'submit'); expect(modal.props('visible')).toBe(true); expect(form.attributes('action')).toBe(`/admin/deploy_keys/${deployKey.id}`); expect(form.find('input[name="_method"]').attributes('value')).toBe('delete'); expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe( 'mock-csrf-token', ); modal.vm.$emit('primary'); expect(submitSpy).toHaveBeenCalled(); }); }); }); describe('pagination', () => { beforeEach(() => { Api.deployKeys.mockResolvedValueOnce({ data: [deployKey], headers: { 'x-total': '3' }, }); createComponent(); }); it('renders pagination', () => { const pagination = findPagination(); expect(pagination.exists()).toBe(true); expect(pagination.props()).toMatchObject({ value: 1, perPage: DEFAULT_PER_PAGE, totalItems: responseBody.length, nextText: DeployKeysTable.i18n.pagination.next, prevText: DeployKeysTable.i18n.pagination.prev, align: 'center', }); }); describe('when pagination is changed', () => { it('calls API with `page` parameter', async () => { const pagination = findPagination(); expectDeployKeyWithFingerprintIsRendered(deployKey, 0); Api.deployKeys.mockResolvedValue({ data: [deployKey2], headers: { 'x-total': '2' }, }); pagination.vm.$emit('input', 2); await nextTick(); expect(findLoadingIcon().exists()).toBe(true); expect(pagination.exists()).toBe(false); await waitForPromises(); expect(Api.deployKeys).toHaveBeenCalledWith({ page: 2, public: true, }); expectDeployKeyWithFingerprintIsRendered(deployKey2, 0); }); }); }); describe('when there are no deploy keys', () => { beforeEach(() => { Api.deployKeys.mockResolvedValue({ data: [], headers: { 'x-total': '0' }, }); createComponent(); }); itRendersTheEmptyState(); }); }); describe('when `deploy_keys` API request is unsuccessful', () => { const error = new Error('Network Error'); beforeEach(() => { Api.deployKeys.mockRejectedValue(error); createComponent(); }); itRendersTheEmptyState(); it('displays flash', () => { expect(createAlert).toHaveBeenCalledWith({ message: DeployKeysTable.i18n.apiErrorMessage, captureError: true, error, }); }); }); });