import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
import {
DEFAULT_PLATFORM,
EXECUTORS_HELP_URL,
SERVICE_COMMANDS_HELP_URL,
STATUS_NEVER_CONTACTED,
STATUS_ONLINE,
RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
I18N_REGISTRATION_SUCCESS,
} from '~/ci/runner/constants';
import { runnerForRegistration, mockAuthenticationToken } from '../../mock_data';
Vue.use(VueApollo);
const mockRunner = {
...runnerForRegistration.data.runner,
ephemeralAuthenticationToken: mockAuthenticationToken,
};
const mockRunnerWithoutToken = {
...runnerForRegistration.data.runner,
ephemeralAuthenticationToken: null,
};
const mockRunnerId = `${getIdFromGraphQLId(mockRunner.id)}`;
describe('RegistrationInstructions', () => {
let wrapper;
let mockRunnerQuery;
const findHeading = () => wrapper.find('h1');
const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i));
const findByText = (text, container = wrapper) => container.findByText(text);
const waitForPolling = async () => {
jest.advanceTimersByTime(RUNNER_REGISTRATION_POLLING_INTERVAL_MS);
await waitForPromises();
};
const mockBeforeunload = () => {
const event = new Event('beforeunload');
const preventDefault = jest.spyOn(event, 'preventDefault');
const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
return {
event,
preventDefault,
returnValueSetter,
};
};
const mockResolvedRunner = (runner = mockRunner) => {
mockRunnerQuery.mockResolvedValue({
data: {
runner,
},
});
};
const createComponent = (props) => {
wrapper = shallowMountExtended(RegistrationInstructions, {
apolloProvider: createMockApollo([[runnerForRegistrationQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
platform: DEFAULT_PLATFORM,
...props,
},
stubs: {
GlSprintf,
},
});
};
beforeEach(() => {
mockRunnerQuery = jest.fn();
mockResolvedRunner();
});
beforeEach(() => {
window.gon.gitlab_url = TEST_HOST;
});
it('loads runner with id', () => {
createComponent();
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id });
});
describe('heading', () => {
it('when runner is loaded, shows heading', async () => {
createComponent();
await waitForPromises();
expect(findHeading().text()).toContain(mockRunner.description);
});
it('when runner is loaded, shows heading safely', async () => {
mockResolvedRunner({
...mockRunner,
description: '',
});
createComponent();
await waitForPromises();
expect(findHeading().text()).toBe('Register "" runner');
expect(findHeading().element.innerHTML).toBe(
'Register "<script>hacked();</script>" runner',
);
});
it('when runner is loading, shows default heading', () => {
createComponent();
expect(findHeading().text()).toBe(s__('Runners|Register runner'));
});
});
it('renders legacy instructions', () => {
createComponent();
findByText('How do I install GitLab Runner?').vm.$emit('click');
expect(wrapper.emitted('toggleDrawer')).toHaveLength(1);
});
describe('step 1', () => {
it('renders step 1', async () => {
createComponent();
await waitForPromises();
const step1 = findStepAt(0);
expect(step1.findComponent(CliCommand).props()).toEqual({
command: [
'gitlab-runner register',
` --url ${TEST_HOST}`,
` --token ${mockAuthenticationToken}`,
],
prompt: '$',
});
expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
});
it('renders step 1 in loading state', () => {
createComponent();
const step1 = findStepAt(0);
expect(step1.findComponent(GlSkeletonLoader).exists()).toBe(true);
expect(step1.find('code').exists()).toBe(false);
expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
});
it('render step 1 after token is not visible', async () => {
mockResolvedRunner(mockRunnerWithoutToken);
createComponent();
await waitForPromises();
const step1 = findStepAt(0);
expect(step1.findComponent(CliCommand).props('command')).toEqual([
'gitlab-runner register',
` --url ${TEST_HOST}`,
]);
expect(step1.findByTestId('runner-token').exists()).toBe(false);
expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
});
describe('polling for changes', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('fetches data', () => {
expect(mockRunnerQuery).toHaveBeenCalledTimes(1);
});
it('polls', async () => {
await waitForPolling();
expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
await waitForPolling();
expect(mockRunnerQuery).toHaveBeenCalledTimes(3);
});
it('when runner is online, stops polling', async () => {
mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
await waitForPolling();
expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
await waitForPolling();
expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
});
it('when token is no longer visible in the API, it is still visible in the UI', async () => {
mockResolvedRunner(mockRunnerWithoutToken);
await waitForPolling();
const step1 = findStepAt(0);
expect(step1.findComponent(CliCommand).props('command')).toEqual([
'gitlab-runner register',
` --url ${TEST_HOST}`,
` --token ${mockAuthenticationToken}`,
]);
expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
});
it('when runner is not available (e.g. deleted), the UI does not update', async () => {
mockResolvedRunner(null);
await waitForPolling();
const step1 = findStepAt(0);
expect(step1.findComponent(CliCommand).props('command')).toEqual([
'gitlab-runner register',
` --url ${TEST_HOST}`,
` --token ${mockAuthenticationToken}`,
]);
expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
});
});
});
it('renders step 2', () => {
createComponent();
const step2 = findStepAt(1);
expect(findByText('Not sure which one to select?', step2).attributes('href')).toBe(
EXECUTORS_HELP_URL,
);
});
it('renders step 3', () => {
createComponent();
const step3 = findStepAt(2);
expect(step3.findComponent(CliCommand).props()).toEqual({
command: 'gitlab-runner run',
prompt: '$',
});
expect(findByText('system or user service', step3).attributes('href')).toBe(
SERVICE_COMMANDS_HELP_URL,
);
});
describe('success state', () => {
describe('when the runner has not been registered', () => {
beforeEach(async () => {
createComponent();
await waitForPolling();
mockResolvedRunner({ ...mockRunner, status: STATUS_NEVER_CONTACTED });
await waitForPolling();
});
it('does not show success message', () => {
expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
});
describe('when the page is closing', () => {
it('warns the user against closing', () => {
const { event, preventDefault, returnValueSetter } = mockBeforeunload();
expect(preventDefault).not.toHaveBeenCalled();
expect(returnValueSetter).not.toHaveBeenCalled();
window.dispatchEvent(event);
expect(preventDefault).toHaveBeenCalledWith();
expect(returnValueSetter).toHaveBeenCalledWith(expect.any(String));
});
});
});
describe('when the runner has been registered', () => {
beforeEach(async () => {
createComponent();
await waitForPolling();
mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
await waitForPolling();
});
it('shows success message', () => {
expect(wrapper.text()).toContain('🎉');
expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
});
describe('when the page is closing', () => {
it('does not warn the user against closing', () => {
const { event, preventDefault, returnValueSetter } = mockBeforeunload();
expect(preventDefault).not.toHaveBeenCalled();
expect(returnValueSetter).not.toHaveBeenCalled();
window.dispatchEvent(event);
expect(preventDefault).not.toHaveBeenCalled();
expect(returnValueSetter).not.toHaveBeenCalled();
});
});
});
});
});