Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/runner/runner_list')
-rw-r--r--spec/frontend/runner/runner_list/runner_list_app_spec.js232
-rw-r--r--spec/frontend/runner/runner_list/runner_search_utils_spec.js239
2 files changed, 471 insertions, 0 deletions
diff --git a/spec/frontend/runner/runner_list/runner_list_app_spec.js b/spec/frontend/runner/runner_list/runner_list_app_spec.js
new file mode 100644
index 00000000000..dd913df7143
--- /dev/null
+++ b/spec/frontend/runner/runner_list/runner_list_app_spec.js
@@ -0,0 +1,232 @@
+import * as Sentry from '@sentry/browser';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
+
+import {
+ CREATED_ASC,
+ CREATED_DESC,
+ DEFAULT_SORT,
+ INSTANCE_TYPE,
+ PARAM_KEY_STATUS,
+ STATUS_ACTIVE,
+ RUNNER_PAGE_SIZE,
+} from '~/runner/constants';
+import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
+import RunnerListApp from '~/runner/runner_list/runner_list_app.vue';
+
+import { runnersData, runnersDataPaginated } from '../mock_data';
+
+const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+const mockActiveRunnersCount = 2;
+
+jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerListApp', () => {
+ let wrapper;
+ let mockRunnersQuery;
+ let originalLocation;
+
+ const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
+ const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRunnerList = () => wrapper.findComponent(RunnerList);
+ const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+ const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+
+ const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const handlers = [[getRunnersQuery, mockRunnersQuery]];
+
+ wrapper = mountFn(RunnerListApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ activeRunnersCount: mockActiveRunnersCount,
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ });
+ };
+
+ const setQuery = (query) => {
+ window.location.href = `${TEST_HOST}/admin/runners/${query}`;
+ window.location.search = query;
+ };
+
+ beforeAll(() => {
+ originalLocation = window.location;
+ Object.defineProperty(window, 'location', { writable: true, value: { href: '', search: '' } });
+ });
+
+ afterAll(() => {
+ window.location = originalLocation;
+ });
+
+ beforeEach(async () => {
+ setQuery('');
+
+ Sentry.withScope.mockImplementation((fn) => {
+ const scope = { setTag: jest.fn() };
+ fn(scope);
+ });
+
+ mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ afterEach(() => {
+ mockRunnersQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ it('shows the runners list', () => {
+ expect(runnersData.data.runners.nodes).toMatchObject(findRunnerList().props('runners'));
+ });
+
+ it('requests the runners with no filters', () => {
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ status: undefined,
+ type: undefined,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+
+ it('shows the runner type help', () => {
+ expect(findRunnerTypeHelp().exists()).toBe(true);
+ });
+
+ it('shows the runner setup instructions', () => {
+ expect(findRunnerManualSetupHelp().exists()).toBe(true);
+ expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ });
+
+ describe('when a filter is preselected', () => {
+ beforeEach(async () => {
+ window.location.search = `?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`;
+
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('sets the filters in the search bar', () => {
+ expect(findRunnerFilteredSearchBar().props('value')).toEqual({
+ filters: [
+ { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
+ ],
+ sort: 'CREATED_DESC',
+ pagination: { page: 1 },
+ });
+ });
+
+ it('requests the runners with filter parameters', () => {
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ status: STATUS_ACTIVE,
+ type: INSTANCE_TYPE,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ describe('when a filter is selected by the user', () => {
+ beforeEach(() => {
+ findRunnerFilteredSearchBar().vm.$emit('input', {
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: '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,
+ });
+ });
+ });
+
+ describe('when no runners are found', () => {
+ beforeEach(async () => {
+ mockRunnersQuery = jest.fn().mockResolvedValue({ data: { runners: { nodes: [] } } });
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('shows a message for no results', async () => {
+ expect(wrapper.text()).toContain('No runners found');
+ });
+ });
+
+ it('when runners have not loaded, shows a loading state', () => {
+ createComponentWithApollo();
+ expect(findRunnerList().props('loading')).toBe(true);
+ });
+
+ describe('when runners query fails', () => {
+ beforeEach(async () => {
+ mockRunnersQuery = jest.fn().mockRejectedValue(new Error());
+ createComponentWithApollo();
+
+ await waitForPromises();
+ });
+
+ it('error is reported to sentry', async () => {
+ expect(Sentry.withScope).toHaveBeenCalled();
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated);
+
+ createComponentWithApollo({ mountFn: mount });
+ });
+
+ it('more pages can be selected', () => {
+ expect(findRunnerPagination().text()).toMatchInterpolatedText('Prev Next');
+ });
+
+ it('cannot navigate to the previous page', () => {
+ expect(findRunnerPagination().find('[aria-disabled]').text()).toBe('Prev');
+ });
+
+ it('navigates to the next page', async () => {
+ const nextPageBtn = findRunnerPagination().find('a');
+ expect(nextPageBtn.text()).toBe('Next');
+
+ await nextPageBtn.trigger('click');
+
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ after: runnersDataPaginated.data.runners.pageInfo.endCursor,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/runner_list/runner_search_utils_spec.js b/spec/frontend/runner/runner_list/runner_search_utils_spec.js
new file mode 100644
index 00000000000..a1f33e9c880
--- /dev/null
+++ b/spec/frontend/runner/runner_list/runner_search_utils_spec.js
@@ -0,0 +1,239 @@
+import { RUNNER_PAGE_SIZE } from '~/runner/constants';
+import {
+ fromUrlQueryToSearch,
+ fromSearchToUrl,
+ fromSearchToVariables,
+} from '~/runner/runner_list/runner_search_utils';
+
+describe('search_params.js', () => {
+ const examples = [
+ {
+ name: 'a default query',
+ urlQuery: '',
+ search: { filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
+ graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'a single status',
+ urlQuery: '?status[]=ACTIVE',
+ search: {
+ filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'a single term text search',
+ urlQuery: '?search=something',
+ search: {
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'something' },
+ },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'a two terms text search',
+ urlQuery: '?search=something+else',
+ search: {
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'something' },
+ },
+ {
+ type: 'filtered-search-term',
+ value: { data: 'else' },
+ },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'single instance type',
+ urlQuery: '?runner_type[]=INSTANCE_TYPE',
+ search: {
+ filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'multiple runner status',
+ urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
+ search: {
+ filters: [
+ { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'status', value: { data: 'PAUSED', operator: '=' } },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'multiple status, a single instance type and a non default sort',
+ urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
+ search: {
+ filters: [
+ { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_ASC',
+ },
+ graphqlVariables: {
+ status: 'ACTIVE',
+ type: 'INSTANCE_TYPE',
+ sort: 'CREATED_ASC',
+ first: RUNNER_PAGE_SIZE,
+ },
+ },
+ {
+ name: 'the next page',
+ urlQuery: '?page=2&after=AFTER_CURSOR',
+ search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
+ graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'the previous page',
+ urlQuery: '?page=2&before=BEFORE_CURSOR',
+ search: {
+ filters: [],
+ pagination: { page: 2, before: 'BEFORE_CURSOR' },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
+ },
+ {
+ name:
+ 'the next page filtered by multiple status, a single instance type and a non default sort',
+ urlQuery:
+ '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
+ search: {
+ filters: [
+ { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
+ ],
+ pagination: { page: 2, after: 'AFTER_CURSOR' },
+ sort: 'CREATED_ASC',
+ },
+ graphqlVariables: {
+ status: 'ACTIVE',
+ type: 'INSTANCE_TYPE',
+ sort: 'CREATED_ASC',
+ after: 'AFTER_CURSOR',
+ first: RUNNER_PAGE_SIZE,
+ },
+ },
+ ];
+
+ describe('fromUrlQueryToSearch', () => {
+ examples.forEach(({ name, urlQuery, search }) => {
+ it(`Converts ${name} to a search object`, () => {
+ expect(fromUrlQueryToSearch(urlQuery)).toEqual(search);
+ });
+ });
+
+ it('When search params appear as array, they are concatenated', () => {
+ expect(fromUrlQueryToSearch('?search[]=my&search[]=text').filters).toEqual([
+ { type: 'filtered-search-term', value: { data: 'my' } },
+ { type: 'filtered-search-term', value: { data: 'text' } },
+ ]);
+ });
+
+ it('When a page cannot be parsed as a number, it defaults to `1`', () => {
+ expect(fromUrlQueryToSearch('?page=NONSENSE&after=AFTER_CURSOR').pagination).toEqual({
+ page: 1,
+ });
+ });
+
+ it('When a page is less than 1, it defaults to `1`', () => {
+ expect(fromUrlQueryToSearch('?page=0&after=AFTER_CURSOR').pagination).toEqual({
+ page: 1,
+ });
+ });
+
+ it('When a page with no cursor is given, it defaults to `1`', () => {
+ expect(fromUrlQueryToSearch('?page=2').pagination).toEqual({
+ page: 1,
+ });
+ });
+ });
+
+ describe('fromSearchToUrl', () => {
+ examples.forEach(({ name, urlQuery, search }) => {
+ it(`Converts ${name} to a url`, () => {
+ expect(fromSearchToUrl(search)).toEqual(`http://test.host/${urlQuery}`);
+ });
+ });
+
+ it.each([
+ 'http://test.host/?status[]=ACTIVE',
+ 'http://test.host/?runner_type[]=INSTANCE_TYPE',
+ 'http://test.host/?search=my_text',
+ ])('When a filter is removed, it is removed from the URL', (initalUrl) => {
+ const search = { filters: [], sort: 'CREATED_DESC' };
+ const expectedUrl = `http://test.host/`;
+
+ expect(fromSearchToUrl(search, initalUrl)).toEqual(expectedUrl);
+ });
+
+ it('When unrelated search parameter is present, it does not get removed', () => {
+ const initialUrl = `http://test.host/?unrelated=UNRELATED&status[]=ACTIVE`;
+ const search = { filters: [], sort: 'CREATED_DESC' };
+ const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
+
+ expect(fromSearchToUrl(search, initialUrl)).toEqual(expectedUrl);
+ });
+ });
+
+ describe('fromSearchToVariables', () => {
+ examples.forEach(({ name, graphqlVariables, search }) => {
+ it(`Converts ${name} to a GraphQL query variables object`, () => {
+ expect(fromSearchToVariables(search)).toEqual(graphqlVariables);
+ });
+ });
+
+ it('When a search param is empty, it gets removed', () => {
+ expect(
+ fromSearchToVariables({
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: '' },
+ },
+ ],
+ }),
+ ).toMatchObject({
+ search: '',
+ });
+
+ expect(
+ fromSearchToVariables({
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'something' },
+ },
+ {
+ type: 'filtered-search-term',
+ value: { data: '' },
+ },
+ ],
+ }),
+ ).toMatchObject({
+ search: 'something',
+ });
+ });
+ });
+});