summaryrefslogtreecommitdiff
path: root/spec/frontend/environments
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/environments')
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js10
-rw-r--r--spec/frontend/environments/canary_update_modal_spec.js10
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js36
-rw-r--r--spec/frontend/environments/delete_environment_modal_spec.js6
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js4
-rw-r--r--spec/frontend/environments/deploy_freeze_alert_spec.js111
-rw-r--r--spec/frontend/environments/edit_environment_spec.js5
-rw-r--r--spec/frontend/environments/empty_state_spec.js69
-rw-r--r--spec/frontend/environments/enable_review_app_modal_spec.js6
-rw-r--r--spec/frontend/environments/environment_actions_spec.js115
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_actions_spec.js119
-rw-r--r--spec/frontend/environments/environment_details/deployments_table_spec.js58
-rw-r--r--spec/frontend/environments/environment_details/index_spec.js (renamed from spec/frontend/environments/environment_details/page_spec.js)60
-rw-r--r--spec/frontend/environments/environment_external_url_spec.js33
-rw-r--r--spec/frontend/environments/environment_folder_spec.js4
-rw-r--r--spec/frontend/environments/environment_form_spec.js20
-rw-r--r--spec/frontend/environments/environment_item_spec.js34
-rw-r--r--spec/frontend/environments/environment_pin_spec.js12
-rw-r--r--spec/frontend/environments/environment_stop_spec.js2
-rw-r--r--spec/frontend/environments/environment_table_spec.js8
-rw-r--r--spec/frontend/environments/environments_app_spec.js16
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js57
-rw-r--r--spec/frontend/environments/environments_folder_view_spec.js1
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js18
-rw-r--r--spec/frontend/environments/graphql/mock_data.js109
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js182
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap34
-rw-r--r--spec/frontend/environments/kubernetes_agent_info_spec.js124
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js131
-rw-r--r--spec/frontend/environments/kubernetes_pods_spec.js114
-rw-r--r--spec/frontend/environments/kubernetes_summary_spec.js115
-rw-r--r--spec/frontend/environments/kubernetes_tabs_spec.js168
-rw-r--r--spec/frontend/environments/mock_data.js3
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js127
-rw-r--r--spec/frontend/environments/new_environment_spec.js5
-rw-r--r--spec/frontend/environments/stop_stale_environments_modal_spec.js10
36 files changed, 1596 insertions, 340 deletions
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index 340740e6499..e0247731b63 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -23,7 +23,7 @@ describe('/environments/components/canary_ingress.vue', () => {
...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
...options,
});
@@ -33,14 +33,6 @@ describe('/environments/components/canary_ingress.vue', () => {
createComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
describe('stable weight', () => {
let stableWeightDropdown;
diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js
index 31b1770da59..4fa7b34d817 100644
--- a/spec/frontend/environments/canary_update_modal_spec.js
+++ b/spec/frontend/environments/canary_update_modal_spec.js
@@ -30,14 +30,6 @@ describe('/environments/components/canary_update_modal.vue', () => {
modal = wrapper.findComponent(GlModal);
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
beforeEach(() => {
createComponent();
});
@@ -47,7 +39,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
modalId: 'confirm-canary-change',
actionPrimary: {
text: 'Change ratio',
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
},
actionCancel: { text: 'Cancel' },
});
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index 2163814528a..d6601447cff 100644
--- a/spec/frontend/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import { trimText } from 'helpers/text_helper';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import eventHub from '~/environments/event_hub';
describe('Confirm Rollback Modal Component', () => {
@@ -53,6 +54,8 @@ describe('Confirm Rollback Modal Component', () => {
});
};
+ const findModal = () => component.findComponent(GlModal);
+
describe.each`
hasMultipleCommits | environmentData | retryUrl | primaryPropsAttrs
${true} | ${envWithLastDeployment} | ${null} | ${[{ variant: 'danger' }]}
@@ -73,7 +76,7 @@ describe('Confirm Rollback Modal Component', () => {
hasMultipleCommits,
retryUrl,
});
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
@@ -92,7 +95,7 @@ describe('Confirm Rollback Modal Component', () => {
hasMultipleCommits,
});
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
@@ -110,7 +113,7 @@ describe('Confirm Rollback Modal Component', () => {
});
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
modal.vm.$emit('ok');
expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', env);
@@ -155,7 +158,7 @@ describe('Confirm Rollback Modal Component', () => {
},
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(trimText(modal.text())).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
@@ -177,7 +180,7 @@ describe('Confirm Rollback Modal Component', () => {
},
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
@@ -201,7 +204,7 @@ describe('Confirm Rollback Modal Component', () => {
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
@@ -220,7 +223,7 @@ describe('Confirm Rollback Modal Component', () => {
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
modal.vm.$emit('ok');
await nextTick();
@@ -231,6 +234,25 @@ describe('Confirm Rollback Modal Component', () => {
expect.anything(),
);
});
+
+ it('should emit the "rollback" event when "ok" is clicked', async () => {
+ const env = { ...environmentData, isLastDeployment: true };
+
+ createComponent(
+ {
+ environment: env,
+ hasMultipleCommits,
+ graphql: true,
+ },
+ { apolloProvider },
+ );
+
+ const modal = findModal();
+ modal.vm.$emit('ok');
+
+ await waitForPromises();
+ expect(component.emitted('rollback')).toEqual([[]]);
+ });
},
);
});
diff --git a/spec/frontend/environments/delete_environment_modal_spec.js b/spec/frontend/environments/delete_environment_modal_spec.js
index cc18bf754eb..96f6ce52a9c 100644
--- a/spec/frontend/environments/delete_environment_modal_spec.js
+++ b/spec/frontend/environments/delete_environment_modal_spec.js
@@ -6,10 +6,10 @@ import { s__, sprintf } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { resolvedEnvironment } from './graphql/mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
describe('~/environments/components/delete_environment_modal.vue', () => {
@@ -67,7 +67,7 @@ describe('~/environments/components/delete_environment_modal.vue', () => {
);
});
- it('should flash a message on error', async () => {
+ it('should alert a message on error', async () => {
createComponent({ apolloProvider: mockApollo });
deleteResolver.mockRejectedValue();
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index 73a366457fb..f50efada91a 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -61,7 +61,7 @@ describe('Deploy Board', () => {
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
- expect(icon.props('name')).toBe('question');
+ expect(icon.props('name')).toBe('question-o');
});
it('renders the canary weight selector', () => {
@@ -116,7 +116,7 @@ describe('Deploy Board', () => {
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
- expect(icon.props('name')).toBe('question');
+ expect(icon.props('name')).toBe('question-o');
});
it('renders the canary weight selector', () => {
diff --git a/spec/frontend/environments/deploy_freeze_alert_spec.js b/spec/frontend/environments/deploy_freeze_alert_spec.js
new file mode 100644
index 00000000000..b7202253e61
--- /dev/null
+++ b/spec/frontend/environments/deploy_freeze_alert_spec.js
@@ -0,0 +1,111 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeployFreezeAlert from '~/environments/components/deploy_freeze_alert.vue';
+import deployFreezesQuery from '~/environments/graphql/queries/deploy_freezes.query.graphql';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+
+const ENVIRONMENT_NAME = 'staging';
+
+Vue.use(VueApollo);
+describe('~/environments/components/deploy_freeze_alert.vue', () => {
+ let wrapper;
+
+ const createWrapper = (deployFreezes = []) => {
+ const mockApollo = createMockApollo([
+ [
+ deployFreezesQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ environment: {
+ id: '1',
+ __typename: 'Environment',
+ deployFreezes,
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+ wrapper = mountExtended(DeployFreezeAlert, {
+ apolloProvider: mockApollo,
+ provide: {
+ projectFullPath: 'gitlab-org/gitlab',
+ },
+ propsData: {
+ name: ENVIRONMENT_NAME,
+ },
+ });
+ };
+
+ describe('with deploy freezes', () => {
+ let deployFreezes;
+ let alert;
+
+ beforeEach(async () => {
+ deployFreezes = [
+ {
+ __typename: 'CiFreezePeriod',
+ startTime: new Date('2020-02-01'),
+ endTime: new Date('2020-02-02'),
+ },
+ {
+ __typename: 'CiFreezePeriod',
+ startTime: new Date('2020-01-01'),
+ endTime: new Date('2020-01-02'),
+ },
+ ];
+
+ createWrapper(deployFreezes);
+
+ await waitForPromises();
+
+ alert = wrapper.findComponent(GlAlert);
+ });
+
+ it('shows an alert', () => {
+ expect(alert.exists()).toBe(true);
+ });
+
+ it('shows the start time of the most recent freeze period', () => {
+ expect(alert.text()).toContain(`from ${formatDate(deployFreezes[1].startTime)}`);
+ });
+
+ it('shows the end time of the most recent freeze period', () => {
+ expect(alert.text()).toContain(`to ${formatDate(deployFreezes[1].endTime)}`);
+ });
+
+ it('shows a link to the docs', () => {
+ const link = alert.findComponent(GlLink);
+ expect(link.attributes('href')).toBe(
+ '/help/user/project/releases/index#prevent-unintentional-releases-by-setting-a-deploy-freeze',
+ );
+ expect(link.text()).toBe('deploy freeze documentation');
+ });
+ });
+
+ describe('without deploy freezes', () => {
+ let deployFreezes;
+ let alert;
+
+ beforeEach(async () => {
+ deployFreezes = [];
+
+ createWrapper(deployFreezes);
+
+ await waitForPromises();
+
+ alert = wrapper.findComponent(GlAlert);
+ });
+
+ it('does not show an alert', () => {
+ expect(alert.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index fb1a8b8c00a..34f338fabe6 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
@@ -37,7 +37,6 @@ describe('~/environments/components/edit.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const findNameInput = () => wrapper.findByLabelText('Name');
diff --git a/spec/frontend/environments/empty_state_spec.js b/spec/frontend/environments/empty_state_spec.js
index 02cf2dc3c68..593200859e4 100644
--- a/spec/frontend/environments/empty_state_spec.js
+++ b/spec/frontend/environments/empty_state_spec.js
@@ -11,12 +11,17 @@ describe('~/environments/components/empty_state.vue', () => {
const findNewEnvironmentLink = () =>
wrapper.findByRole('link', {
- name: s__('Environments|New environment'),
+ name: s__('Environments|Create an environment'),
});
const findDocsLink = () =>
wrapper.findByRole('link', {
- name: s__('Environments|How do I create an environment?'),
+ name: 'Learn more',
+ });
+
+ const finfEnablingReviewButton = () =>
+ wrapper.findByRole('button', {
+ name: s__('Environments|Enable review apps'),
});
const createWrapper = ({ propsData = {} } = {}) =>
@@ -29,42 +34,44 @@ describe('~/environments/components/empty_state.vue', () => {
provide: { newEnvironmentPath: NEW_PATH },
});
- afterEach(() => {
- wrapper.destroy();
- });
+ describe('without search term', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
- it('shows an empty state for available environments', () => {
- wrapper = createWrapper();
+ it('shows an empty state environments', () => {
+ const title = wrapper.findByRole('heading', {
+ name: s__('Environments|Get started with environments'),
+ });
- const title = wrapper.findByRole('heading', {
- name: s__("Environments|You don't have any environments."),
+ expect(title.exists()).toBe(true);
});
- expect(title.exists()).toBe(true);
- });
-
- it('shows an empty state for stopped environments', () => {
- wrapper = createWrapper({ propsData: { scope: ENVIRONMENTS_SCOPE.STOPPED } });
+ it('shows a link to the the help path', () => {
+ const link = findDocsLink();
- const title = wrapper.findByRole('heading', {
- name: s__("Environments|You don't have any stopped environments."),
+ expect(link.attributes('href')).toBe(HELP_PATH);
});
- expect(title.exists()).toBe(true);
- });
+ it('shows a link to creating a new environment', () => {
+ const link = findNewEnvironmentLink();
- it('shows a link to the the help path', () => {
- wrapper = createWrapper();
+ expect(link.attributes('href')).toBe(NEW_PATH);
+ });
- const link = findDocsLink();
+ it('shows a button to enable review apps', () => {
+ const button = finfEnablingReviewButton();
- expect(link.attributes('href')).toBe(HELP_PATH);
- });
+ expect(button.exists()).toBe(true);
+ });
+
+ it('should emit enable review', () => {
+ const button = finfEnablingReviewButton();
- it('hides a link to creating a new environment', () => {
- const link = findNewEnvironmentLink();
+ button.vm.$emit('click');
- expect(link.exists()).toBe(false);
+ expect(wrapper.emitted('enable-review')).toBeDefined();
+ });
});
describe('with search term', () => {
@@ -90,10 +97,16 @@ describe('~/environments/components/empty_state.vue', () => {
expect(link.exists()).toBe(false);
});
- it('shows a link to create a new environment', () => {
+ it('hide a link to create a new environment', () => {
const link = findNewEnvironmentLink();
- expect(link.attributes('href')).toBe(NEW_PATH);
+ expect(link.exists()).toBe(false);
+ });
+
+ it('hide a button to enable review apps', () => {
+ const button = finfEnablingReviewButton();
+
+ expect(button.exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/environments/enable_review_app_modal_spec.js b/spec/frontend/environments/enable_review_app_modal_spec.js
index 7939bd600dc..f5571609931 100644
--- a/spec/frontend/environments/enable_review_app_modal_spec.js
+++ b/spec/frontend/environments/enable_review_app_modal_spec.js
@@ -10,7 +10,7 @@ jest.mock('lodash/uniqueId', () => (x) => `${x}77`);
const EXPECTED_COPY_PRE_ID = 'enable-review-app-copy-string-77';
-describe('Enable Review App Modal', () => {
+describe('Enable Review Apps Modal', () => {
let wrapper;
let modal;
@@ -18,10 +18,6 @@ describe('Enable Review App Modal', () => {
const findInstructionAt = (i) => wrapper.findAll('ol li').at(i);
const findCopyString = () => wrapper.find(`#${EXPECTED_COPY_PRE_ID}`);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders the modal', () => {
beforeEach(() => {
wrapper = extendedWrapper(
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 48483152f7a..b7e192839da 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,14 +1,8 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
-import eventHub from '~/environments/event_hub';
-import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import createMockApollo from 'helpers/mock_apollo_helper';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -29,15 +23,9 @@ const expiredJobAction = {
describe('EnvironmentActions Component', () => {
let wrapper;
- const findEnvironmentActionsButton = () =>
- wrapper.find('[data-testid="environment-actions-button"]');
-
- function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
- wrapper = mountFn(EnvironmentActions, {
+ function createComponent(props, { options = {} } = {}) {
+ wrapper = mount(EnvironmentActions, {
propsData: { actions: [], ...props },
- directives: {
- GlTooltip: createMockDirective(),
- },
...options,
});
}
@@ -46,30 +34,26 @@ describe('EnvironmentActions Component', () => {
return createComponent({ actions: [scheduledJobAction, expiredJobAction] }, opts);
}
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findDropdownItem = (action) => {
- const buttons = wrapper.findAllComponents(GlDropdownItem);
- return buttons.filter((button) => button.text().startsWith(action.name)).at(0);
+ const items = findDropdownItems();
+ return items.filter((item) => item.text().startsWith(action.name)).at(0);
};
afterEach(() => {
- wrapper.destroy();
confirmAction.mockReset();
});
it('should render a dropdown button with 2 icons', () => {
- createComponent({}, { mountFn: mount });
- expect(wrapper.findComponent(GlDropdown).findAllComponents(GlIcon).length).toBe(2);
- });
-
- it('should render a dropdown button with aria-label description', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
+ expect(wrapper.findComponent(GlDisclosureDropdown).findAllComponents(GlIcon).length).toBe(2);
});
- it('should render a tooltip', () => {
+ it('should render a dropdown button with aria-label description', () => {
createComponent();
- const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip');
- expect(tooltip).toBeDefined();
+ expect(wrapper.findComponent(GlDisclosureDropdown).attributes('aria-label')).toBe(
+ 'Deploy to...',
+ );
});
describe('manual actions', () => {
@@ -94,96 +78,31 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown with the provided list of actions', () => {
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(actions.length);
+ expect(findDropdownItems()).toHaveLength(actions.length);
});
it("should render a disabled action when it's not playable", () => {
- const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
+ const dropdownItems = findDropdownItems();
const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
- expect(lastDropdownItem.attributes('disabled')).toBe('true');
+ expect(lastDropdownItem.find('button').attributes('disabled')).toBeDefined();
});
});
describe('scheduled jobs', () => {
- let emitSpy;
-
- const clickAndConfirm = async ({ confirm = true } = {}) => {
- confirmAction.mockResolvedValueOnce(confirm);
-
- findDropdownItem(scheduledJobAction).vm.$emit('click');
- await nextTick();
- };
-
beforeEach(() => {
- emitSpy = jest.fn();
- eventHub.$on('postAction', emitSpy);
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
});
- describe('when postAction event is confirmed', () => {
- beforeEach(async () => {
- createComponentWithScheduledJobs({ mountFn: mount });
- clickAndConfirm();
- });
-
- it('emits postAction event', () => {
- expect(confirmAction).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
- });
-
- it('should render a dropdown button with a loading icon', () => {
- expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
- });
- });
-
- describe('when postAction event is denied', () => {
- beforeEach(async () => {
- createComponentWithScheduledJobs({ mountFn: mount });
- clickAndConfirm({ confirm: false });
- });
-
- it('does not emit postAction event if confirmation is cancelled', () => {
- expect(confirmAction).toHaveBeenCalled();
- expect(emitSpy).not.toHaveBeenCalled();
- });
- });
-
it('displays the remaining time in the dropdown', () => {
+ confirmAction.mockResolvedValueOnce(true);
createComponentWithScheduledJobs();
expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
});
it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ confirmAction.mockResolvedValueOnce(true);
createComponentWithScheduledJobs();
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
-
- describe('graphql', () => {
- Vue.use(VueApollo);
-
- const action = {
- name: 'bar',
- play_path: 'https://gitlab.com/play',
- };
-
- let mockApollo;
-
- beforeEach(() => {
- mockApollo = createMockApollo();
- createComponent(
- { actions: [action], graphql: true },
- { options: { apolloProvider: mockApollo } },
- );
- });
-
- it('should trigger a graphql mutation on click', () => {
- jest.spyOn(mockApollo.defaultClient, 'mutate');
- findDropdownItem(action).vm.$emit('click');
- expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
- mutation: actionMutation,
- variables: { action },
- });
- });
- });
});
diff --git a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
index 725c8c6479e..a0eb4c494e6 100644
--- a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
@@ -1,8 +1,15 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButton } from '@gitlab/ui';
import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { translations } from '~/environments/environment_details/constants';
import ActionsComponent from '~/environments/components/environment_actions.vue';
describe('~/environments/environment_details/components/deployment_actions.vue', () => {
+ Vue.use(VueApollo);
let wrapper;
const actionsData = [
@@ -14,34 +21,116 @@ describe('~/environments/environment_details/components/deployment_actions.vue',
},
];
- const createWrapper = ({ actions }) => {
+ const rollbackData = {
+ id: '123',
+ name: 'enironment-name',
+ lastDeployment: {
+ commit: {
+ shortSha: 'abcd1234',
+ },
+ isLast: true,
+ },
+ retryUrl: 'deployment/retry',
+ };
+
+ const mockSetEnvironmentToRollback = jest.fn();
+ const mockResolvers = {
+ Mutation: {
+ setEnvironmentToRollback: mockSetEnvironmentToRollback,
+ },
+ };
+ const createWrapper = ({ actions, rollback, approvalEnvironment }) => {
+ const mockApollo = createMockApollo([], mockResolvers);
return mountExtended(DeploymentActions, {
+ apolloProvider: mockApollo,
+ provide: {
+ projectPath: 'fullProjectPath',
+ },
propsData: {
actions,
+ rollback,
+ approvalEnvironment,
},
});
};
- describe('when there is no actions provided', () => {
- beforeEach(() => {
- wrapper = createWrapper({ actions: [] });
+ const findRollbackButton = () => wrapper.findComponent(GlButton);
+
+ describe('deployment actions', () => {
+ describe('when there is no actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: [] });
+ });
+
+ it('should not render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(false);
+ });
});
- it('should not render actions component', () => {
- const actionsComponent = wrapper.findComponent(ActionsComponent);
- expect(actionsComponent.exists()).toBe(false);
+ describe('when there are actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: actionsData });
+ });
+
+ it('should render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(true);
+ expect(actionsComponent.props().actions).toBe(actionsData);
+ });
});
});
- describe('when there are actions provided', () => {
- beforeEach(() => {
- wrapper = createWrapper({ actions: actionsData });
+ describe('rollback action', () => {
+ describe('when there is no rollback data available', () => {
+ it('should not show a rollback button', () => {
+ wrapper = createWrapper({ actions: [] });
+ const button = findRollbackButton();
+ expect(button.exists()).toBe(false);
+ });
});
- it('should render actions component', () => {
- const actionsComponent = wrapper.findComponent(ActionsComponent);
- expect(actionsComponent.exists()).toBe(true);
- expect(actionsComponent.props().actions).toBe(actionsData);
- });
+ describe.each([
+ { isLast: true, buttonTitle: translations.redeployButtonTitle, icon: 'repeat' },
+ { isLast: false, buttonTitle: translations.rollbackButtonTitle, icon: 'redo' },
+ ])(
+ `when there is a rollback data available and the deployment isLast=$isLast`,
+ ({ isLast, buttonTitle, icon }) => {
+ let rollback;
+ beforeEach(() => {
+ const lastDeployment = { ...rollbackData.lastDeployment, isLast };
+ rollback = { ...rollbackData };
+ rollback.lastDeployment = lastDeployment;
+ wrapper = createWrapper({ actions: [], rollback });
+ });
+
+ it('should show the rollback button', () => {
+ const button = findRollbackButton();
+ expect(button.exists()).toBe(true);
+ });
+
+ it(`the rollback button should have "${icon}" icon`, () => {
+ const button = findRollbackButton();
+ expect(button.props().icon).toBe(icon);
+ });
+
+ it(`the rollback button should have "${buttonTitle}" title`, () => {
+ const button = findRollbackButton();
+ expect(button.attributes().title).toBe(buttonTitle);
+ });
+
+ it(`the rollback button click should send correct mutation`, async () => {
+ const button = findRollbackButton();
+ button.vm.$emit('click');
+ await waitForPromises();
+ expect(mockSetEnvironmentToRollback).toHaveBeenCalledWith(
+ expect.anything(),
+ { environment: rollback },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/environments/environment_details/deployments_table_spec.js b/spec/frontend/environments/environment_details/deployments_table_spec.js
new file mode 100644
index 00000000000..7dad5617383
--- /dev/null
+++ b/spec/frontend/environments/environment_details/deployments_table_spec.js
@@ -0,0 +1,58 @@
+import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Commit from '~/vue_shared/components/commit.vue';
+import DeploymentStatusLink from '~/environments/environment_details/components/deployment_status_link.vue';
+import DeploymentJob from '~/environments/environment_details/components/deployment_job.vue';
+import DeploymentTriggerer from '~/environments/environment_details/components/deployment_triggerer.vue';
+import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
+import DeploymentsTable from '~/environments/environment_details/deployments_table.vue';
+import { convertToDeploymentTableRow } from '~/environments/helpers/deployment_data_transformation_helper';
+
+const { environment } = resolvedEnvironmentDetails.data.project;
+const deployments = environment.deployments.nodes.map((d) =>
+ convertToDeploymentTableRow(d, environment),
+);
+
+describe('~/environments/environment_details/index.vue', () => {
+ let wrapper;
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = mountExtended(DeploymentsTable, {
+ propsData: {
+ deployments,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('deployment row', () => {
+ const [, , deployment] = deployments;
+
+ let row;
+
+ beforeEach(() => {
+ createWrapper();
+
+ row = wrapper.find('tr:nth-child(3)');
+ });
+
+ it.each`
+ cell | component | props
+ ${'status'} | ${DeploymentStatusLink} | ${{ deploymentJob: deployment.job, status: deployment.status }}
+ ${'triggerer'} | ${DeploymentTriggerer} | ${{ triggerer: deployment.triggerer }}
+ ${'commit'} | ${Commit} | ${deployment.commit}
+ ${'job'} | ${DeploymentJob} | ${{ job: deployment.job }}
+ ${'created date'} | ${'[data-testid="deployment-created-at"]'} | ${{ time: deployment.created }}
+ ${'deployed date'} | ${'[data-testid="deployment-deployed-at"]'} | ${{ time: deployment.deployed }}
+ ${'deployment actions'} | ${DeploymentActions} | ${{ actions: deployment.actions, rollback: deployment.rollback, approvalEnvironment: deployment.deploymentApproval }}
+ `('should show the correct component for $cell', ({ component, props }) => {
+ expect(row.findComponent(component).props()).toMatchObject(props);
+ });
+
+ it('hides the deployed at timestamp for not-finished deployments', () => {
+ row = wrapper.find('tr');
+
+ expect(row.find('[data-testid="deployment-deployed-at"]').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/page_spec.js b/spec/frontend/environments/environment_details/index_spec.js
index 3a1a3238abe..4bf5194b86e 100644
--- a/spec/frontend/environments/environment_details/page_spec.js
+++ b/spec/frontend/environments/environment_details/index_spec.js
@@ -5,31 +5,63 @@ import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graph
import emptyEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.empty.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentsDetailPage from '~/environments/environment_details/index.vue';
+import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
import EmptyState from '~/environments/environment_details/empty_state.vue';
import getEnvironmentDetails from '~/environments/graphql/queries/environment_details.query.graphql';
-import createMockApollo from '../../__helpers__/mock_apollo_helper';
-import waitForPromises from '../../__helpers__/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
-describe('~/environments/environment_details/page.vue', () => {
+const GRAPHQL_ETAG_KEY = '/graphql/environments';
+
+describe('~/environments/environment_details/index.vue', () => {
Vue.use(VueApollo);
let wrapper;
+ let routerMock;
+
+ const emptyEnvironmentToRollbackData = { id: '', name: '', lastDeployment: null, retryUrl: '' };
+ const environmentToRollbackMock = jest.fn();
+
+ const mockResolvers = {
+ Query: {
+ environmentToRollback: environmentToRollbackMock,
+ },
+ };
const defaultWrapperParameters = {
resolvedData: resolvedEnvironmentDetails,
+ environmentToRollbackData: emptyEnvironmentToRollbackData,
};
- const createWrapper = ({ resolvedData } = defaultWrapperParameters) => {
- const mockApollo = createMockApollo([
- [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)],
- ]);
+ const createWrapper = ({
+ resolvedData,
+ environmentToRollbackData,
+ } = defaultWrapperParameters) => {
+ const mockApollo = createMockApollo(
+ [[getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)]],
+ mockResolvers,
+ );
+ environmentToRollbackMock.mockReturnValue(
+ environmentToRollbackData || emptyEnvironmentToRollbackData,
+ );
+ const projectFullPath = 'gitlab-group/test-project';
+ routerMock = {
+ push: jest.fn(),
+ };
return mountExtended(EnvironmentsDetailPage, {
apolloProvider: mockApollo,
+ provide: {
+ projectPath: projectFullPath,
+ graphqlEtagKey: GRAPHQL_ETAG_KEY,
+ },
propsData: {
- projectFullPath: 'gitlab-group/test-project',
+ projectFullPath,
environmentName: 'test-environment-name',
},
+ mocks: {
+ $router: routerMock,
+ },
});
};
@@ -48,10 +80,18 @@ describe('~/environments/environment_details/page.vue', () => {
wrapper = createWrapper();
await waitForPromises();
});
- it('should render a table when query is loaded', async () => {
+ it('should render a table when query is loaded', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
});
+
+ describe('on rollback', () => {
+ it('sets the page back to default', () => {
+ wrapper.findComponent(ConfirmRollbackModal).vm.$emit('rollback');
+
+ expect(routerMock.push).toHaveBeenCalledWith({ query: {} });
+ });
+ });
});
describe('and there are no deployments', () => {
@@ -60,7 +100,7 @@ describe('~/environments/environment_details/page.vue', () => {
await waitForPromises();
});
- it('should render empty state component', async () => {
+ it('should render empty state component', () => {
expect(wrapper.findComponent(GlTableLite).exists()).toBe(false);
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
});
diff --git a/spec/frontend/environments/environment_external_url_spec.js b/spec/frontend/environments/environment_external_url_spec.js
index 5966993166b..2cccbb3b63c 100644
--- a/spec/frontend/environments/environment_external_url_spec.js
+++ b/spec/frontend/environments/environment_external_url_spec.js
@@ -1,35 +1,18 @@
import { mount } from '@vue/test-utils';
-import { s__, __ } from '~/locale';
+import { GlButton } from '@gitlab/ui';
import ExternalUrlComp from '~/environments/components/environment_external_url.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('External URL Component', () => {
let wrapper;
- let externalUrl;
+ const externalUrl = 'https://gitlab.com';
- describe('with safe link', () => {
- beforeEach(() => {
- externalUrl = 'https://gitlab.com';
- wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
- });
-
- it('should link to the provided externalUrl prop', () => {
- expect(wrapper.attributes('href')).toBe(externalUrl);
- expect(wrapper.find('a').exists()).toBe(true);
- });
+ beforeEach(() => {
+ wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
});
- describe('with unsafe link', () => {
- beforeEach(() => {
- externalUrl = 'postgres://gitlab';
- wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
- });
-
- it('should show a copy button instead', () => {
- const button = wrapper.findComponent(ModalCopyButton);
- expect(button.props('text')).toBe(externalUrl);
- expect(button.text()).toBe(__('Copy URL'));
- expect(button.props('title')).toBe(s__('Environments|Copy live environment URL'));
- });
+ it('should link to the provided externalUrl prop', () => {
+ const button = wrapper.findComponent(GlButton);
+ expect(button.attributes('href')).toEqual(externalUrl);
+ expect(button.props('isUnsafeLink')).toBe(true);
});
});
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index a37515bc3f7..4716f807657 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -35,10 +35,10 @@ describe('~/environments/components/environments_folder.vue', () => {
...propsData,
},
stubs: { transition: stubTransition() },
- provide: { helpPagePath: '/help' },
+ provide: { helpPagePath: '/help', projectId: '1' },
});
- beforeEach(async () => {
+ beforeEach(() => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index b9b34bee80f..50e4e637aa3 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -15,19 +15,16 @@ const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings
describe('~/environments/components/form.vue', () => {
let wrapper;
- const createWrapper = (propsData = {}) =>
+ const createWrapper = (propsData = {}, options = {}) =>
mountExtended(EnvironmentForm, {
provide: PROVIDE,
+ ...options,
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
wrapper = createWrapper();
@@ -105,6 +102,7 @@ describe('~/environments/components/form.vue', () => {
wrapper = createWrapper({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+
describe('when a new environment is being created', () => {
beforeEach(() => {
wrapper = createWrapper({
@@ -133,6 +131,18 @@ describe('~/environments/components/form.vue', () => {
});
});
+ describe('when no protected environment link is provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ provide: {},
+ });
+ });
+
+ it('does not show protected environment documentation', () => {
+ expect(wrapper.findByRole('link', { name: 'Protected environments' }).exists()).toBe(false);
+ });
+ });
+
describe('when an existing environment is being edited', () => {
beforeEach(() => {
wrapper = createWrapper({
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index dd909cf4473..e2b184adc8a 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -19,10 +19,6 @@ describe('Environment item', () => {
let tracking;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = mount(EnvironmentItem, {
...options,
});
@@ -55,10 +51,7 @@ describe('Environment item', () => {
const findUpcomingDeploymentAvatarLink = () =>
findUpcomingDeployment().findComponent(GlAvatarLink);
const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findMonitoringLink = () => wrapper.find('[data-testid="environment-monitoring"]');
describe('when item is not folder', () => {
it('should render environment name', () => {
@@ -390,10 +383,6 @@ describe('Environment item', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render folder icon and name', () => {
expect(wrapper.find('.folder-name').text()).toContain(folder.name);
expect(wrapper.find('.folder-icon')).toBeDefined();
@@ -446,4 +435,25 @@ describe('Environment item', () => {
});
});
});
+
+ describe.each([true, false])(
+ 'when `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ model: {
+ metrics_path: 'http://0.0.0.0:3000/flightjs/Flight/-/metrics?environment=6',
+ },
+ tableData,
+ },
+ provide: { glFeatures: { removeMonitorMetrics } },
+ });
+ });
+
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} link to metrics`, () => {
+ expect(findMonitoringLink().exists()).toBe(!removeMonitorMetrics);
+ });
+ },
+ );
});
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 170036b5b00..ee195b41bc8 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -11,10 +11,6 @@ describe('Pin Component', () => {
let wrapper;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = shallowMount(PinComponent, {
...options,
});
@@ -31,10 +27,6 @@ describe('Pin Component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
@@ -64,10 +56,6 @@ describe('Pin Component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index 851e24c22cc..3e27b8822e1 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -73,7 +73,7 @@ describe('Stop Component', () => {
});
});
- it('should show a loading icon if the environment is currently stopping', async () => {
+ it('should show a loading icon if the environment is currently stopping', () => {
expect(findButton().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index a86cfdd56ba..f41d1324b81 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -16,10 +16,6 @@ describe('Environment table', () => {
let wrapper;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = mount(EnvironmentTable, {
...options,
});
@@ -34,10 +30,6 @@ describe('Environment table', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Should render a table', async () => {
const mockItem = {
name: 'review',
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 986ecca4e84..dc450eb2aa7 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -96,10 +96,6 @@ describe('~/environments/components/environments_app.vue', () => {
paginationMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should request available environments if the scope is invalid', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
@@ -174,12 +170,8 @@ describe('~/environments/components/environments_app.vue', () => {
folder: resolvedFolder,
});
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
- button.trigger('click');
-
- await nextTick();
-
- expect(wrapper.findByText(s__('ReviewApp|Enable Review App')).exists()).toBe(true);
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
+ expect(button.exists()).toBe(true);
});
it('should not show a button to open the review app modal if review apps are configured', async () => {
@@ -191,7 +183,7 @@ describe('~/environments/components/environments_app.vue', () => {
folder: resolvedFolder,
});
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
expect(button.exists()).toBe(false);
});
@@ -426,7 +418,7 @@ describe('~/environments/components/environments_app.vue', () => {
);
});
- it('should sync search term from query params on load', async () => {
+ it('should sync search term from query params on load', () => {
expect(searchBox.element.value).toBe('prod');
});
});
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 1f233c05fbf..9464aeff028 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -1,12 +1,11 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { __, s__ } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import EnvironmentsDetailHeader from '~/environments/components/environments_detail_header.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import DeployFreezeAlert from '~/environments/components/deploy_freeze_alert.vue';
import { createEnvironment } from './mock_data';
describe('Environments detail header component', () => {
@@ -22,13 +21,14 @@ describe('Environments detail header component', () => {
const findCancelAutoStopAtButton = () => wrapper.findByTestId('cancel-auto-stop-button');
const findCancelAutoStopAtForm = () => wrapper.findByTestId('cancel-auto-stop-form');
const findTerminalButton = () => wrapper.findByTestId('terminal-button');
- const findExternalUrlButton = () => wrapper.findByTestId('external-url-button');
+ const findExternalUrlButton = () => wrapper.findComponentByTestId('external-url-button');
const findMetricsButton = () => wrapper.findByTestId('metrics-button');
const findEditButton = () => wrapper.findByTestId('edit-button');
const findStopButton = () => wrapper.findByTestId('stop-button');
const findDestroyButton = () => wrapper.findByTestId('destroy-button');
const findStopEnvironmentModal = () => wrapper.findComponent(StopEnvironmentModal);
const findDeleteEnvironmentModal = () => wrapper.findComponent(DeleteEnvironmentModal);
+ const findDeployFreezeAlert = () => wrapper.findComponent(DeployFreezeAlert);
const buttons = [
['Cancel Auto Stop At', findCancelAutoStopAtButton],
@@ -40,14 +40,17 @@ describe('Environments detail header component', () => {
['Destroy', findDestroyButton],
];
- const createWrapper = ({ props }) => {
+ const createWrapper = ({ props, glFeatures = {} }) => {
wrapper = shallowMountExtended(EnvironmentsDetailHeader, {
stubs: {
GlSprintf,
TimeAgo,
},
+ provide: {
+ glFeatures,
+ },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
canAdminEnvironment: false,
@@ -59,10 +62,6 @@ describe('Environments detail header component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default state with minimal access', () => {
beforeEach(() => {
createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
@@ -175,6 +174,7 @@ describe('Environments detail header component', () => {
it('displays the external url button with correct path', () => {
expect(findExternalUrlButton().attributes('href')).toBe(externalUrl);
+ expect(findExternalUrlButton().props('isUnsafeLink')).toBe(true);
});
});
@@ -199,6 +199,25 @@ describe('Environments detail header component', () => {
expect(tooltip).toBeDefined();
expect(button.attributes('title')).toBe('See metrics');
});
+
+ describe.each([true, false])(
+ 'and `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment({ metricsUrl: 'my metrics url' }),
+ metricsPath,
+ },
+ glFeatures: { removeMonitorMetrics },
+ });
+ });
+
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} Metrics button`, () => {
+ expect(findMetricsButton().exists()).toBe(!removeMonitorMetrics);
+ });
+ },
+ );
});
describe('when has all admin rights', () => {
@@ -246,22 +265,12 @@ describe('Environments detail header component', () => {
});
});
- describe('when the environment has an unsafe external url', () => {
- const externalUrl = 'postgres://staging';
-
- beforeEach(() => {
- createWrapper({
- props: {
- environment: createEnvironment({ externalUrl }),
- },
- });
- });
+ describe('deploy freeze alert', () => {
+ it('passes the environment name to the alert', () => {
+ const environment = createEnvironment();
+ createWrapper({ props: { environment } });
- it('should show a copy button instead', () => {
- const button = wrapper.findComponent(ModalCopyButton);
- expect(button.props('title')).toBe(s__('Environments|Copy live environment URL'));
- expect(button.props('text')).toBe(externalUrl);
- expect(button.text()).toBe(__('Copy URL'));
+ expect(findDeployFreezeAlert().props('name')).toBe(environment.name);
});
});
});
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index a87060f83d8..75fb3a31120 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -24,7 +24,6 @@ describe('Environments Folder View', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('successful request', () => {
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index 23506eb018d..6a40c68397b 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -123,22 +123,4 @@ describe('Environments Folder View', () => {
expect(tabTable.find('.badge').text()).toContain('0');
});
});
-
- describe('methods', () => {
- beforeEach(() => {
- mockEnvironments([]);
- createWrapper();
- jest.spyOn(window.history, 'pushState').mockImplementation(() => {});
- return axios.waitForAll();
- });
-
- describe('updateContent', () => {
- it('should set given parameters', () =>
- wrapper.vm.updateContent({ scope: 'stopped', page: '4' }).then(() => {
- expect(wrapper.vm.page).toEqual('4');
- expect(wrapper.vm.scope).toEqual('stopped');
- expect(wrapper.vm.requestData.page).toEqual('4');
- }));
- });
- });
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 5ea0be41614..addbf2c21dc 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -798,3 +798,112 @@ export const resolvedDeploymentDetails = {
},
},
};
+
+export const agent = {
+ project: 'agent-project',
+ id: 'gid://gitlab/ClusterAgent/1',
+ name: 'agent-name',
+ kubernetesNamespace: 'agent-namespace',
+};
+
+const runningPod = { status: { phase: 'Running' } };
+const pendingPod = { status: { phase: 'Pending' } };
+const succeededPod = { status: { phase: 'Succeeded' } };
+const failedPod = { status: { phase: 'Failed' } };
+
+export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod];
+
+export const k8sServicesMock = [
+ {
+ metadata: {
+ name: 'my-first-service',
+ namespace: 'default',
+ creationTimestamp: new Date(),
+ },
+ spec: {
+ ports: [
+ {
+ name: 'https',
+ protocol: 'TCP',
+ port: 443,
+ targetPort: 8443,
+ },
+ ],
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ type: 'ClusterIP',
+ },
+ },
+ {
+ metadata: {
+ name: 'my-second-service',
+ namespace: 'default',
+ creationTimestamp: '2020-07-03T14:06:04Z',
+ },
+ spec: {
+ ports: [
+ {
+ name: 'http',
+ protocol: 'TCP',
+ appProtocol: 'http',
+ port: 80,
+ targetPort: 'http',
+ nodePort: 31989,
+ },
+ {
+ name: 'https',
+ protocol: 'TCP',
+ appProtocol: 'https',
+ port: 443,
+ targetPort: 'https',
+ nodePort: 32679,
+ },
+ ],
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ type: 'NodePort',
+ },
+ },
+];
+
+const readyDeployment = {
+ status: {
+ conditions: [
+ { type: 'Available', status: 'True' },
+ { type: 'Progressing', status: 'True' },
+ ],
+ },
+};
+const failedDeployment = {
+ status: {
+ conditions: [
+ { type: 'Available', status: 'False' },
+ { type: 'Progressing', status: 'False' },
+ ],
+ },
+};
+const readyDaemonSet = {
+ status: { numberReady: 1, desiredNumberScheduled: 1, numberMisscheduled: 0 },
+};
+const failedDaemonSet = {
+ status: { numberMisscheduled: 1, numberReady: 0, desiredNumberScheduled: 1 },
+};
+const readySet = { spec: { replicas: 2 }, status: { readyReplicas: 2 } };
+const failedSet = { spec: { replicas: 2 }, status: { readyReplicas: 1 } };
+const completedJob = { spec: { completions: 1 }, status: { succeeded: 1, failed: 0 } };
+const failedJob = { spec: { completions: 1 }, status: { succeeded: 0, failed: 1 } };
+const completedCronJob = {
+ spec: { suspend: 0 },
+ status: { active: 0, lastScheduleTime: new Date().toString() },
+};
+const suspendedCronJob = { spec: { suspend: 1 }, status: { active: 0, lastScheduleTime: '' } };
+const failedCronJob = { spec: { suspend: 0 }, status: { active: 2, lastScheduleTime: '' } };
+
+export const k8sWorkloadsMock = {
+ DeploymentList: [readyDeployment, failedDeployment],
+ DaemonSetList: [readyDaemonSet, failedDaemonSet, failedDaemonSet],
+ StatefulSetList: [readySet, readySet, failedSet],
+ ReplicaSetList: [readySet, failedSet],
+ JobList: [completedJob, completedJob, failedJob],
+ CronJobList: [completedCronJob, suspendedCronJob, failedCronJob],
+};
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 2c223d3a1a7..edffc00e185 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -17,6 +18,8 @@ import {
resolvedEnvironment,
folder,
resolvedFolder,
+ k8sPodsMock,
+ k8sServicesMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -27,6 +30,14 @@ describe('~/frontend/environments/graphql/resolvers', () => {
let mockApollo;
let localState;
+ const configuration = {
+ basePath: 'kas-proxy/',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+ const namespace = 'default';
+
beforeEach(() => {
mockResolvers = resolvers(ENDPOINT);
mock = new MockAdapter(axios);
@@ -143,13 +154,178 @@ describe('~/frontend/environments/graphql/resolvers', () => {
expect(environmentFolder).toEqual(resolvedFolder);
});
});
- describe('stopEnvironment', () => {
+ describe('k8sPods', () => {
+ const mockPodsListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sPodsMock,
+ },
+ });
+ });
+
+ const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+ const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
+
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace);
+ expect(mockAllPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
+
+ expect(mockAllPodsListFn).toHaveBeenCalled();
+ expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
+ describe('k8sServices', () => {
+ const mockServicesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sServicesMock,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockImplementation(mockServicesListFn);
+ });
+
+ it('should request services from the cluster_client library', async () => {
+ const services = await mockResolvers.Query.k8sServices(null, { configuration });
+
+ expect(mockServicesListFn).toHaveBeenCalled();
+
+ expect(services).toEqual(k8sServicesMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(mockResolvers.Query.k8sServices(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
+ describe('k8sWorkloads', () => {
+ const emptyImplementation = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: [],
+ },
+ });
+ });
+
+ const [
+ mockNamespacedDeployment,
+ mockNamespacedDaemonSet,
+ mockNamespacedStatefulSet,
+ mockNamespacedReplicaSet,
+ mockNamespacedJob,
+ mockNamespacedCronJob,
+ mockAllDeployment,
+ mockAllDaemonSet,
+ mockAllStatefulSet,
+ mockAllReplicaSet,
+ mockAllJob,
+ mockAllCronJob,
+ ] = Array(12).fill(emptyImplementation);
+
+ const namespacedMocks = [
+ { method: 'listAppsV1NamespacedDeployment', api: AppsV1Api, spy: mockNamespacedDeployment },
+ { method: 'listAppsV1NamespacedDaemonSet', api: AppsV1Api, spy: mockNamespacedDaemonSet },
+ { method: 'listAppsV1NamespacedStatefulSet', api: AppsV1Api, spy: mockNamespacedStatefulSet },
+ { method: 'listAppsV1NamespacedReplicaSet', api: AppsV1Api, spy: mockNamespacedReplicaSet },
+ { method: 'listBatchV1NamespacedJob', api: BatchV1Api, spy: mockNamespacedJob },
+ { method: 'listBatchV1NamespacedCronJob', api: BatchV1Api, spy: mockNamespacedCronJob },
+ ];
+
+ const allMocks = [
+ { method: 'listAppsV1DeploymentForAllNamespaces', api: AppsV1Api, spy: mockAllDeployment },
+ { method: 'listAppsV1DaemonSetForAllNamespaces', api: AppsV1Api, spy: mockAllDaemonSet },
+ { method: 'listAppsV1StatefulSetForAllNamespaces', api: AppsV1Api, spy: mockAllStatefulSet },
+ { method: 'listAppsV1ReplicaSetForAllNamespaces', api: AppsV1Api, spy: mockAllReplicaSet },
+ { method: 'listBatchV1JobForAllNamespaces', api: BatchV1Api, spy: mockAllJob },
+ { method: 'listBatchV1CronJobForAllNamespaces', api: BatchV1Api, spy: mockAllCronJob },
+ ];
+
+ beforeEach(() => {
+ [...namespacedMocks, ...allMocks].forEach((workloadMock) => {
+ jest
+ .spyOn(workloadMock.api.prototype, workloadMock.method)
+ .mockImplementation(workloadMock.spy);
+ });
+ });
+
+ it('should request namespaced workload types from the cluster_client library if namespace is specified', async () => {
+ await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace });
+
+ namespacedMocks.forEach((workloadMock) => {
+ expect(workloadMock.spy).toHaveBeenCalledWith(namespace);
+ });
+ });
+
+ it('should request all workload types from the cluster_client library if namespace is not specified', async () => {
+ await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace: '' });
+
+ allMocks.forEach((workloadMock) => {
+ expect(workloadMock.spy).toHaveBeenCalled();
+ });
+ });
+ it('should pass fulfilled calls data if one of the API calls fail', async () => {
+ jest
+ .spyOn(AppsV1Api.prototype, 'listAppsV1DeploymentForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(
+ mockResolvers.Query.k8sWorkloads(null, { configuration }),
+ ).resolves.toBeDefined();
+ });
+ it('should throw an error if all the API calls fail', async () => {
+ [...allMocks].forEach((workloadMock) => {
+ jest
+ .spyOn(workloadMock.api.prototype, workloadMock.method)
+ .mockRejectedValue(new Error('API error'));
+ });
+
+ await expect(mockResolvers.Query.k8sWorkloads(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
+ describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
- await mockResolvers.Mutation.stopEnvironment(null, { environment }, { client });
+ await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
@@ -166,7 +342,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
- await mockResolvers.Mutation.stopEnvironment(null, { environment }, { client });
+ await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
diff --git a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
index 326a28bd769..ec0fe0c5541 100644
--- a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
+++ b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
@@ -26,11 +26,37 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "2022-10-17T07:44:43Z",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": Object {
"label": "deploy-prod (#860)",
"webPath": "/gitlab-org/pipelinestest/-/jobs/860",
},
+ "rollback": Object {
+ "id": "gid://gitlab/Deployment/76",
+ "lastDeployment": Object {
+ "commit": Object {
+ "author": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+ "authorEmail": "admin@example.com",
+ "authorGravatar": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "authorName": "Administrator",
+ "id": "gid://gitlab/CommitPresenter/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "message": "Update .gitlab-ci.yml file",
+ "shortId": "0cb48dd5",
+ "webUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ },
+ "isLast": false,
+ },
+ "name": undefined,
+ "retryUrl": "/gitlab-org/pipelinestest/-/jobs/860/retry",
+ },
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -60,8 +86,12 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "2022-10-17T07:44:43Z",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": undefined,
+ "rollback": null,
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -91,8 +121,12 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": null,
+ "rollback": null,
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js
new file mode 100644
index 00000000000..b1795065281
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_agent_info_spec.js
@@ -0,0 +1,124 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
+import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql';
+
+Vue.use(VueApollo);
+
+const propsData = {
+ agentName: 'my-agent',
+ agentId: '1',
+ agentProjectPath: 'path/to/agent-config-project',
+};
+
+const mockClusterAgent = {
+ id: '1',
+ name: 'token-1',
+ webPath: 'path/to/agent-page',
+};
+
+const connectedTimeNow = new Date();
+const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+
+describe('~/environments/components/kubernetes_agent_info.vue', () => {
+ let wrapper;
+ let agentQueryResponse;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAgentLink = () => wrapper.findComponent(GlLink);
+ const findAgentStatus = () => wrapper.findByTestId('agent-status');
+ const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon);
+ const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = ({ tokens = [], queryResponse = null } = {}) => {
+ const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } };
+
+ agentQueryResponse =
+ queryResponse ||
+ jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } });
+ const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]);
+
+ wrapper = extendedWrapper(
+ shallowMount(KubernetesAgentInfo, {
+ apolloProvider,
+ propsData,
+ stubs: { TimeAgoTooltip, GlSprintf },
+ }),
+ );
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows loading icon while fetching the agent details', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ await waitForPromises();
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('sends expected params', async () => {
+ await waitForPromises();
+
+ const variables = {
+ agentName: propsData.agentName,
+ projectPath: propsData.agentProjectPath,
+ };
+
+ expect(agentQueryResponse).toHaveBeenCalledWith(variables);
+ });
+
+ it('renders the agent name with the link', async () => {
+ await waitForPromises();
+
+ expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath);
+ expect(findAgentLink().text()).toContain(mockClusterAgent.id);
+ });
+ });
+
+ describe.each`
+ lastUsedAt | status | lastUsedText
+ ${null} | ${'unused'} | ${KubernetesAgentInfo.i18n.neverConnectedText}
+ ${connectedTimeNow} | ${'active'} | ${'just now'}
+ ${connectedTimeInactive} | ${'inactive'} | ${'8 minutes ago'}
+ `('when agent connection status is "$status"', ({ lastUsedAt, status, lastUsedText }) => {
+ beforeEach(async () => {
+ const tokens = [{ id: 'token-id', lastUsedAt }];
+ createWrapper({ tokens });
+ await waitForPromises();
+ });
+
+ it('displays correct status text', () => {
+ expect(findAgentStatus().text()).toBe(AGENT_STATUSES[status].name);
+ });
+
+ it('displays correct status icon', () => {
+ expect(findAgentStatusIcon().props('name')).toBe(AGENT_STATUSES[status].icon);
+ expect(findAgentStatusIcon().attributes('class')).toBe(AGENT_STATUSES[status].class);
+ });
+
+ it('displays correct last used date status', () => {
+ expect(findAgentLastUsedDate().text()).toBe(lastUsedText);
+ });
+ });
+
+ describe('when the agent query has errored', () => {
+ beforeEach(() => {
+ createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
+ return waitForPromises();
+ });
+
+ it('displays an alert message', () => {
+ expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
new file mode 100644
index 00000000000..394fd200edf
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -0,0 +1,131 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui';
+import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
+import { agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
+
+const propsData = {
+ agentId: agent.id,
+ agentName: agent.name,
+ agentProjectPath: agent.project,
+ namespace: agent.kubernetesNamespace,
+};
+
+const provide = {
+ kasTunnelUrl: mockKasTunnelUrl,
+};
+
+const configuration = {
+ basePath: provide.kasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+};
+
+describe('~/environments/components/kubernetes_overview.vue', () => {
+ let wrapper;
+
+ const findCollapse = () => wrapper.findComponent(GlCollapse);
+ const findCollapseButton = () => wrapper.findComponent(GlButton);
+ const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
+ const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
+ const findKubernetesTabs = () => wrapper.findComponent(KubernetesTabs);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = () => {
+ wrapper = shallowMount(KubernetesOverview, {
+ propsData,
+ provide,
+ });
+ };
+
+ const toggleCollapse = async () => {
+ findCollapseButton().vm.$emit('click');
+ await nextTick();
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the kubernetes overview title', () => {
+ expect(wrapper.text()).toBe(KubernetesOverview.i18n.sectionTitle);
+ });
+ });
+
+ describe('collapse', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('is collapsed by default', () => {
+ expect(findCollapse().props('visible')).toBeUndefined();
+ expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.expand);
+ expect(findCollapseButton().props('icon')).toBe('chevron-right');
+ });
+
+ it("doesn't render components when the collapse is not visible", () => {
+ expect(findAgentInfo().exists()).toBe(false);
+ expect(findKubernetesPods().exists()).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ findCollapseButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findCollapse().attributes('visible')).toBe('true');
+ expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.collapse);
+ expect(findCollapseButton().props('icon')).toBe('chevron-down');
+ });
+ });
+
+ describe('when section is expanded', () => {
+ beforeEach(() => {
+ createWrapper();
+ toggleCollapse();
+ });
+
+ it('renders kubernetes agent info', () => {
+ expect(findAgentInfo().props()).toEqual({
+ agentName: agent.name,
+ agentId: agent.id,
+ agentProjectPath: agent.project,
+ });
+ });
+
+ it('renders kubernetes pods', () => {
+ expect(findKubernetesPods().props()).toEqual({
+ namespace: agent.kubernetesNamespace,
+ configuration,
+ });
+ });
+
+ it('renders kubernetes tabs', () => {
+ expect(findKubernetesTabs().props()).toEqual({
+ namespace: agent.kubernetesNamespace,
+ configuration,
+ });
+ });
+ });
+
+ describe('on cluster error', () => {
+ beforeEach(() => {
+ createWrapper();
+ toggleCollapse();
+ });
+
+ it('shows alert with the error message', async () => {
+ const error = 'Error message from pods';
+
+ findKubernetesPods().vm.$emit('cluster-error', error);
+ await nextTick();
+
+ expect(findAlert().text()).toBe(error);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js
new file mode 100644
index 00000000000..137309d7853
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_pods_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sPodsMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_pods.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAllStats = () => wrapper.findAllComponents(GlSingleStat);
+ const findSingleStat = (at) => findAllStats().at(at);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockReturnValue(k8sPodsMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMount(KubernetesPods, {
+ propsData: { namespace, configuration },
+ apolloProvider,
+ });
+ };
+
+ describe('mounted', () => {
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('hides the loading icon when the list of pods loaded', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when gets pods data', () => {
+ it('renders stats', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findAllStats()).toHaveLength(4);
+ });
+
+ it.each`
+ count | title | index
+ ${2} | ${KubernetesPods.i18n.runningPods} | ${0}
+ ${1} | ${KubernetesPods.i18n.pendingPods} | ${1}
+ ${1} | ${KubernetesPods.i18n.succeededPods} | ${2}
+ ${2} | ${KubernetesPods.i18n.failedPods} | ${3}
+ `(
+ 'renders stat with title "$title" and count "$count" at index $index',
+ async ({ count, title, index }) => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findSingleStat(index).props()).toMatchObject({
+ value: count,
+ title,
+ });
+ },
+ );
+ });
+
+ describe('when gets an error from the cluster_client API', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+ });
+
+ it("doesn't show pods stats", () => {
+ expect(findAllStats()).toHaveLength(0);
+ });
+
+ it('emits an error message', () => {
+ expect(wrapper.emitted('cluster-error')).toMatchObject([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_summary_spec.js b/spec/frontend/environments/kubernetes_summary_spec.js
new file mode 100644
index 00000000000..53b83079486
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_summary_spec.js
@@ -0,0 +1,115 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTab, GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesSummary from '~/environments/components/kubernetes_summary.vue';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sWorkloadsMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_summary.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTab = () => wrapper.findComponent(GlTab);
+ const findSummaryListItem = (at) => wrapper.findAllByTestId('summary-list-item').at(at);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sWorkloads: jest.fn().mockReturnValue(k8sWorkloadsMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMountExtended(KubernetesSummary, {
+ propsData: { configuration, namespace },
+ apolloProvider,
+ stubs: {
+ GlTab,
+ GlBadge,
+ },
+ });
+ };
+
+ describe('mounted', () => {
+ it('renders summary tab', () => {
+ createWrapper();
+
+ expect(findTab().text()).toMatchInterpolatedText(`${KubernetesSummary.i18n.summaryTitle} 0`);
+ });
+
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ describe('when workloads data is loaded', () => {
+ beforeEach(async () => {
+ await createWrapper();
+ await waitForPromises();
+ });
+
+ it('hides the loading icon when the list of workload types loaded', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it.each`
+ type | successText | successCount | failedCount | suspendedCount | index
+ ${'Deployments'} | ${'ready'} | ${1} | ${1} | ${0} | ${0}
+ ${'DaemonSets'} | ${'ready'} | ${1} | ${2} | ${0} | ${1}
+ ${'StatefulSets'} | ${'ready'} | ${2} | ${1} | ${0} | ${2}
+ ${'ReplicaSets'} | ${'ready'} | ${1} | ${1} | ${0} | ${3}
+ ${'Jobs'} | ${'completed'} | ${2} | ${1} | ${0} | ${4}
+ ${'CronJobs'} | ${'ready'} | ${1} | ${1} | ${1} | ${5}
+ `(
+ 'populates view with the correct badges for workload type $type',
+ ({ type, successText, successCount, failedCount, suspendedCount, index }) => {
+ const findAllBadges = () => findSummaryListItem(index).findAllComponents(GlBadge);
+ const findBadgeByVariant = (variant) =>
+ findAllBadges().wrappers.find((badge) => badge.props('variant') === variant);
+
+ expect(findSummaryListItem(index).text()).toContain(type);
+ expect(findBadgeByVariant('success').text()).toBe(`${successCount} ${successText}`);
+ expect(findBadgeByVariant('danger').text()).toBe(`${failedCount} failed`);
+ if (suspendedCount > 0) {
+ expect(findBadgeByVariant('neutral').text()).toBe(`${suspendedCount} suspended`);
+ }
+ },
+ );
+ });
+
+ it('emits an error message when gets an error from the cluster_client API', async () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sWorkloads: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+
+ expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_tabs_spec.js b/spec/frontend/environments/kubernetes_tabs_spec.js
new file mode 100644
index 00000000000..429f267347b
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_tabs_spec.js
@@ -0,0 +1,168 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTabs, GlTab, GlTable, GlPagination, GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import { useFakeDate } from 'helpers/fake_date';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
+import KubernetesSummary from '~/environments/components/kubernetes_summary.vue';
+import { SERVICES_LIMIT_PER_PAGE } from '~/environments/constants';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sServicesMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_tabs.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTab = () => wrapper.findComponent(GlTab);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const findKubernetesSummary = () => wrapper.findComponent(KubernetesSummary);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockReturnValue(k8sServicesMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMountExtended(KubernetesTabs, {
+ propsData: { configuration, namespace },
+ apolloProvider,
+ stubs: {
+ GlTab,
+ GlTable: stubComponent(GlTable, {
+ props: ['items', 'per-page'],
+ }),
+ GlBadge,
+ },
+ });
+ };
+
+ describe('mounted', () => {
+ it('shows tabs', () => {
+ createWrapper();
+
+ expect(findTabs().exists()).toBe(true);
+ });
+
+ it('renders summary tab', () => {
+ createWrapper();
+
+ expect(findKubernetesSummary().props()).toEqual({ namespace, configuration });
+ });
+
+ it('renders services tab', () => {
+ createWrapper();
+
+ expect(findTab().text()).toMatchInterpolatedText(`${KubernetesTabs.i18n.servicesTitle} 0`);
+ });
+ });
+
+ describe('services tab', () => {
+ useFakeDate(2020, 6, 6);
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ describe('when services data is loaded', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('hides the loading icon when the list of services loaded', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders services table when gets services data', () => {
+ expect(findTable().props('perPage')).toBe(SERVICES_LIMIT_PER_PAGE);
+ expect(findTable().props('items')).toMatchObject([
+ {
+ name: 'my-first-service',
+ namespace: 'default',
+ type: 'ClusterIP',
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ ports: '443/TCP',
+ age: '0s',
+ },
+ {
+ name: 'my-second-service',
+ namespace: 'default',
+ type: 'NodePort',
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ ports: '80:31989/TCP, 443:32679/TCP',
+ age: '2d',
+ },
+ ]);
+ });
+
+ it("doesn't render pagination when services are less then SERVICES_LIMIT_PER_PAGE", async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ it('shows pagination when services are more then SERVICES_LIMIT_PER_PAGE', async () => {
+ const createApolloProviderWithPagination = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest
+ .fn()
+ .mockReturnValue(
+ Array.from({ length: 6 }, () => k8sServicesMock).flatMap((array) => array),
+ ),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ createWrapper(createApolloProviderWithPagination());
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('emits an error message when gets an error from the cluster_client API', async () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+
+ expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index a6d67c26304..bd2c6b7c892 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -313,6 +313,8 @@ const createEnvironment = (data = {}) => ({
...data,
});
+const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy';
+
export {
environment,
environmentsList,
@@ -321,4 +323,5 @@ export {
tableData,
deployBoardMockData,
createEnvironment,
+ mockKasTunnelUrl,
};
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 76cd09cfb4e..5583e737dd8 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -7,9 +7,12 @@ import { stubTransition } from 'helpers/stub_transition';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
+import EnvironmentActions from '~/environments/components/environment_actions.vue';
import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
-import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
+import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
Vue.use(VueApollo);
@@ -20,15 +23,24 @@ describe('~/environments/components/new_environment_item.vue', () => {
return createMockApollo();
};
- const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
+ const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
- provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' },
+ provide: {
+ helpPagePath: '/help',
+ projectId: '1',
+ projectPath: '/1',
+ kasTunnelUrl: mockKasTunnelUrl,
+ ...provideData,
+ },
stubs: { transition: stubTransition() },
});
const findDeployment = () => wrapper.findComponent(Deployment);
+ const findActions = () => wrapper.findComponent(EnvironmentActions);
+ const findKubernetesOverview = () => wrapper.findComponent(KubernetesOverview);
+ const findMonitoringLink = () => wrapper.find('[data-testid="environment-monitoring"]');
const expandCollapsedSection = async () => {
const button = wrapper.findByRole('button', { name: __('Expand') });
@@ -37,10 +49,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
return button;
};
- afterEach(() => {
- wrapper?.destroy();
- });
-
it('displays the name when not in a folder', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
@@ -126,9 +134,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('shows a dropdown if there are actions to perform', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
-
- expect(actions.exists()).toBe(true);
+ expect(findActions().exists()).toBe(true);
});
it('does not show a dropdown if there are no actions to perform', () => {
@@ -142,22 +148,20 @@ describe('~/environments/components/new_environment_item.vue', () => {
},
});
- const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
-
- expect(actions.exists()).toBe(false);
+ expect(findActions().exists()).toBe(false);
});
it('passes all the actions down to the action component', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
-
- expect(action.exists()).toBe(true);
+ expect(findActions().props('actions')).toMatchObject(
+ resolvedEnvironment.lastDeployment.manualActions,
+ );
});
});
describe('stop', () => {
- it('shows a buton to stop the environment if the environment is available', () => {
+ it('shows a button to stop the environment if the environment is available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
@@ -165,7 +169,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(stop.exists()).toBe(true);
});
- it('does not show a buton to stop the environment if the environment is stopped', () => {
+ it('does not show a button to stop the environment if the environment is stopped', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, canStop: false } },
apolloProvider: createApolloProvider(),
@@ -311,7 +315,25 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(rollback.exists()).toBe(false);
});
+
+ describe.each([true, false])(
+ 'when `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } },
+ apolloProvider: createApolloProvider(),
+ provideData: { glFeatures: { removeMonitorMetrics } },
+ });
+ });
+
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} link to metrics`, () => {
+ expect(findMonitoringLink().exists()).toBe(!removeMonitorMetrics);
+ });
+ },
+ );
});
+
describe('terminal', () => {
it('shows the link to the terminal if set up', () => {
wrapper = createWrapper({
@@ -384,6 +406,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
const button = await expandCollapsedSection();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(button.props('category')).toBe('secondary');
expect(collapse.attributes('visible')).toBe('visible');
expect(icon.props('name')).toBe('chevron-lg-down');
expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
@@ -515,4 +538,72 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(deployBoard.exists()).toBe(false);
});
});
+
+ describe('kubernetes overview', () => {
+ const environmentWithAgent = {
+ ...resolvedEnvironment,
+ agent,
+ };
+
+ it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => {
+ wrapper = createWrapper({
+ propsData: { environment: environmentWithAgent },
+ provideData: {
+ glFeatures: {
+ kasUserAccessProject: true,
+ },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().props()).toMatchObject({
+ agentProjectPath: agent.project,
+ agentName: agent.name,
+ agentId: agent.id,
+ namespace: agent.kubernetesNamespace,
+ });
+ });
+
+ it('should not render if the feature flag is not enabled', () => {
+ wrapper = createWrapper({
+ propsData: { environment: environmentWithAgent },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+
+ it('should not render if the environment has no agent object', () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+
+ it('should not render if the environment has an agent object without agent id specified', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ agent: {
+ project: agent.project,
+ name: agent.name,
+ },
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index a8cc05b297b..743f4ad6786 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
@@ -41,7 +41,6 @@ describe('~/environments/components/new.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js
index a2ab4f707b5..3d28ceba318 100644
--- a/spec/frontend/environments/stop_stale_environments_modal_spec.js
+++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js
@@ -18,7 +18,6 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
let wrapper;
let mock;
let before;
- let originalGon;
const createWrapper = (opts = {}) =>
shallowMount(StopStaleEnvironmentsModal, {
@@ -28,8 +27,7 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
});
beforeEach(() => {
- originalGon = window.gon;
- window.gon = { api_version: 'v4' };
+ window.gon.api_version = 'v4';
mock = new MockAdapter(axios);
jest.spyOn(axios, 'post');
@@ -39,17 +37,15 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
jest.resetAllMocks();
- window.gon = originalGon;
});
- it('sets the correct min and max dates', async () => {
+ it('sets the correct min and max dates', () => {
expect(before.props().minDate.toISOString()).toBe(TEN_YEARS_AGO.toISOString());
expect(before.props().maxDate.toISOString()).toBe(ONE_WEEK_AGO.toISOString());
});
- it('requests cleanup when submit is clicked', async () => {
+ it('requests cleanup when submit is clicked', () => {
mock.onPost().replyOnce(HTTP_STATUS_OK);
wrapper.findComponent(GlModal).vm.$emit('primary');
const url = STOP_STALE_ENVIRONMENTS_PATH.replace(':id', 1).replace(':version', 'v4');