import { GlLoadingIcon, GlPagination, GlSkeletonLoader, GlTableLite } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import Clusters from '~/clusters_list/components/clusters.vue'; import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue'; import ClusterStore from '~/clusters_list/store'; import axios from '~/lib/utils/axios_utils'; import { apiData } from '../mock_data'; describe('Clusters', () => { let mock; let store; let wrapper; const endpoint = 'some/endpoint'; const totalClustersNumber = 6; const clustersEmptyStateImage = 'path/to/svg'; const emptyStateHelpText = null; const addClusterPath = '/path/to/new/cluster'; const entryData = { endpoint, imgTagsAwsText: 'AWS Icon', imgTagsDefaultText: 'Default Icon', imgTagsGcpText: 'GCP Icon', totalClusters: totalClustersNumber, }; const provideData = { clustersEmptyStateImage, emptyStateHelpText, addClusterPath, }; const findLoader = () => wrapper.findComponent(GlLoadingIcon); const findPaginatedButtons = () => wrapper.findComponent(GlPagination); const findTable = () => wrapper.findComponent(GlTableLite); const findStatuses = () => findTable().findAll('.js-status'); const findEmptyState = () => wrapper.findComponent(ClustersEmptyState); const mockPollingApi = (response, body, header) => { mock.onGet(`${endpoint}?page=${header['x-page']}`).reply(response, body, header); }; const createWrapper = ({ propsData = {} }) => { store = ClusterStore(entryData); wrapper = mount(Clusters, { propsData, provide: provideData, store, stubs: { GlTableLite } }); return axios.waitForAll(); }; const paginationHeader = (total = apiData.clusters.length, perPage = 20, currentPage = 1) => { return { 'x-total': total, 'x-per-page': perPage, 'x-page': currentPage, }; }; let captureException; beforeEach(() => { captureException = jest.spyOn(Sentry, 'captureException'); mock = new MockAdapter(axios); mockPollingApi(200, apiData, paginationHeader()); return createWrapper({}); }); afterEach(() => { wrapper.destroy(); mock.restore(); captureException.mockRestore(); }); describe('clusters table', () => { describe('when data is loading', () => { beforeEach(() => { wrapper.vm.$store.state.loadingClusters = true; }); it('displays a loader instead of the table while loading', () => { expect(findLoader().exists()).toBe(true); expect(findTable().exists()).toBe(false); }); }); describe('when clusters are present', () => { it('displays a table component', () => { expect(findTable().exists()).toBe(true); }); }); describe('when there are no clusters', () => { beforeEach(() => { wrapper.vm.$store.state.totalClusters = 0; }); it('should render empty state', () => { expect(findEmptyState().exists()).toBe(true); }); }); describe('when is loaded as a child component', () => { beforeEach(() => { createWrapper({ limit: 6 }); }); it("shouldn't render pagination buttons", () => { expect(findPaginatedButtons().exists()).toBe(false); }); }); }); describe('cluster icon', () => { it.each` providerText | lineNumber ${'GCP Icon'} | ${0} ${'AWS Icon'} | ${1} ${'Default Icon'} | ${2} ${'Default Icon'} | ${3} ${'Default Icon'} | ${4} ${'Default Icon'} | ${5} `('renders provider image and alt text for each cluster', ({ providerText, lineNumber }) => { const images = findTable().findAll('.js-status img'); const image = images.at(lineNumber); expect(image.attributes('alt')).toBe(providerText); }); }); describe('cluster status', () => { it.each` statusName | lineNumber | result ${'creating'} | ${0} | ${true} ${null} | ${1} | ${false} ${null} | ${2} | ${false} ${'deleting'} | ${3} | ${true} ${null} | ${4} | ${false} ${null} | ${5} | ${false} `( 'renders $result when status=$statusName and lineNumber=$lineNumber', ({ lineNumber, result }) => { const statuses = findStatuses(); const status = statuses.at(lineNumber); expect(status.find(GlLoadingIcon).exists()).toBe(result); }, ); }); describe('nodes present', () => { describe('nodes while loading', () => { it.each` nodeSize | lineNumber ${null} | ${0} ${'1'} | ${1} ${'2'} | ${2} ${'1'} | ${3} ${'1'} | ${4} ${null} | ${5} `('renders node size for each cluster', ({ nodeSize, lineNumber }) => { const sizes = findTable().findAll('td:nth-child(3)'); const size = sizes.at(lineNumber); if (nodeSize) { expect(size.text()).toBe(nodeSize); } else { expect(size.findComponent(GlSkeletonLoader).exists()).toBe(true); } }); }); describe('nodes finish loading', () => { beforeEach(async () => { wrapper.vm.$store.state.loadingNodes = false; await nextTick(); }); it.each` nodeText | lineNumber ${'Unable to Authenticate'} | ${0} ${'1'} | ${1} ${'2'} | ${2} ${'1'} | ${3} ${'1'} | ${4} ${'Unknown Error'} | ${5} `('renders node size for each cluster', ({ nodeText, lineNumber }) => { const sizes = findTable().findAll('td:nth-child(3)'); const size = sizes.at(lineNumber); expect(size.text()).toContain(nodeText); expect(size.findComponent(GlSkeletonLoader).exists()).toBe(false); }); }); describe('nodes with unknown quantity', () => { it('notifies Sentry about all missing quantity types', () => { expect(captureException).toHaveBeenCalledTimes(8); }); it('notifies Sentry about CPU missing quantity types', () => { const missingCpuTypeError = new Error('UnknownK8sCpuQuantity:1missingCpuUnit'); expect(captureException).toHaveBeenCalledWith(missingCpuTypeError); }); it('notifies Sentry about Memory missing quantity types', () => { const missingMemoryTypeError = new Error('UnknownK8sMemoryQuantity:1missingMemoryUnit'); expect(captureException).toHaveBeenCalledWith(missingMemoryTypeError); }); }); }); describe('cluster CPU', () => { it.each` clusterCpu | lineNumber ${'Loading'} | ${0} ${'1.93 (87% free)'} | ${1} ${'3.87 (86% free)'} | ${2} ${'(% free)'} | ${3} ${'(% free)'} | ${4} ${'Loading'} | ${5} `('renders total cpu for each cluster', ({ clusterCpu, lineNumber }) => { const clusterCpus = findTable().findAll('td:nth-child(4)'); const cpuData = clusterCpus.at(lineNumber); expect(cpuData.text()).toBe(clusterCpu); }); }); describe('cluster Memory', () => { it.each` clusterMemory | lineNumber ${'Loading'} | ${0} ${'5.92 (78% free)'} | ${1} ${'12.86 (79% free)'} | ${2} ${'(% free)'} | ${3} ${'(% free)'} | ${4} ${'Loading'} | ${5} `('renders total memory for each cluster', ({ clusterMemory, lineNumber }) => { const clusterMemories = findTable().findAll('td:nth-child(5)'); const memoryData = clusterMemories.at(lineNumber); expect(memoryData.text()).toBe(clusterMemory); }); }); describe('pagination', () => { const perPage = apiData.clusters.length; const totalFirstPage = 100; const totalSecondPage = 500; beforeEach(() => { mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1)); return createWrapper({}); }); it('should load to page 1 with header values', () => { const buttons = findPaginatedButtons(); expect(buttons.props('perPage')).toBe(perPage); expect(buttons.props('totalItems')).toBe(totalFirstPage); expect(buttons.props('value')).toBe(1); }); describe('when updating currentPage', () => { beforeEach(() => { mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2)); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentPage: 2 }); return axios.waitForAll(); }); it('should change pagination when currentPage changes', () => { const buttons = findPaginatedButtons(); expect(buttons.props('perPage')).toBe(perPage); expect(buttons.props('totalItems')).toBe(totalSecondPage); expect(buttons.props('value')).toBe(2); }); }); }); });