debian-mirror-gitlab/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
2022-04-04 11:22:00 +05:30

338 lines
10 KiB
JavaScript

import Vue from 'vue';
import { GlLink } 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 AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
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 RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import getRunnersCountQuery from '~/runner/graphql/get_runners_count.query.graphql';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
}));
Vue.use(VueApollo);
describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
let mockRunnersCountQuery;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
findRunnerPagination().findByLabelText('Go to previous page');
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const handlers = [
[getRunnersQuery, mockRunnersQuery],
[getRunnersCountQuery, mockRunnersCountQuery],
];
wrapper = mountFn(AdminRunnersApp, {
apolloProvider: createMockApollo(handlers),
propsData: {
registrationToken: mockRegistrationToken,
...props,
},
});
};
beforeEach(async () => {
setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData);
createComponent();
await waitForPromises();
});
afterEach(() => {
mockRunnersQuery.mockReset();
wrapper.destroy();
});
it('shows total runner counts', async () => {
createComponent({ mountFn: mountExtended });
await waitForPromises();
const stats = findRunnerStats().text();
expect(stats).toMatch('Online runners 4');
expect(stats).toMatch('Offline runners 4');
expect(stats).toMatch('Stale runners 4');
});
it('shows the runner tabs with a runner count for each type', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
switch (type) {
case INSTANCE_TYPE:
count = 3;
break;
case GROUP_TYPE:
count = 2;
break;
case PROJECT_TYPE:
count = 1;
break;
default:
count = 6;
break;
}
return Promise.resolve({ data: { runners: { count } } });
});
createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
`All 6 Instance 3 Group 2 Project 1`,
);
});
it('shows the runner tabs with a formatted runner count', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
switch (type) {
case INSTANCE_TYPE:
count = 3000;
break;
case GROUP_TYPE:
count = 2000;
break;
case PROJECT_TYPE:
count = 1000;
break;
default:
count = 6000;
break;
}
return Promise.resolve({ data: { runners: { count } } });
});
createComponent({ mountFn: mountExtended });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
`All 6,000 Instance 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(INSTANCE_TYPE);
});
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
});
it('runner item links to the runner admin page', async () => {
createComponent({ mountFn: mountExtended });
await waitForPromises();
const { id, shortSha } = runnersData.data.runners.nodes[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
});
it('requests the runners with no filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: undefined,
type: undefined,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
});
it('sets tokens in the filtered search', () => {
createComponent({ mountFn: mountExtended });
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_TAG,
recentSuggestionsStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
}),
]);
});
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
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_ACTIVE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
});
it('requests the runners with filter parameters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE,
type: INSTANCE_TYPE,
tagList: ['tag1'],
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
});
});
describe('when a filter is selected by the user', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
});
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
url: 'http://test.host/admin/runners?status[]=ACTIVE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
});
});
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 () => {
mockRunnersQuery = jest.fn().mockResolvedValue({
data: {
runners: { nodes: [] },
},
});
createComponent();
await waitForPromises();
});
it('shows a message for no results', async () => {
expect(wrapper.text()).toContain('No runners found');
});
});
describe('when runners query fails', () => {
beforeEach(async () => {
mockRunnersQuery = 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: 'AdminRunnersApp',
});
});
});
describe('Pagination', () => {
beforeEach(async () => {
mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated);
createComponent({ mountFn: mountExtended });
await waitForPromises();
});
it('more pages can be selected', () => {
expect(findRunnerPagination().text()).toMatchInterpolatedText('Prev Next');
});
it('cannot navigate to the previous page', () => {
expect(findRunnerPaginationPrev().attributes('aria-disabled')).toBe('true');
});
it('navigates to the next page', async () => {
await findRunnerPaginationNext().trigger('click');
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
after: runnersDataPaginated.data.runners.pageInfo.endCursor,
});
});
});
});