Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
803fa8d1c7
commit
0976c2c3d9
11 changed files with 440 additions and 304 deletions
|
@ -1,29 +1,16 @@
|
|||
<script>
|
||||
import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { createAlert } from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { GlButtonGroup } from '@gitlab/ui';
|
||||
import RunnerEditButton from '../runner_edit_button.vue';
|
||||
import RunnerPauseButton from '../runner_pause_button.vue';
|
||||
import RunnerDeleteModal from '../runner_delete_modal.vue';
|
||||
|
||||
const I18N_DELETE = s__('Runners|Delete runner');
|
||||
const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
|
||||
import RunnerDeleteButton from '../runner_delete_button.vue';
|
||||
|
||||
export default {
|
||||
name: 'RunnerActionsCell',
|
||||
components: {
|
||||
GlButton,
|
||||
GlButtonGroup,
|
||||
RunnerEditButton,
|
||||
RunnerPauseButton,
|
||||
RunnerDeleteModal,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
RunnerDeleteButton,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
|
@ -31,30 +18,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updating: false,
|
||||
deleting: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
deleteTitle() {
|
||||
if (this.deleting) {
|
||||
// Prevent a "sticky" tooltip: If this button is disabled,
|
||||
// mouseout listeners don't run leaving the tooltip stuck
|
||||
return '';
|
||||
}
|
||||
return I18N_DELETE;
|
||||
},
|
||||
runnerId() {
|
||||
return getIdFromGraphQLId(this.runner.id);
|
||||
},
|
||||
runnerName() {
|
||||
return `#${this.runnerId} (${this.runner.shortSha})`;
|
||||
},
|
||||
runnerDeleteModalId() {
|
||||
return `delete-runner-modal-${this.runnerId}`;
|
||||
},
|
||||
canUpdate() {
|
||||
return this.runner.userPermissions?.updateRunner;
|
||||
},
|
||||
|
@ -62,49 +26,6 @@ export default {
|
|||
return this.runner.userPermissions?.deleteRunner;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onDelete() {
|
||||
// Deleting stays "true" until this row is removed,
|
||||
// should only change back if the operation fails.
|
||||
this.deleting = true;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
runnerDelete: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: runnerDeleteMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.runner.id,
|
||||
},
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: ['getRunners', 'getGroupRunners'],
|
||||
});
|
||||
if (errors && errors.length) {
|
||||
throw new Error(errors.join(' '));
|
||||
} else {
|
||||
// Use $root to have the toast message stay after this element is removed
|
||||
this.$root.$toast?.show(sprintf(I18N_DELETED_TOAST, { name: this.runnerName }));
|
||||
}
|
||||
} catch (e) {
|
||||
this.deleting = false;
|
||||
this.onError(e);
|
||||
}
|
||||
},
|
||||
|
||||
onError(error) {
|
||||
const { message } = error;
|
||||
createAlert({ message });
|
||||
|
||||
this.reportToSentry(error);
|
||||
},
|
||||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
},
|
||||
},
|
||||
I18N_DELETE,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -119,23 +40,6 @@ export default {
|
|||
-->
|
||||
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
|
||||
<runner-pause-button v-if="canUpdate" :runner="runner" :compact="true" />
|
||||
<gl-button
|
||||
v-if="canDelete"
|
||||
v-gl-tooltip.hover.viewport="deleteTitle"
|
||||
v-gl-modal="runnerDeleteModalId"
|
||||
:aria-label="deleteTitle"
|
||||
icon="close"
|
||||
:loading="deleting"
|
||||
variant="danger"
|
||||
data-testid="delete-runner"
|
||||
/>
|
||||
|
||||
<runner-delete-modal
|
||||
v-if="canDelete"
|
||||
:ref="runnerDeleteModalId"
|
||||
:modal-id="runnerDeleteModalId"
|
||||
:runner-name="runnerName"
|
||||
@primary="onDelete"
|
||||
/>
|
||||
<runner-delete-button v-if="canDelete" :runner="runner" :compact="true" />
|
||||
</gl-button-group>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
<script>
|
||||
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import { createAlert } from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { I18N_DELETE_RUNNER } from '../constants';
|
||||
import RunnerDeleteModal from './runner_delete_modal.vue';
|
||||
|
||||
const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
|
||||
|
||||
export default {
|
||||
name: 'RunnerDeleteButton',
|
||||
components: {
|
||||
GlButton,
|
||||
RunnerDeleteModal,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (runner) => {
|
||||
return runner?.id && runner?.shortSha;
|
||||
},
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
deleting: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
runnerId() {
|
||||
return getIdFromGraphQLId(this.runner.id);
|
||||
},
|
||||
runnerName() {
|
||||
return `#${this.runnerId} (${this.runner.shortSha})`;
|
||||
},
|
||||
runnerDeleteModalId() {
|
||||
return `delete-runner-modal-${this.runnerId}`;
|
||||
},
|
||||
icon() {
|
||||
if (this.compact) {
|
||||
return 'close';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
buttonContent() {
|
||||
if (this.compact) {
|
||||
return null;
|
||||
}
|
||||
return I18N_DELETE_RUNNER;
|
||||
},
|
||||
buttonClass() {
|
||||
// Ensure a square button is shown when compact: true.
|
||||
// Without this class we will have distorted/rectangular button.
|
||||
if (this.compact) {
|
||||
return 'btn-icon';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
ariaLabel() {
|
||||
if (this.compact) {
|
||||
return I18N_DELETE_RUNNER;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
tooltip() {
|
||||
// Only show tooltip when compact.
|
||||
// Also prevent a "sticky" tooltip: If this button is
|
||||
// disabled, mouseout listeners don't run leaving the tooltip stuck
|
||||
if (this.compact && !this.deleting) {
|
||||
return I18N_DELETE_RUNNER;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onDelete() {
|
||||
// Deleting stays "true" until this row is removed,
|
||||
// should only change back if the operation fails.
|
||||
this.deleting = true;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
runnerDelete: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: runnerDeleteMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.runner.id,
|
||||
},
|
||||
},
|
||||
refetchQueries: ['getRunners', 'getGroupRunners'],
|
||||
});
|
||||
if (errors && errors.length) {
|
||||
throw new Error(errors.join(' '));
|
||||
} else {
|
||||
this.$root.$toast?.show(sprintf(I18N_DELETED_TOAST, { name: this.runnerName }));
|
||||
}
|
||||
} catch (e) {
|
||||
this.deleting = false;
|
||||
this.onError(e);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
const { message } = error;
|
||||
createAlert({ message });
|
||||
|
||||
this.reportToSentry(error);
|
||||
},
|
||||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button
|
||||
v-gl-tooltip.hover.viewport="tooltip"
|
||||
v-gl-modal="runnerDeleteModalId"
|
||||
:aria-label="ariaLabel"
|
||||
:icon="icon"
|
||||
:class="buttonClass"
|
||||
:loading="deleting"
|
||||
variant="danger"
|
||||
>
|
||||
{{ buttonContent }}
|
||||
<runner-delete-modal
|
||||
:modal-id="runnerDeleteModalId"
|
||||
:runner-name="runnerName"
|
||||
@primary="onDelete"
|
||||
/>
|
||||
</gl-button>
|
||||
</template>
|
|
@ -38,6 +38,7 @@ export const I18N_STALE_RUNNER_DESCRIPTION = s__(
|
|||
// Active flag
|
||||
export const I18N_PAUSE = __('Pause');
|
||||
export const I18N_RESUME = __('Resume');
|
||||
export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
|
||||
|
||||
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
|
||||
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
|
||||
|
|
|
@ -32,15 +32,15 @@ where you can select a project to enable the GitLab Slack application for.
|
|||
Alternatively, you can configure the Slack application with a project's
|
||||
integration settings.
|
||||
|
||||
Keep in mind that you need to have the appropriate permissions for your Slack
|
||||
team in order to be able to install a new application, read more in Slack's
|
||||
docs on [Adding an app to your workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
|
||||
Keep in mind that you must have the appropriate permissions for your Slack
|
||||
team to be able to install a new application, read more in Slack's
|
||||
documentation on [Adding an app to your workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
|
||||
|
||||
To enable the GitLab service for your Slack team:
|
||||
|
||||
1. Go to your project's **Settings > Integration > Slack application** (only
|
||||
visible on GitLab.com).
|
||||
1. Click **Add to Slack**.
|
||||
1. Select **Add to Slack**.
|
||||
|
||||
That's all! You can now start using the Slack slash commands.
|
||||
|
||||
|
@ -49,11 +49,11 @@ That's all! You can now start using the Slack slash commands.
|
|||
To create a project alias on GitLab.com for Slack integration:
|
||||
|
||||
1. Go to your project's home page.
|
||||
1. Navigate to **Settings > Integrations** (only visible on GitLab.com)
|
||||
1. On the **Integrations** page, click **Slack application**.
|
||||
1. Go to **Settings > Integrations** (only visible on GitLab.com)
|
||||
1. On the **Integrations** page, select **Slack application**.
|
||||
1. The current **Project Alias**, if any, is displayed. To edit this value,
|
||||
click **Edit**.
|
||||
1. Enter your desired alias, and click **Save changes**.
|
||||
select **Edit**.
|
||||
1. Enter your desired alias, and select **Save changes**.
|
||||
|
||||
Some Slack commands require a project alias, and fail with the following error
|
||||
if the project alias is incorrect or missing from the command:
|
||||
|
|
|
@ -32,7 +32,7 @@ on your configuration:
|
|||
If Mattermost is installed on the same server as GitLab, the configuration process can be
|
||||
done for you by GitLab.
|
||||
|
||||
Go to the Mattermost Slash Command service on your project and click **Add to Mattermost** button.
|
||||
Go to the Mattermost Slash Command service on your project and select **Add to Mattermost**.
|
||||
|
||||
## Manual configuration
|
||||
|
||||
|
@ -52,13 +52,13 @@ installations from source.
|
|||
To enable custom slash commands from the Mattermost administrator console:
|
||||
|
||||
1. Sign in to Mattermost as a user with administrator privileges.
|
||||
1. Next to your username, click the **{ellipsis_v}** **Settings** icon, and
|
||||
1. Next to your username, select the **{ellipsis_v}** **Settings** icon, and
|
||||
select **System Console**.
|
||||
1. Select **Integration Management**, and set these values to `TRUE`:
|
||||
- **Enable Custom Slash Commands**
|
||||
- **Enable integrations to override usernames**
|
||||
- **Enable integrations to override profile picture icons**
|
||||
1. Click **Save**, but do not close this browser tab, because you need it in
|
||||
1. Select **Save**, but do not close this browser tab, because you need it in
|
||||
a later step.
|
||||
|
||||
### Get configuration values from GitLab
|
||||
|
@ -85,9 +85,9 @@ the previous step:
|
|||
1. In the Mattermost tab you left open when you
|
||||
[enabled custom slash commands](#enable-custom-slash-commands), go to your
|
||||
team page.
|
||||
1. Click the **{ellipsis_v}** **Settings** icon, and select **Integrations**.
|
||||
1. Select the **{ellipsis_v}** **Settings** icon, and select **Integrations**.
|
||||
1. In the left menu, select **Slash commands**.
|
||||
1. Click **Add Slash Command**:
|
||||
1. Select **Add Slash Command**:
|
||||
|
||||
![Mattermost add command](img/mattermost_add_slash_command.png)
|
||||
1. Provide a **Display Name** and **Description** for your new command.
|
||||
|
@ -101,7 +101,7 @@ the previous step:
|
|||
[viewed configuration values](#get-configuration-values-from-gitlab).
|
||||
1. For all other values, you may use the suggestions from GitLab or use your
|
||||
preferred values.
|
||||
1. Copy the **Token** value, as you need it in a later step, and click **Done**.
|
||||
1. Copy the **Token** value, as you need it in a later step, and select **Done**.
|
||||
|
||||
### Provide the Mattermost token to GitLab
|
||||
|
||||
|
@ -116,7 +116,7 @@ provide to GitLab:
|
|||
|
||||
![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
|
||||
|
||||
1. Click **Save changes** for the changes to take effect.
|
||||
1. Select **Save changes** for the changes to take effect.
|
||||
|
||||
Your slash command can now communicate with your GitLab project.
|
||||
|
||||
|
|
|
@ -3,11 +3,7 @@
|
|||
module QA
|
||||
# Disable on staging until bulk_import_projects toggle is on by default
|
||||
# Otherwise tests running in parallel can disable feature in the middle of other test
|
||||
RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging }, quarantine: {
|
||||
only: { job: 'praefect' },
|
||||
type: :investigating,
|
||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999'
|
||||
} do
|
||||
RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging } do
|
||||
let(:source_project_with_readme) { false }
|
||||
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
|
||||
let(:admin_api_client) { Runtime::API::Client.as_admin }
|
||||
|
|
|
@ -65,7 +65,7 @@ module QA
|
|||
file_path: file_path,
|
||||
status: example.execution_result.status,
|
||||
reliable: example.metadata.key?(:reliable).to_s,
|
||||
quarantined: example.metadata.key?(:quarantine).to_s,
|
||||
quarantined: quarantined(example.metadata),
|
||||
retried: ((example.metadata[:retry_attempts] || 0) > 0).to_s,
|
||||
job_name: job_name,
|
||||
merge_request: merge_request,
|
||||
|
@ -144,6 +144,17 @@ module QA
|
|||
(!!merge_request_iid).to_s
|
||||
end
|
||||
|
||||
# Is spec quarantined
|
||||
#
|
||||
# @param [Hash] metadata
|
||||
# @return [String]
|
||||
def quarantined(metadata)
|
||||
return "false" unless metadata.key?(:quarantine)
|
||||
return "true" unless metadata[:quarantine].is_a?(Hash)
|
||||
|
||||
(!Specs::Helpers::Quarantine.quarantined_different_context?(metadata[:quarantine])).to_s
|
||||
end
|
||||
|
||||
# Print log message
|
||||
#
|
||||
# @param [Symbol] level
|
||||
|
|
|
@ -158,6 +158,23 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with context quarantined spec' do
|
||||
let(:quarantined) { 'false' }
|
||||
|
||||
it 'exports data to influxdb with correct qurantine tag' do
|
||||
run_spec do
|
||||
it(
|
||||
'spec',
|
||||
quarantine: { only: { job: 'praefect' } },
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234'
|
||||
) {}
|
||||
end
|
||||
|
||||
expect(influx_write_api).to have_received(:write).once
|
||||
expect(influx_write_api).to have_received(:write).with(data: [data])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with staging full run' do
|
||||
let(:run_type) { 'staging-full' }
|
||||
|
||||
|
|
|
@ -1,84 +1,36 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { createAlert } from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
|
||||
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
|
||||
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
|
||||
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
|
||||
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
|
||||
import { runnersData } from '../../mock_data';
|
||||
|
||||
const mockRunner = runnersData.data.runners.nodes[0];
|
||||
|
||||
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
|
||||
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/runner/sentry_utils');
|
||||
|
||||
describe('RunnerTypeCell', () => {
|
||||
describe('RunnerActionsCell', () => {
|
||||
let wrapper;
|
||||
|
||||
const mockToastShow = jest.fn();
|
||||
const runnerDeleteMutationHandler = jest.fn();
|
||||
|
||||
const findEditBtn = () => wrapper.findComponent(RunnerEditButton);
|
||||
const findRunnerPauseBtn = () => wrapper.findComponent(RunnerPauseButton);
|
||||
const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal);
|
||||
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
|
||||
const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value;
|
||||
const findDeleteBtn = () => wrapper.findComponent(RunnerDeleteButton);
|
||||
|
||||
const createComponent = (runner = {}, options) => {
|
||||
wrapper = shallowMountExtended(RunnerActionCell, {
|
||||
wrapper = shallowMountExtended(RunnerActionsCell, {
|
||||
propsData: {
|
||||
runner: {
|
||||
id: mockRunner.id,
|
||||
shortSha: mockRunner.shortSha,
|
||||
editAdminUrl: mockRunner.editAdminUrl,
|
||||
userPermissions: mockRunner.userPermissions,
|
||||
active: mockRunner.active,
|
||||
...runner,
|
||||
},
|
||||
},
|
||||
apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteMutationHandler]]),
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
GlModal: createMockDirective(),
|
||||
},
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: mockToastShow,
|
||||
},
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
runnerDeleteMutationHandler.mockResolvedValue({
|
||||
data: {
|
||||
runnerDelete: {
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockToastShow.mockReset();
|
||||
runnerDeleteMutationHandler.mockReset();
|
||||
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
|
@ -129,51 +81,10 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
|
||||
describe('Delete action', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
stubs: { RunnerDeleteModal },
|
||||
},
|
||||
);
|
||||
});
|
||||
it('Renders a compact delete button', () => {
|
||||
createComponent();
|
||||
|
||||
it('Renders delete button', () => {
|
||||
expect(findDeleteBtn().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Delete button opens delete modal', () => {
|
||||
const modalId = getBinding(findDeleteBtn().element, 'gl-modal').value;
|
||||
|
||||
expect(findRunnerDeleteModal().attributes('modal-id')).toBeDefined();
|
||||
expect(findRunnerDeleteModal().attributes('modal-id')).toBe(modalId);
|
||||
});
|
||||
|
||||
it('Delete modal shows the runner name', () => {
|
||||
expect(findRunnerDeleteModal().props('runnerName')).toBe(
|
||||
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
|
||||
);
|
||||
});
|
||||
it('The delete button does not have a loading icon', () => {
|
||||
expect(findDeleteBtn().props('loading')).toBe(false);
|
||||
expect(getTooltip(findDeleteBtn())).toBe('Delete runner');
|
||||
});
|
||||
|
||||
it('When delete mutation is called, current runners are refetched', () => {
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate');
|
||||
|
||||
findRunnerDeleteModal().vm.$emit('primary');
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: runnerDeleteMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: mockRunner.id,
|
||||
},
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getRunnersQueryName, getGroupRunnersQueryName],
|
||||
});
|
||||
expect(findDeleteBtn().props('compact')).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render the runner delete button when user cannot delete', () => {
|
||||
|
@ -185,91 +96,6 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
|
||||
expect(findDeleteBtn().exists()).toBe(false);
|
||||
expect(findRunnerDeleteModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('When delete is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
findRunnerDeleteModal().vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('The delete mutation is called correctly', () => {
|
||||
expect(runnerDeleteMutationHandler).toHaveBeenCalledTimes(1);
|
||||
expect(runnerDeleteMutationHandler).toHaveBeenCalledWith({
|
||||
input: { id: mockRunner.id },
|
||||
});
|
||||
});
|
||||
|
||||
it('The delete button has a loading icon', () => {
|
||||
expect(findDeleteBtn().props('loading')).toBe(true);
|
||||
expect(getTooltip(findDeleteBtn())).toBe('');
|
||||
});
|
||||
|
||||
it('The toast notification is shown', async () => {
|
||||
await waitForPromises();
|
||||
expect(mockToastShow).toHaveBeenCalledTimes(1);
|
||||
expect(mockToastShow).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When delete fails', () => {
|
||||
describe('On a network error', () => {
|
||||
const mockErrorMsg = 'Delete error!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerDeleteMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
|
||||
|
||||
findRunnerDeleteModal().vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(mockErrorMsg),
|
||||
component: 'RunnerActionsCell',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toast notification is not shown', () => {
|
||||
expect(mockToastShow).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('On a validation error', () => {
|
||||
const mockErrorMsg = 'Runner not found!';
|
||||
const mockErrorMsg2 = 'User not allowed!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerDeleteMutationHandler.mockResolvedValue({
|
||||
data: {
|
||||
runnerDelete: {
|
||||
errors: [mockErrorMsg, mockErrorMsg2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
findRunnerDeleteModal().vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
|
||||
component: 'RunnerActionsCell',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
233
spec/frontend/runner/components/runner_delete_button_spec.js
Normal file
233
spec/frontend/runner/components/runner_delete_button_spec.js
Normal file
|
@ -0,0 +1,233 @@
|
|||
import Vue from 'vue';
|
||||
import { GlButton, GlToast } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { createAlert } from '~/flash';
|
||||
import { I18N_DELETE_RUNNER } from '~/runner/constants';
|
||||
|
||||
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
|
||||
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
|
||||
import { runnersData } from '../mock_data';
|
||||
|
||||
const mockRunner = runnersData.data.runners.nodes[0];
|
||||
const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(GlToast);
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/runner/sentry_utils');
|
||||
|
||||
describe('RunnerDeleteButton', () => {
|
||||
let wrapper;
|
||||
let runnerDeleteHandler;
|
||||
let showToast;
|
||||
|
||||
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
|
||||
const getModal = () => getBinding(wrapper.element, 'gl-modal').value;
|
||||
const findBtn = () => wrapper.findComponent(GlButton);
|
||||
const findModal = () => wrapper.findComponent(RunnerDeleteModal);
|
||||
|
||||
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
|
||||
const { runner, ...propsData } = props;
|
||||
|
||||
wrapper = mountFn(RunnerDeleteButton, {
|
||||
propsData: {
|
||||
runner: {
|
||||
id: mockRunner.id,
|
||||
shortSha: mockRunner.shortSha,
|
||||
...runner,
|
||||
},
|
||||
...propsData,
|
||||
},
|
||||
apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]),
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
GlModal: createMockDirective(),
|
||||
},
|
||||
});
|
||||
|
||||
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show').mockImplementation(() => {});
|
||||
};
|
||||
|
||||
const clickOkAndWait = async () => {
|
||||
findModal().vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
runnerDeleteHandler = jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
runnerDelete: {
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('Displays a delete button without an icon', () => {
|
||||
expect(findBtn().props()).toMatchObject({
|
||||
loading: false,
|
||||
icon: '',
|
||||
});
|
||||
expect(findBtn().classes('btn-icon')).toBe(false);
|
||||
expect(findBtn().text()).toBe(I18N_DELETE_RUNNER);
|
||||
});
|
||||
|
||||
it('Displays a modal with the runner name', () => {
|
||||
expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`);
|
||||
});
|
||||
|
||||
it('Displays a modal when clicked', () => {
|
||||
const modalId = `delete-runner-modal-${mockRunnerId}`;
|
||||
|
||||
expect(getModal()).toBe(modalId);
|
||||
expect(findModal().attributes('modal-id')).toBe(modalId);
|
||||
});
|
||||
|
||||
it('Does not display redundant text for screen readers', () => {
|
||||
expect(findBtn().attributes('aria-label')).toBe(undefined);
|
||||
});
|
||||
|
||||
describe(`Before the delete button is clicked`, () => {
|
||||
it('The mutation has not been called', () => {
|
||||
expect(runnerDeleteHandler).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Immediately after the delete button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
findModal().vm.$emit('primary');
|
||||
});
|
||||
|
||||
it('The button has a loading state', async () => {
|
||||
expect(findBtn().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('The stale tooltip is removed', async () => {
|
||||
expect(getTooltip()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After clicking on the delete button', () => {
|
||||
beforeEach(async () => {
|
||||
await clickOkAndWait();
|
||||
});
|
||||
|
||||
it('The mutation to delete is called', async () => {
|
||||
expect(runnerDeleteHandler).toHaveBeenCalledTimes(1);
|
||||
expect(runnerDeleteHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: mockRunner.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('The user is notified', async () => {
|
||||
expect(showToast).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When update fails', () => {
|
||||
describe('On a network error', () => {
|
||||
const mockErrorMsg = 'Update error!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerDeleteHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
|
||||
|
||||
await clickOkAndWait();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(mockErrorMsg),
|
||||
component: 'RunnerDeleteButton',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('On a validation error', () => {
|
||||
const mockErrorMsg = 'Runner not found!';
|
||||
const mockErrorMsg2 = 'User not allowed!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerDeleteHandler.mockResolvedValueOnce({
|
||||
data: {
|
||||
runnerDelete: {
|
||||
errors: [mockErrorMsg, mockErrorMsg2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await clickOkAndWait();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
|
||||
component: 'RunnerDeleteButton',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When displaying a compact button for an active runner', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
runner: {
|
||||
active: true,
|
||||
},
|
||||
compact: true,
|
||||
},
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
});
|
||||
|
||||
it('Displays no text', () => {
|
||||
expect(findBtn().text()).toBe('');
|
||||
expect(findBtn().classes('btn-icon')).toBe(true);
|
||||
});
|
||||
|
||||
it('Display correctly for screen readers', () => {
|
||||
expect(findBtn().attributes('aria-label')).toBe(I18N_DELETE_RUNNER);
|
||||
expect(getTooltip()).toBe(I18N_DELETE_RUNNER);
|
||||
});
|
||||
|
||||
describe('Immediately after the button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
findModal().vm.$emit('primary');
|
||||
});
|
||||
|
||||
it('The button has a loading state', async () => {
|
||||
expect(findBtn().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('The stale tooltip is removed', async () => {
|
||||
expect(getTooltip()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import RunnerList from '~/runner/components/runner_list.vue';
|
||||
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
|
||||
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
|
||||
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
|
||||
import { runnersData } from '../mock_data';
|
||||
|
||||
const mockRunners = runnersData.data.runners.nodes;
|
||||
|
@ -94,7 +95,7 @@ describe('RunnerList', () => {
|
|||
|
||||
expect(actions.findComponent(RunnerEditButton).exists()).toBe(true);
|
||||
expect(actions.findComponent(RunnerPauseButton).exists()).toBe(true);
|
||||
expect(actions.findByTestId('delete-runner').exists()).toBe(true);
|
||||
expect(actions.findComponent(RunnerDeleteButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('Table data formatting', () => {
|
||||
|
|
Loading…
Reference in a new issue