Commit 1a9eee3d authored by Evan Read's avatar Evan Read

Migrate environment dropdown

Also refactor tests and fix grammar in UI text
parent b30b6f5e
<script>
import { GlButton, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
......@@ -9,7 +9,8 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
GlLoadingIcon,
},
......@@ -35,7 +36,7 @@ export default {
if (action.scheduledAt) {
const confirmationMessage = sprintf(
s__(
"DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
......@@ -67,40 +68,32 @@ export default {
};
</script>
<template>
<div class="btn-group" role="group">
<gl-button
v-gl-tooltip
:title="title"
:aria-label="title"
:disabled="isLoading"
class="dropdown dropdown-new js-environment-actions-dropdown"
data-container="body"
data-toggle="dropdown"
data-testid="environment-actions-button"
<gl-dropdown
v-gl-tooltip
:title="title"
:aria-label="title"
:disabled="isLoading"
right
data-container="body"
data-testid="environment-actions-button"
>
<template #button-content>
<gl-icon name="play" />
<gl-icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" />
</template>
<gl-dropdown-item
v-for="(action, i) in actions"
:key="i"
:disabled="isActionDisabled(action)"
data-testid="manual-action-link"
@click="onClickAction(action)"
>
<span>
<gl-icon name="play" />
<gl-icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" />
<span class="gl-flex-fill-1">{{ action.name }}</span>
<span v-if="action.scheduledAt" class="gl-text-gray-500 float-right">
<gl-icon name="clock" />
{{ remainingTime(action) }}
</span>
</gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="(action, i) in actions" :key="i" class="gl-display-flex">
<gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
variant="link"
class="js-manual-action-link gl-flex-fill-1"
@click="onClickAction(action)"
>
<span class="gl-flex-fill-1">{{ action.name }}</span>
<span v-if="action.scheduledAt" class="text-secondary float-right">
<gl-icon name="clock" />
{{ remainingTime(action) }}
</span>
</gl-button>
</li>
</ul>
</div>
</gl-dropdown-item>
</gl-dropdown>
</template>
......@@ -32,7 +32,7 @@ export default {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
"DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
......
......@@ -15,6 +15,14 @@ RSpec.describe 'Environments page', :js do
sign_in(user)
end
def action_link_selector
'[data-testid="manual-action-link"]'
end
def actions_button_selector
'[data-testid="environment-actions-button"]'
end
context 'when an environment is protected and user has access to it' do
before do
create(:protected_environment,
......@@ -45,10 +53,9 @@ RSpec.describe 'Environments page', :js do
end
it 'shows an enabled play button' do
find('.js-environment-actions-dropdown').click
play_button = %q{button.js-manual-action-link}
find(actions_button_selector).click
expect(page).to have_selector(play_button)
expect(page).to have_selector(action_link_selector)
end
it 'shows a stop button' do
......@@ -129,8 +136,8 @@ RSpec.describe 'Environments page', :js do
end
it 'show a disabled play button' do
find('.js-environment-actions-dropdown').click
disabled_play_button = %q{button.js-manual-action-link.disabled}
find(actions_button_selector).click
disabled_play_button = %Q{#{action_link_selector}[disabled="disabled"]}
expect(page).to have_selector(disabled_play_button)
end
......
......@@ -8743,7 +8743,7 @@ msgstr ""
msgid "Delayed Project Deletion (%{adjourned_deletion})"
msgstr ""
msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes."
msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes."
msgstr ""
msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes."
......
......@@ -12,8 +12,16 @@ RSpec.describe 'Environments page', :js do
sign_in(user)
end
def actions_button_selector
'[data-testid="environment-actions-button"]'
end
def action_link_selector
'[data-testid="manual-action-link"]'
end
def stop_button_selector
%q{button[title="Stop environment"]}
'button[title="Stop environment"]'
end
describe 'page tabs' do
......@@ -187,18 +195,17 @@ RSpec.describe 'Environments page', :js do
end
it 'shows a play button' do
find('.js-environment-actions-dropdown').click
find(actions_button_selector).click
expect(page).to have_content(action.name)
end
it 'allows to play a manual action', :js do
expect(action).to be_manual
find('.js-environment-actions-dropdown').click
find(actions_button_selector).click
expect(page).to have_content(action.name)
expect { find('.js-manual-action-link').click }
expect { find(action_link_selector).click }
.not_to change { Ci::Pipeline.count }
end
......@@ -301,11 +308,11 @@ RSpec.describe 'Environments page', :js do
end
it 'has a dropdown for actionable jobs' do
expect(page).to have_selector('.dropdown-new.btn.btn-default [data-testid="play-icon"]')
expect(page).to have_selector("#{actions_button_selector} [data-testid=\"play-icon\"]")
end
it "has link to the delayed job's action" do
find('.js-environment-actions-dropdown').click
find(actions_button_selector).click
expect(page).to have_button('delayed job')
expect(page).to have_content(/\d{2}:\d{2}:\d{2}/)
......@@ -320,7 +327,7 @@ RSpec.describe 'Environments page', :js do
end
it "shows 00:00:00 as the remaining time" do
find('.js-environment-actions-dropdown').click
find(actions_button_selector).click
expect(page).to have_content("00:00:00")
end
......@@ -328,8 +335,8 @@ RSpec.describe 'Environments page', :js do
context 'when user played a delayed job immediately' do
before do
find('.js-environment-actions-dropdown').click
page.accept_confirm { click_button('delayed job') }
find(actions_button_selector).click
accept_confirm { find(action_link_selector).click }
wait_for_requests
end
......
import { shallowMount } from '@vue/test-utils';
import { shallowMount, mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
const scheduledJobAction = {
name: 'scheduled action',
playPath: `${TEST_HOST}/scheduled/job/action`,
playable: true,
scheduledAt: '2063-04-05T00:42:00Z',
};
const expiredJobAction = {
name: 'expired action',
playPath: `${TEST_HOST}/expired/job/action`,
playable: true,
scheduledAt: '2018-10-05T08:23:00Z',
};
describe('EnvironmentActions Component', () => {
let vm;
let wrapper;
const findEnvironmentActionsButton = () => vm.find('[data-testid="environment-actions-button"]');
const findEnvironmentActionsButton = () =>
wrapper.find('[data-testid="environment-actions-button"]');
beforeEach(() => {
vm = shallowMount(EnvironmentActions, {
propsData: { actions: [] },
function createComponent(props, { mountFn = shallowMount } = {}) {
wrapper = mountFn(EnvironmentActions, {
propsData: { actions: [], ...props },
directives: {
GlTooltip: createMockDirective(),
},
});
});
}
function createComponentWithScheduledJobs(opts = {}) {
return createComponent({ actions: [scheduledJobAction, expiredJobAction] }, opts);
}
const findDropdownItem = action => {
const buttons = wrapper.findAll(GlDropdownItem);
return buttons.filter(button => button.text().startsWith(action.name)).at(0);
};
afterEach(() => {
vm.destroy();
wrapper.destroy();
wrapper = null;
});
it('should render a dropdown button with 2 icons', () => {
expect(vm.find('.dropdown-new').findAll(GlIcon).length).toBe(2);
createComponent({}, { mountFn: mount });
expect(wrapper.find(GlDropdown).findAll(GlIcon).length).toBe(2);
});
it('should render a dropdown button with aria-label description', () => {
expect(vm.find('.dropdown-new').attributes('aria-label')).toEqual('Deploy to...');
createComponent();
expect(wrapper.find(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
});
it('should render a tooltip', () => {
createComponent();
const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
});
describe('is loading', () => {
beforeEach(() => {
vm.setData({ isLoading: true });
});
it('should render a dropdown button with a loading icon', () => {
expect(vm.findAll(GlLoadingIcon).length).toBe(1);
});
});
describe('manual actions', () => {
const actions = [
{
......@@ -64,68 +82,71 @@ describe('EnvironmentActions Component', () => {
];
beforeEach(() => {
vm.setProps({ actions });
createComponent({ actions });
});
it('should render a dropdown with the provided list of actions', () => {
expect(vm.findAll('.dropdown-menu li').length).toEqual(actions.length);
expect(wrapper.findAll(GlDropdownItem)).toHaveLength(actions.length);
});
it("should render a disabled action when it's not playable", () => {
expect(vm.find('.dropdown-menu li:last-child gl-button-stub').props('disabled')).toBe(true);
const dropdownItems = wrapper.findAll(GlDropdownItem);
const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
expect(lastDropdownItem.attributes('disabled')).toBe('true');
});
});
describe('scheduled jobs', () => {
const scheduledJobAction = {
name: 'scheduled action',
playPath: `${TEST_HOST}/scheduled/job/action`,
playable: true,
scheduledAt: '2063-04-05T00:42:00Z',
};
const expiredJobAction = {
name: 'expired action',
playPath: `${TEST_HOST}/expired/job/action`,
playable: true,
scheduledAt: '2018-10-05T08:23:00Z',
};
const findDropdownItem = action => {
const buttons = vm.findAll('.dropdown-menu li gl-button-stub');
return buttons.filter(button => button.text().startsWith(action.name)).at(0);
let emitSpy;
const clickAndConfirm = async ({ confirm = true } = {}) => {
jest.spyOn(window, 'confirm').mockImplementation(() => confirm);
findDropdownItem(scheduledJobAction).vm.$emit('click');
await wrapper.vm.$nextTick();
};
beforeEach(() => {
emitSpy = jest.fn();
eventHub.$on('postAction', emitSpy);
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
vm.setProps({ actions: [scheduledJobAction, expiredJobAction] });
});
it('emits postAction event after confirming', () => {
const emitSpy = jest.fn();
eventHub.$on('postAction', emitSpy);
jest.spyOn(window, 'confirm').mockImplementation(() => true);
describe('when postAction event is confirmed', () => {
beforeEach(async () => {
createComponentWithScheduledJobs({ mountFn: mount });
clickAndConfirm();
});
findDropdownItem(scheduledJobAction).vm.$emit('click');
it('emits postAction event', () => {
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
});
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
it('should render a dropdown button with a loading icon', () => {
expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
});
});
it('does not emit postAction event if confirmation is cancelled', () => {
const emitSpy = jest.fn();
eventHub.$on('postAction', emitSpy);
jest.spyOn(window, 'confirm').mockImplementation(() => false);
findDropdownItem(scheduledJobAction).vm.$emit('click');
describe('when postAction event is denied', () => {
beforeEach(() => {
createComponentWithScheduledJobs({ mountFn: mount });
clickAndConfirm({ confirm: false });
});
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalled();
it('does not emit postAction event if confirmation is cancelled', () => {
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalled();
});
});
it('displays the remaining time in the dropdown', () => {
createComponentWithScheduledJobs();
expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
});
it('displays 00:00:00 for expired jobs in the dropdown', () => {
createComponentWithScheduledJobs();
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment