debian-mirror-gitlab/spec/frontend/runner/group_runners/group_runners_app_spec.js
2022-07-23 20:15:48 +02:00

440 lines
14 KiB
JavaScript

import Vue, { nextTick } from 'vue';
import { GlButton, GlLink, GlToast } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import {
extendedWrapper,
shallowMountExtended,
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
import getGroupRunnersQuery from '~/runner/graphql/list/group_runners.query.graphql';
import getGroupRunnersCountQuery from '~/runner/graphql/list/group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
groupRunnersData,
groupRunnersDataPaginated,
groupRunnersCountData,
onlineContactTimeoutSecs,
staleTimeoutSecs,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
} from '../mock_data';
Vue.use(VueApollo);
Vue.use(GlToast);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges;
const mockGroupRunnersCount = mockGroupRunnersEdges.length;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
}));
describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
let mockGroupRunnersCountQuery;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`));
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const mockCountQueryResult = (count) =>
Promise.resolve({
data: { group: { id: groupRunnersCountData.data.group.id, runners: { count } } },
});
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const handlers = [
[getGroupRunnersQuery, mockGroupRunnersQuery],
[getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
];
wrapper = mountFn(GroupRunnersApp, {
apolloProvider: createMockApollo(handlers),
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
onlineContactTimeoutSecs,
staleTimeoutSecs,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
},
});
};
beforeEach(async () => {
setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData);
createComponent();
await waitForPromises();
});
it('shows total runner counts', async () => {
expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
});
expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_OFFLINE,
});
expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_STALE,
});
expect(findRunnerStats().props()).toMatchObject({
onlineRunnersCount: mockGroupRunnersCount,
offlineRunnersCount: mockGroupRunnersCount,
staleRunnersCount: mockGroupRunnersCount,
});
});
it('shows the runner tabs with a runner count for each type', async () => {
mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
switch (type) {
case GROUP_TYPE:
return mockCountQueryResult(2);
case PROJECT_TYPE:
return mockCountQueryResult(1);
default:
return mockCountQueryResult(4);
}
});
createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText('All 4 Group 2 Project 1');
});
it('shows the runner tabs with a formatted runner count', async () => {
mockGroupRunnersCountQuery.mockImplementation(({ type }) => {
switch (type) {
case GROUP_TYPE:
return mockCountQueryResult(2000);
case PROJECT_TYPE:
return mockCountQueryResult(1000);
default:
return mockCountQueryResult(3000);
}
});
createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
'All 3,000 Group 2,000 Project 1,000',
);
});
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
});
it('shows the runners list', () => {
const runners = findRunnerList().props('runners');
expect(runners).toEqual(mockGroupRunnersEdges.map(({ node }) => node));
});
it('requests the runners with group path and no other filters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: undefined,
type: undefined,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
});
it('sets tokens in the filtered search', () => {
createComponent({ mountFn: mountExtended });
const tokens = findFilteredSearch().props('tokens');
expect(tokens).toEqual([
expect.objectContaining({
type: PARAM_KEY_PAUSED,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
]);
});
describe('Single runner row', () => {
let showToast;
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
mockGroupRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
await waitForPromises();
});
it('view link is displayed correctly', () => {
const viewLink = findRunnerRow(id).findByTestId('td-summary').findComponent(GlLink);
expect(viewLink.text()).toBe(`#${id} (${shortSha})`);
expect(viewLink.attributes('href')).toBe(webUrl);
});
it('edit link is displayed correctly', () => {
const editLink = findRunnerRow(id).findByTestId('td-actions').findComponent(GlButton);
expect(editLink.attributes()).toMatchObject({
'aria-label': I18N_EDIT,
href: editUrl,
});
});
it('When runner is paused or unpaused, some data is refetched', async () => {
expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(
COUNT_QUERIES + FILTERED_COUNT_QUERIES,
);
expect(showToast).toHaveBeenCalledTimes(0);
});
it('When runner is deleted, data is refetched and a toast message is shown', async () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
});
});
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`);
createComponent();
await waitForPromises();
});
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
});
it('requests the runners with filter parameters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
type: INSTANCE_TYPE,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
});
it('fetches count results for requested status', () => {
expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
type: INSTANCE_TYPE,
status: STATUS_ONLINE,
});
expect(findRunnerStats().props()).toMatchObject({
onlineRunnersCount: mockGroupRunnersCount,
});
});
});
describe('when a filter is selected by the user', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
filters: [
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
],
sort: CREATED_ASC,
});
await nextTick();
});
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
});
it('fetches count results for requested status', () => {
expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
tagList: ['tag1'],
status: STATUS_ONLINE,
});
expect(findRunnerStats().props()).toMatchObject({
onlineRunnersCount: mockGroupRunnersCount,
});
});
it('skips fetching count results for status that were not in filter', () => {
expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
tagList: ['tag1'],
status: STATUS_OFFLINE,
});
expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
tagList: ['tag1'],
status: STATUS_STALE,
});
expect(findRunnerStats().props()).toMatchObject({
offlineRunnersCount: null,
staleRunnersCount: null,
});
});
});
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
});
describe('when no runners are found', () => {
beforeEach(async () => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue({
data: {
group: {
id: '1',
runners: { nodes: [] },
},
},
});
createComponent();
await waitForPromises();
});
it('shows an empty state', async () => {
expect(findRunnerListEmptyState().exists()).toBe(true);
});
});
describe('when runners query fails', () => {
beforeEach(async () => {
mockGroupRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
createComponent();
await waitForPromises();
});
it('error is shown to the user', async () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'GroupRunnersApp',
});
});
});
describe('Pagination', () => {
beforeEach(async () => {
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated);
createComponent({ mountFn: mountExtended });
await waitForPromises();
});
it('navigates to the next page', async () => {
await findRunnerPaginationNext().trigger('click');
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
after: groupRunnersDataPaginated.data.group.runners.pageInfo.endCursor,
});
});
});
});