diff options
Diffstat (limited to 'spec/frontend/environments')
10 files changed, 739 insertions, 29 deletions
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js index b699f953945..b8dcb7c0d08 100644 --- a/spec/frontend/environments/confirm_rollback_modal_spec.js +++ b/spec/frontend/environments/confirm_rollback_modal_spec.js @@ -26,7 +26,7 @@ describe('Confirm Rollback Modal Component', () => { commit: { shortId: 'abc0123', }, - 'last?': true, + isLast: true, }, modalId: 'test', }; @@ -145,7 +145,7 @@ describe('Confirm Rollback Modal Component', () => { ...environment, lastDeployment: { ...environment.lastDeployment, - 'last?': false, + isLast: false, }, }, hasMultipleCommits, @@ -167,7 +167,7 @@ describe('Confirm Rollback Modal Component', () => { ...environment, lastDeployment: { ...environment.lastDeployment, - 'last?': false, + isLast: false, }, }, hasMultipleCommits, @@ -191,7 +191,7 @@ describe('Confirm Rollback Modal Component', () => { ...environment, lastDeployment: { ...environment.lastDeployment, - 'last?': true, + isLast: true, }, }, hasMultipleCommits, diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js new file mode 100644 index 00000000000..37209bdc86c --- /dev/null +++ b/spec/frontend/environments/deployment_spec.js @@ -0,0 +1,29 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import Deployment from '~/environments/components/deployment.vue'; +import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue'; +import { resolvedEnvironment } from './graphql/mock_data'; + +describe('~/environments/components/deployment.vue', () => { + let wrapper; + + const createWrapper = ({ propsData = {} } = {}) => + mountExtended(Deployment, { + propsData: { + deployment: resolvedEnvironment.lastDeployment, + ...propsData, + }, + }); + + afterEach(() => { + wrapper?.destroy(); + }); + + describe('status', () => { + it('should pass the deployable status to the badge', () => { + wrapper = createWrapper(); + expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe( + resolvedEnvironment.lastDeployment.status, + ); + }); + }); +}); diff --git a/spec/frontend/environments/deployment_status_badge_spec.js b/spec/frontend/environments/deployment_status_badge_spec.js new file mode 100644 index 00000000000..02aae57396a --- /dev/null +++ b/spec/frontend/environments/deployment_status_badge_spec.js @@ -0,0 +1,42 @@ +import { GlBadge } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; +import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue'; + +describe('~/environments/components/deployment_status_badge.vue', () => { + let wrapper; + + const createWrapper = ({ propsData = {} } = {}) => + mountExtended(DeploymentStatusBadge, { + propsData, + }); + + describe.each` + status | text | variant | icon + ${'created'} | ${s__('Deployment|Created')} | ${'neutral'} | ${'status_created'} + ${'running'} | ${s__('Deployment|Running')} | ${'info'} | ${'status_running'} + ${'success'} | ${s__('Deployment|Success')} | ${'success'} | ${'status_success'} + ${'failed'} | ${s__('Deployment|Failed')} | ${'danger'} | ${'status_failed'} + ${'canceled'} | ${s__('Deployment|Cancelled')} | ${'neutral'} | ${'status_canceled'} + ${'skipped'} | ${s__('Deployment|Skipped')} | ${'neutral'} | ${'status_skipped'} + ${'blocked'} | ${s__('Deployment|Waiting')} | ${'neutral'} | ${'status_manual'} + `('$status', ({ status, text, variant, icon }) => { + let badge; + + beforeEach(() => { + wrapper = createWrapper({ propsData: { status } }); + badge = wrapper.findComponent(GlBadge); + }); + + it(`sets the text to ${text}`, () => { + expect(wrapper.text()).toBe(text); + }); + + it(`sets the variant to ${variant}`, () => { + expect(badge.props('variant')).toBe(variant); + }); + it(`sets the icon to ${icon}`, () => { + expect(badge.props('icon')).toBe(icon); + }); + }); +}); diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js index db78a6b0cdd..1b68a692db8 100644 --- a/spec/frontend/environments/environment_actions_spec.js +++ b/spec/frontend/environments/environment_actions_spec.js @@ -1,9 +1,13 @@ import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; 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 createMockApollo from 'helpers/mock_apollo_helper'; const scheduledJobAction = { name: 'scheduled action', @@ -25,12 +29,13 @@ describe('EnvironmentActions Component', () => { const findEnvironmentActionsButton = () => wrapper.find('[data-testid="environment-actions-button"]'); - function createComponent(props, { mountFn = shallowMount } = {}) { + function createComponent(props, { mountFn = shallowMount, options = {} } = {}) { wrapper = mountFn(EnvironmentActions, { propsData: { actions: [], ...props }, directives: { GlTooltip: createMockDirective(), }, + ...options, }); } @@ -150,4 +155,32 @@ describe('EnvironmentActions Component', () => { 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_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js index dff444b79f3..358abca2f77 100644 --- a/spec/frontend/environments/environment_stop_spec.js +++ b/spec/frontend/environments/environment_stop_spec.js @@ -1,38 +1,80 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import $ from 'jquery'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import setEnvironmentToStopMutation from '~/environments/graphql/mutations/set_environment_to_stop.mutation.graphql'; +import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql'; import StopComponent from '~/environments/components/environment_stop.vue'; import eventHub from '~/environments/event_hub'; - -$.fn.tooltip = () => {}; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { resolvedEnvironment } from './graphql/mock_data'; describe('Stop Component', () => { let wrapper; - const createWrapper = () => { + const createWrapper = (props = {}, options = {}) => { wrapper = shallowMount(StopComponent, { propsData: { environment: {}, + ...props, }, + ...options, }); }; const findButton = () => wrapper.find(GlButton); - beforeEach(() => { - jest.spyOn(window, 'confirm'); + describe('eventHub', () => { + beforeEach(() => { + createWrapper(); + }); - createWrapper(); - }); + it('should render a button to stop the environment', () => { + expect(findButton().exists()).toBe(true); + expect(wrapper.attributes('title')).toEqual('Stop environment'); + }); - it('should render a button to stop the environment', () => { - expect(findButton().exists()).toBe(true); - expect(wrapper.attributes('title')).toEqual('Stop environment'); + it('emits requestStopEnvironment in the event hub when button is clicked', () => { + jest.spyOn(eventHub, '$emit'); + findButton().vm.$emit('click'); + expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment); + }); }); - it('emits requestStopEnvironment in the event hub when button is clicked', () => { - jest.spyOn(eventHub, '$emit'); - findButton().vm.$emit('click'); - expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment); + describe('graphql', () => { + Vue.use(VueApollo); + let mockApollo; + + beforeEach(() => { + mockApollo = createMockApollo(); + mockApollo.clients.defaultClient.writeQuery({ + query: isEnvironmentStoppingQuery, + variables: { environment: resolvedEnvironment }, + data: { isEnvironmentStopping: true }, + }); + + createWrapper( + { graphql: true, environment: resolvedEnvironment }, + { apolloProvider: mockApollo }, + ); + }); + + it('should render a button to stop the environment', () => { + expect(findButton().exists()).toBe(true); + expect(wrapper.attributes('title')).toEqual('Stop environment'); + }); + + it('sets the environment to stop on click', () => { + jest.spyOn(mockApollo.defaultClient, 'mutate'); + findButton().vm.$emit('click'); + expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({ + mutation: setEnvironmentToStopMutation, + variables: { environment: resolvedEnvironment }, + }); + }); + + it('should show a loading icon if the environment is currently stopping', async () => { + expect(findButton().props('loading')).toBe(true); + }); }); }); diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index e75d3ac0321..fce30973547 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -477,7 +477,141 @@ export const resolvedEnvironment = { externalUrl: 'https://example.org', environmentType: 'review', nameWithoutType: 'hello', - lastDeployment: null, + lastDeployment: { + id: 78, + iid: 24, + sha: 'f3ba6dd84f8f891373e9b869135622b954852db1', + ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' }, + status: 'success', + createdAt: '2022-01-07T15:47:27.415Z', + deployedAt: '2022-01-07T15:47:32.450Z', + tag: false, + isLast: true, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + webUrl: 'http://gck.test:3000/root', + showStatus: false, + path: '/root', + }, + deployable: { + id: 1014, + name: 'deploy-prod', + started: '2022-01-07T15:47:31.037Z', + complete: true, + archived: false, + buildPath: '/h5bp/html5-boilerplate/-/jobs/1014', + retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry', + playable: false, + scheduled: false, + createdAt: '2022-01-07T15:47:27.404Z', + updatedAt: '2022-01-07T15:47:32.341Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + hasDetails: true, + detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/1014/retry', + method: 'post', + buttonTitle: 'Retry this job', + }, + }, + }, + commit: { + id: 'f3ba6dd84f8f891373e9b869135622b954852db1', + shortId: 'f3ba6dd8', + createdAt: '2022-01-07T15:47:26.000+00:00', + parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'], + title: 'Update .gitlab-ci.yml file', + message: 'Update .gitlab-ci.yml file', + authorName: 'Administrator', + authorEmail: 'admin@example.com', + authoredDate: '2022-01-07T15:47:26.000+00:00', + committerName: 'Administrator', + committerEmail: 'admin@example.com', + committedDate: '2022-01-07T15:47:26.000+00:00', + trailers: {}, + webUrl: + 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1', + author: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + webUrl: 'http://gck.test:3000/root', + showStatus: false, + path: '/root', + }, + authorGravatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commitUrl: + 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1', + commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1', + }, + manualActions: [ + { + id: 1015, + name: 'deploy-staging', + started: null, + complete: false, + archived: false, + buildPath: '/h5bp/html5-boilerplate/-/jobs/1015', + playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play', + playable: true, + scheduled: false, + createdAt: '2022-01-07T15:47:27.422Z', + updatedAt: '2022-01-07T15:47:28.557Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + hasDetails: true, + detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015', + illustration: { + image: + '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.', + }, + favicon: + '/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/h5bp/html5-boilerplate/-/jobs/1015/play', + method: 'post', + buttonTitle: 'Trigger this manual action', + }, + }, + }, + ], + scheduledActions: [], + cluster: null, + }, hasStopAction: false, rolloutStatus: null, environmentPath: '/h5bp/html5-boilerplate/-/environments/41', diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js index d8d26b74504..6b53dc24f0f 100644 --- a/spec/frontend/environments/graphql/resolvers_spec.js +++ b/spec/frontend/environments/graphql/resolvers_spec.js @@ -1,8 +1,10 @@ import MockAdapter from 'axios-mock-adapter'; +import { s__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { resolvers } from '~/environments/graphql/resolvers'; import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql'; import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql'; +import environmentToStopQuery from '~/environments/graphql/queries/environment_to_stop.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql'; @@ -210,4 +212,36 @@ describe('~/frontend/environments/graphql/resolvers', () => { }); }); }); + describe('setEnvironmentToStop', () => { + it('should write the given environment to the cache', () => { + localState.client.writeQuery = jest.fn(); + mockResolvers.Mutation.setEnvironmentToStop( + null, + { environment: resolvedEnvironment }, + localState, + ); + + expect(localState.client.writeQuery).toHaveBeenCalledWith({ + query: environmentToStopQuery, + data: { environmentToStop: resolvedEnvironment }, + }); + }); + }); + describe('action', () => { + it('should POST to the given path', async () => { + mock.onPost(ENDPOINT).reply(200); + const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } }); + + expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] }); + }); + it('should return a nice error message on fail', async () => { + mock.onPost(ENDPOINT).reply(500); + const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } }); + + expect(errors).toEqual({ + __typename: 'LocalEnvironmentErrors', + errors: [s__('Environments|An error occurred while making the request.')], + }); + }); + }); }); diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js index 27d27d5869a..6823c88a5a1 100644 --- a/spec/frontend/environments/new_environment_folder_spec.js +++ b/spec/frontend/environments/new_environment_folder_spec.js @@ -1,10 +1,13 @@ import VueApollo from 'vue-apollo'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { GlCollapse, GlIcon } from '@gitlab/ui'; +import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubTransition } from 'helpers/stub_transition'; import { __, s__ } from '~/locale'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; +import EnvironmentItem from '~/environments/components/new_environment_item.vue'; import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data'; Vue.use(VueApollo); @@ -25,13 +28,20 @@ describe('~/environments/components/new_environments_folder.vue', () => { }; const createWrapper = (propsData, apolloProvider) => - mountExtended(EnvironmentsFolder, { apolloProvider, propsData }); + mountExtended(EnvironmentsFolder, { + apolloProvider, + propsData, + stubs: { transition: stubTransition() }, + }); - beforeEach(() => { + beforeEach(async () => { environmentFolderMock = jest.fn(); [nestedEnvironment] = resolvedEnvironmentsApp.environments; environmentFolderMock.mockReturnValue(resolvedFolder); wrapper = createWrapper({ nestedEnvironment }, createApolloProvider()); + + await nextTick(); + await waitForPromises(); folderName = wrapper.findByText(nestedEnvironment.name); button = wrapper.findByRole('button', { name: __('Expand') }); }); @@ -57,7 +67,8 @@ describe('~/environments/components/new_environments_folder.vue', () => { const link = findLink(); expect(collapse.attributes('visible')).toBeUndefined(); - expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']); + const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2); + expect(iconNames).toEqual(['angle-right', 'folder-o']); expect(folderName.classes('gl-font-weight-bold')).toBe(false); expect(link.exists()).toBe(false); }); @@ -68,10 +79,21 @@ describe('~/environments/components/new_environments_folder.vue', () => { const link = findLink(); expect(button.attributes('aria-label')).toBe(__('Collapse')); - expect(collapse.attributes('visible')).toBe('true'); - expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']); + expect(collapse.attributes('visible')).toBe('visible'); + const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2); + expect(iconNames).toEqual(['angle-down', 'folder-open']); expect(folderName.classes('gl-font-weight-bold')).toBe(true); expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath); }); + + it('displays all environments when opened', async () => { + await button.trigger('click'); + + const names = resolvedFolder.environments.map((e) => + expect.stringMatching(e.nameWithoutType), + ); + const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text()); + expect(environments).toEqual(expect.arrayContaining(names)); + }); }); }); diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js new file mode 100644 index 00000000000..244aef5c43b --- /dev/null +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -0,0 +1,341 @@ +import VueApollo from 'vue-apollo'; +import Vue from 'vue'; +import { GlCollapse, GlIcon } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubTransition } from 'helpers/stub_transition'; +import { __, s__ } from '~/locale'; +import EnvironmentItem from '~/environments/components/new_environment_item.vue'; +import Deployment from '~/environments/components/deployment.vue'; +import { resolvedEnvironment } from './graphql/mock_data'; + +Vue.use(VueApollo); + +describe('~/environments/components/new_environment_item.vue', () => { + let wrapper; + + const createApolloProvider = () => { + return createMockApollo(); + }; + + const createWrapper = ({ propsData = {}, apolloProvider } = {}) => + mountExtended(EnvironmentItem, { + apolloProvider, + propsData: { environment: resolvedEnvironment, ...propsData }, + stubs: { transition: stubTransition() }, + }); + + const findDeployment = () => wrapper.findComponent(Deployment); + + afterEach(() => { + wrapper?.destroy(); + }); + + it('displays the name when not in a folder', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const name = wrapper.findByRole('link', { name: resolvedEnvironment.name }); + expect(name.exists()).toBe(true); + }); + + it('displays the name minus the folder prefix when in a folder', () => { + wrapper = createWrapper({ + propsData: { inFolder: true }, + apolloProvider: createApolloProvider(), + }); + + const name = wrapper.findByRole('link', { name: resolvedEnvironment.nameWithoutType }); + expect(name.exists()).toBe(true); + }); + + it('truncates the name if it is very long', () => { + const environment = { + ...resolvedEnvironment, + name: + 'this is a really long name that should be truncated because otherwise it would look strange in the UI', + }; + wrapper = createWrapper({ propsData: { environment }, apolloProvider: createApolloProvider() }); + + const name = wrapper.findByRole('link', { + name: (text) => environment.name.startsWith(text.slice(0, -1)), + }); + expect(name.exists()).toBe(true); + expect(name.text()).toHaveLength(80); + }); + + describe('url', () => { + it('shows a link for the url if one is present', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') }); + + expect(url.attributes('href')).toEqual(resolvedEnvironment.externalUrl); + }); + + it('does not show a link for the url if one is missing', () => { + wrapper = createWrapper({ + propsData: { environment: { ...resolvedEnvironment, externalUrl: '' } }, + apolloProvider: createApolloProvider(), + }); + + const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') }); + + expect(url.exists()).toBe(false); + }); + }); + + describe('actions', () => { + 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); + }); + + it('does not show a dropdown if there are no actions to perform', () => { + wrapper = createWrapper({ + propsData: { + environment: { + ...resolvedEnvironment, + lastDeployment: null, + }, + apolloProvider: createApolloProvider(), + }, + }); + + const actions = wrapper.findByRole('button', { name: __('Deploy to...') }); + + expect(actions.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); + }); + }); + + describe('stop', () => { + it('shows a buton to stop the environment if the environment is available', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') }); + + expect(stop.exists()).toBe(true); + }); + + it('does not show a buton to stop the environment if the environment is stopped', () => { + wrapper = createWrapper({ + propsData: { environment: { ...resolvedEnvironment, canStop: false } }, + apolloProvider: createApolloProvider(), + }); + + const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') }); + + expect(stop.exists()).toBe(false); + }); + }); + + describe('rollback', () => { + it('shows the option to rollback/re-deploy if available', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const rollback = wrapper.findByRole('menuitem', { + name: s__('Environments|Re-deploy to environment'), + }); + + expect(rollback.exists()).toBe(true); + }); + + it('does not show the option to rollback/re-deploy if not available', () => { + wrapper = createWrapper({ + propsData: { environment: { ...resolvedEnvironment, lastDeployment: null } }, + apolloProvider: createApolloProvider(), + }); + + const rollback = wrapper.findByRole('menuitem', { + name: s__('Environments|Re-deploy to environment'), + }); + + expect(rollback.exists()).toBe(false); + }); + }); + + describe('pin', () => { + it('shows the option to pin the environment if there is an autostop date', () => { + wrapper = createWrapper({ + propsData: { + environment: { ...resolvedEnvironment, autoStopAt: new Date(Date.now() + 100000) }, + }, + apolloProvider: createApolloProvider(), + }); + + const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); + + expect(rollback.exists()).toBe(true); + }); + + it('does not show the option to pin the environment if there is no autostop date', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); + + expect(rollback.exists()).toBe(false); + }); + }); + + describe('monitoring', () => { + it('shows the link to monitoring if metrics are set up', () => { + wrapper = createWrapper({ + propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } }, + apolloProvider: createApolloProvider(), + }); + + const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') }); + + expect(rollback.exists()).toBe(true); + }); + + it('does not show the link to monitoring if metrics are not set up', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') }); + + expect(rollback.exists()).toBe(false); + }); + }); + describe('terminal', () => { + it('shows the link to the terminal if set up', () => { + wrapper = createWrapper({ + propsData: { environment: { ...resolvedEnvironment, terminalPath: '/terminal' } }, + apolloProvider: createApolloProvider(), + }); + + const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') }); + + expect(rollback.exists()).toBe(true); + }); + + it('does not show the link to the terminal if not set up', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') }); + + expect(rollback.exists()).toBe(false); + }); + }); + + describe('delete', () => { + it('shows the button to delete the environment if possible', () => { + wrapper = createWrapper({ + propsData: { + environment: { ...resolvedEnvironment, canDelete: true, deletePath: '/terminal' }, + }, + apolloProvider: createApolloProvider(), + }); + + const rollback = wrapper.findByRole('menuitem', { + name: s__('Environments|Delete environment'), + }); + + expect(rollback.exists()).toBe(true); + }); + + it('does not show the button to delete the environment if not possible', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const rollback = wrapper.findByRole('menuitem', { + name: s__('Environments|Delete environment'), + }); + + expect(rollback.exists()).toBe(false); + }); + }); + + describe('collapse', () => { + let icon; + let collapse; + let button; + let environmentName; + + beforeEach(() => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + collapse = wrapper.findComponent(GlCollapse); + icon = wrapper.findComponent(GlIcon); + button = wrapper.findByRole('button', { name: __('Expand') }); + environmentName = wrapper.findByText(resolvedEnvironment.name); + }); + + it('is collapsed by default', () => { + expect(collapse.attributes('visible')).toBeUndefined(); + expect(icon.props('name')).toEqual('angle-right'); + expect(environmentName.classes('gl-font-weight-bold')).toBe(false); + }); + + it('opens on click', async () => { + expect(findDeployment().isVisible()).toBe(false); + + await button.trigger('click'); + + expect(button.attributes('aria-label')).toBe(__('Collapse')); + expect(collapse.attributes('visible')).toBe('visible'); + expect(icon.props('name')).toEqual('angle-down'); + expect(environmentName.classes('gl-font-weight-bold')).toBe(true); + expect(findDeployment().isVisible()).toBe(true); + }); + }); + describe('last deployment', () => { + it('should pass the last deployment to the deployment component when it exists', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const deployment = findDeployment(); + expect(deployment.props('deployment')).toEqual(resolvedEnvironment.lastDeployment); + }); + it('should not show the last deployment when it is missing', () => { + const environment = { + ...resolvedEnvironment, + lastDeployment: null, + }; + + wrapper = createWrapper({ + propsData: { environment }, + apolloProvider: createApolloProvider(), + }); + + const deployment = findDeployment(); + expect(deployment.exists()).toBe(false); + }); + }); + + describe('upcoming deployment', () => { + it('should pass the upcoming deployment to the deployment component when it exists', () => { + const upcomingDeployment = resolvedEnvironment.lastDeployment; + const environment = { ...resolvedEnvironment, lastDeployment: null, upcomingDeployment }; + wrapper = createWrapper({ + propsData: { environment }, + apolloProvider: createApolloProvider(), + }); + + const deployment = findDeployment(); + expect(deployment.props('deployment')).toEqual(upcomingDeployment); + }); + it('should not show the upcoming deployment when it is missing', () => { + const environment = { + ...resolvedEnvironment, + lastDeployment: null, + upcomingDeployment: null, + }; + + wrapper = createWrapper({ + propsData: { environment }, + apolloProvider: createApolloProvider(), + }); + + const deployment = findDeployment(); + expect(deployment.exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js index 1e9bd4d64c9..c9eccc26694 100644 --- a/spec/frontend/environments/new_environments_app_spec.js +++ b/spec/frontend/environments/new_environments_app_spec.js @@ -8,7 +8,9 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { sprintf, __, s__ } from '~/locale'; import EnvironmentsApp from '~/environments/components/new_environments_app.vue'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; -import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data'; +import EnvironmentsItem from '~/environments/components/new_environment_item.vue'; +import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue'; +import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data'; Vue.use(VueApollo); @@ -17,6 +19,7 @@ describe('~/environments/components/new_environments_app.vue', () => { let environmentAppMock; let environmentFolderMock; let paginationMock; + let environmentToStopMock; const createApolloProvider = () => { const mockResolvers = { @@ -24,6 +27,7 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentApp: environmentAppMock, folder: environmentFolderMock, pageInfo: paginationMock, + environmentToStop: environmentToStopMock, }, }; @@ -45,6 +49,7 @@ describe('~/environments/components/new_environments_app.vue', () => { provide = {}, environmentsApp, folder, + environmentToStop = {}, pageInfo = { total: 20, perPage: 5, @@ -58,6 +63,7 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentAppMock.mockReturnValue(environmentsApp); environmentFolderMock.mockReturnValue(folder); paginationMock.mockReturnValue(pageInfo); + environmentToStopMock.mockReturnValue(environmentToStop); const apolloProvider = createApolloProvider(); wrapper = createWrapper({ apolloProvider, provide }); @@ -68,6 +74,7 @@ describe('~/environments/components/new_environments_app.vue', () => { beforeEach(() => { environmentAppMock = jest.fn(); environmentFolderMock = jest.fn(); + environmentToStopMock = jest.fn(); paginationMock = jest.fn(); }); @@ -87,6 +94,18 @@ describe('~/environments/components/new_environments_app.vue', () => { expect(text).not.toContainEqual(expect.stringMatching('production')); }); + it('should show all the environments that are fetched', async () => { + await createWrapperWithMocked({ + environmentsApp: resolvedEnvironmentsApp, + folder: resolvedFolder, + }); + + const text = wrapper.findAllComponents(EnvironmentsItem).wrappers.map((w) => w.text()); + + expect(text).not.toContainEqual(expect.stringMatching('review')); + expect(text).toContainEqual(expect.stringMatching('production')); + }); + it('should show a button to create a new environment', async () => { await createWrapperWithMocked({ environmentsApp: resolvedEnvironmentsApp, @@ -168,13 +187,27 @@ describe('~/environments/components/new_environments_app.vue', () => { expect(environmentAppMock).toHaveBeenCalledWith( expect.anything(), - expect.objectContaining({ scope: 'stopped' }), + expect.objectContaining({ scope: 'stopped', page: 1 }), expect.anything(), expect.anything(), ); }); }); + describe('modals', () => { + it('should pass the environment to stop to the stop environment modal', async () => { + await createWrapperWithMocked({ + environmentsApp: resolvedEnvironmentsApp, + folder: resolvedFolder, + environmentToStop: resolvedEnvironment, + }); + + const modal = wrapper.findComponent(StopEnvironmentModal); + + expect(modal.props('environment')).toMatchObject(resolvedEnvironment); + }); + }); + describe('pagination', () => { it('should sync page from query params on load', async () => { await createWrapperWithMocked({ |