diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /spec/frontend/environments | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) | |
download | gitlab-ce-a7b3560714b4d9cc4ab32dffcd1f74a284b93580.tar.gz |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'spec/frontend/environments')
15 files changed, 857 insertions, 70 deletions
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js index 6c7a786e652..d58f9f9b8a2 100644 --- a/spec/frontend/environments/canary_ingress_spec.js +++ b/spec/frontend/environments/canary_ingress_spec.js @@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import CanaryIngress from '~/environments/components/canary_ingress.vue'; import { CANARY_UPDATE_MODAL } from '~/environments/constants'; +import { rolloutStatus } from './graphql/mock_data'; describe('/environments/components/canary_ingress.vue', () => { let wrapper; @@ -13,16 +14,18 @@ describe('/environments/components/canary_ingress.vue', () => { .at(x / 5) .vm.$emit('click'); - const createComponent = () => { + const createComponent = (props = {}, options = {}) => { wrapper = mount(CanaryIngress, { propsData: { canaryIngress: { canary_weight: 60, }, + ...props, }, directives: { GlModal: createMockDirective(), }, + ...options, }); }; @@ -94,9 +97,25 @@ describe('/environments/components/canary_ingress.vue', () => { }); it('is set to open the change modal', () => { - const options = canaryWeightDropdown.findAll(GlDropdownItem); - expect(options).toHaveLength(21); - options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString())); + canaryWeightDropdown + .findAll(GlDropdownItem) + .wrappers.forEach((w) => + expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }), + ); + }); + }); + + describe('graphql', () => { + beforeEach(() => { + createComponent({ + graphql: true, + canaryIngress: rolloutStatus.canaryIngress, + }); + }); + + it('shows the correct weight', () => { + const canaryWeightDropdown = wrapper.find('[data-testid="canary-weight"]'); + expect(canaryWeightDropdown.props('text')).toBe('50'); }); }); }); diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js index c7129ee1320..22d13558a84 100644 --- a/spec/frontend/environments/canary_update_modal_spec.js +++ b/spec/frontend/environments/canary_update_modal_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlModal } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue'; import updateCanaryIngress from '~/environments/graphql/mutations/update_canary_ingress.mutation.graphql'; @@ -86,7 +87,7 @@ describe('/environments/components/canary_update_modal.vue', () => { mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: [] } } }); modal.vm.$emit('primary'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findAlert().exists()).toBe(false); }); @@ -95,7 +96,7 @@ describe('/environments/components/canary_update_modal.vue', () => { mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: ['error'] } } }); modal.vm.$emit('primary'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findAlert().text()).toBe('error'); }); @@ -105,7 +106,7 @@ describe('/environments/components/canary_update_modal.vue', () => { modal.vm.$emit('primary'); await waitForPromises(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findAlert().text()).toBe('Something went wrong. Please try again later'); }); @@ -114,12 +115,12 @@ describe('/environments/components/canary_update_modal.vue', () => { mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: ['error'] } } }); modal.vm.$emit('primary'); - await wrapper.vm.$nextTick(); + await nextTick(); const alert = findAlert(); alert.vm.$emit('dismiss'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(alert.exists()).toBe(false); }); diff --git a/spec/frontend/environments/commit_spec.js b/spec/frontend/environments/commit_spec.js new file mode 100644 index 00000000000..32eb4b77528 --- /dev/null +++ b/spec/frontend/environments/commit_spec.js @@ -0,0 +1,71 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import Commit from '~/environments/components/commit.vue'; +import { resolvedEnvironment } from './graphql/mock_data'; + +describe('~/environments/components/commit.vue', () => { + let commit; + let wrapper; + + beforeEach(() => { + commit = resolvedEnvironment.lastDeployment.commit; + }); + + const createWrapper = ({ propsData = {} } = {}) => + mountExtended(Commit, { + propsData: { + commit, + ...propsData, + }, + }); + + afterEach(() => { + wrapper?.destroy(); + }); + + describe('with gitlab user', () => { + beforeEach(() => { + wrapper = createWrapper(); + }); + + it('links to the user profile', () => { + const link = wrapper.findByRole('link', { name: commit.author.name }); + expect(link.attributes('href')).toBe(commit.author.path); + }); + + it('displays the user avatar', () => { + const avatar = wrapper.findByRole('img', { name: 'avatar' }); + expect(avatar.attributes('src')).toBe(commit.author.avatarUrl); + }); + + it('links the commit message to the commit', () => { + const message = wrapper.findByRole('link', { name: commit.message }); + + expect(message.attributes('href')).toBe(commit.commitPath); + }); + }); + describe('without gitlab user', () => { + beforeEach(() => { + commit = { + ...commit, + author: null, + }; + wrapper = createWrapper(); + }); + + it('links to the user profile', () => { + const link = wrapper.findByRole('link', { name: commit.authorName }); + expect(link.attributes('href')).toBe(`mailto:${commit.authorEmail}`); + }); + + it('displays the user avatar', () => { + const avatar = wrapper.findByRole('img', { name: 'avatar' }); + expect(avatar.attributes('src')).toBe(commit.authorGravatarUrl); + }); + + it('displays the commit message', () => { + const message = wrapper.findByRole('link', { name: commit.message }); + + expect(message.attributes('href')).toBe(commit.commitPath); + }); + }); +}); diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js index 24e94867afd..f0fb4d1027c 100644 --- a/spec/frontend/environments/deploy_board_component_spec.js +++ b/spec/frontend/environments/deploy_board_component_spec.js @@ -1,9 +1,10 @@ import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import CanaryIngress from '~/environments/components/canary_ingress.vue'; import DeployBoard from '~/environments/components/deploy_board.vue'; import { deployBoardMockData, environment } from './mock_data'; +import { rolloutStatus } from './graphql/mock_data'; const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`; @@ -24,11 +25,11 @@ describe('Deploy Board', () => { describe('with valid data', () => { beforeEach((done) => { wrapper = createComponent(); - wrapper.vm.$nextTick(done); + nextTick(done); }); it('should render percentage with completion value provided', () => { - expect(wrapper.vm.$refs.percentage.innerText).toEqual(`${deployBoardMockData.completion}%`); + expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${deployBoardMockData.completion}%`); }); it('should render total instance count', () => { @@ -57,20 +58,74 @@ describe('Deploy Board', () => { it('sets up a tooltip for the legend', () => { const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]'); - const tooltip = wrapper.find(GlTooltip); - const icon = iconSpan.find(GlIcon); + const tooltip = wrapper.findComponent(GlTooltip); + const icon = iconSpan.findComponent(GlIcon); expect(tooltip.props('target')()).toBe(iconSpan.element); expect(icon.props('name')).toBe('question'); }); it('renders the canary weight selector', () => { - const canary = wrapper.find(CanaryIngress); + const canary = wrapper.findComponent(CanaryIngress); expect(canary.exists()).toBe(true); expect(canary.props('canaryIngress')).toEqual({ canary_weight: 50 }); }); }); + describe('with new valid data', () => { + beforeEach(async () => { + wrapper = createComponent({ + graphql: true, + deployBoardData: rolloutStatus, + }); + await nextTick(); + }); + + it('should render percentage with completion value provided', () => { + expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${rolloutStatus.completion}%`); + }); + + it('should render total instance count', () => { + const renderedTotal = wrapper.find('.deploy-board-instances-text'); + const actualTotal = rolloutStatus.instances.length; + const output = `${actualTotal > 1 ? 'Instances' : 'Instance'} (${actualTotal})`; + + expect(renderedTotal.text()).toEqual(output); + }); + + it('should render all instances', () => { + const instances = wrapper.findAll('.deploy-board-instances-container a'); + + expect(instances).toHaveLength(rolloutStatus.instances.length); + expect( + instances.at(1).classes(`deployment-instance-${rolloutStatus.instances[2].status}`), + ).toBe(true); + }); + + it('should render an abort and a rollback button with the provided url', () => { + const buttons = wrapper.findAll('.deploy-board-actions a'); + + expect(buttons.at(0).attributes('href')).toEqual(rolloutStatus.rollbackUrl); + expect(buttons.at(1).attributes('href')).toEqual(rolloutStatus.abortUrl); + }); + + it('sets up a tooltip for the legend', () => { + const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]'); + const tooltip = wrapper.findComponent(GlTooltip); + const icon = iconSpan.findComponent(GlIcon); + + expect(tooltip.props('target')()).toBe(iconSpan.element); + expect(icon.props('name')).toBe('question'); + }); + + it('renders the canary weight selector', () => { + const canary = wrapper.findComponent(CanaryIngress); + expect(canary.exists()).toBe(true); + expect(canary.props('canaryIngress')).toEqual({ canaryWeight: 50 }); + expect(canary.props('graphql')).toBe(true); + }); + }); + describe('with empty state', () => { beforeEach((done) => { wrapper = createComponent({ @@ -79,7 +134,7 @@ describe('Deploy Board', () => { isEmpty: true, logsPath, }); - wrapper.vm.$nextTick(done); + nextTick(done); }); it('should render the empty state', () => { @@ -98,11 +153,11 @@ describe('Deploy Board', () => { isEmpty: false, logsPath, }); - wrapper.vm.$nextTick(done); + nextTick(done); }); it('should render loading spinner', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); @@ -116,7 +171,7 @@ describe('Deploy Board', () => { deployBoardData: deployBoardMockData, }); ({ statuses } = wrapper.vm); - wrapper.vm.$nextTick(done); + nextTick(done); }); it('with all the possible statuses', () => { diff --git a/spec/frontend/environments/deploy_board_wrapper_spec.js b/spec/frontend/environments/deploy_board_wrapper_spec.js new file mode 100644 index 00000000000..c8e6df4d324 --- /dev/null +++ b/spec/frontend/environments/deploy_board_wrapper_spec.js @@ -0,0 +1,124 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlCollapse, GlIcon } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubTransition } from 'helpers/stub_transition'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { __, s__ } from '~/locale'; +import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue'; +import DeployBoard from '~/environments/components/deploy_board.vue'; +import setEnvironmentToChangeCanaryMutation from '~/environments/graphql/mutations/set_environment_to_change_canary.mutation.graphql'; +import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data'; + +Vue.use(VueApollo); + +describe('~/environments/components/deploy_board_wrapper.vue', () => { + let wrapper; + let mockApollo; + + const findDeployBoard = () => wrapper.findComponent(DeployBoard); + + const createWrapper = ({ propsData = {} } = {}) => { + mockApollo = createMockApollo(); + return mountExtended(DeployBoardWrapper, { + propsData: { environment: resolvedEnvironment, rolloutStatus, ...propsData }, + provide: { helpPagePath: '/help' }, + stubs: { transition: stubTransition() }, + apolloProvider: mockApollo, + }); + }; + + const expandCollapsedSection = async () => { + const button = wrapper.findByRole('button', { name: __('Expand') }); + await button.trigger('click'); + + return button; + }; + + afterEach(() => { + wrapper?.destroy(); + }); + + it('is labeled Kubernetes Pods', () => { + wrapper = createWrapper(); + + expect(wrapper.findByText(s__('DeployBoard|Kubernetes Pods')).exists()).toBe(true); + }); + + describe('collapse', () => { + let icon; + let collapse; + + beforeEach(() => { + wrapper = createWrapper(); + collapse = wrapper.findComponent(GlCollapse); + icon = wrapper.findComponent(GlIcon); + }); + + it('is collapsed by default', () => { + expect(collapse.attributes('visible')).toBeUndefined(); + expect(icon.props('name')).toBe('angle-right'); + }); + + it('opens on click', async () => { + const button = await expandCollapsedSection(); + + expect(button.attributes('aria-label')).toBe(__('Collapse')); + expect(collapse.attributes('visible')).toBe('visible'); + expect(icon.props('name')).toBe('angle-down'); + + const deployBoard = findDeployBoard(); + expect(deployBoard.exists()).toBe(true); + }); + }); + + describe('deploy board', () => { + it('passes the rollout status on and sets graphql to true', async () => { + wrapper = createWrapper(); + await expandCollapsedSection(); + + const deployBoard = findDeployBoard(); + expect(deployBoard.props('deployBoardData')).toEqual(rolloutStatus); + expect(deployBoard.props('graphql')).toBe(true); + }); + + it('sets the update to the canary via graphql', () => { + wrapper = createWrapper(); + jest.spyOn(mockApollo.defaultClient, 'mutate'); + const deployBoard = findDeployBoard(); + deployBoard.vm.$emit('changeCanaryWeight', 15); + expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({ + mutation: setEnvironmentToChangeCanaryMutation, + variables: { environment: resolvedEnvironment, weight: 15 }, + }); + }); + + describe('is loading', () => { + it('should set the loading prop', async () => { + wrapper = createWrapper({ + propsData: { rolloutStatus: { ...rolloutStatus, status: 'loading' } }, + }); + + await expandCollapsedSection(); + + const deployBoard = findDeployBoard(); + + expect(deployBoard.props('isLoading')).toBe(true); + }); + }); + + describe('is empty', () => { + it('should set the empty prop', async () => { + wrapper = createWrapper({ + propsData: { rolloutStatus: { ...rolloutStatus, status: 'not_found' } }, + }); + + await expandCollapsedSection(); + + const deployBoard = findDeployBoard(); + + expect(deployBoard.props('isEmpty')).toBe(true); + }); + }); + }); +}); diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js index 37209bdc86c..6cc363e000b 100644 --- a/spec/frontend/environments/deployment_spec.js +++ b/spec/frontend/environments/deployment_spec.js @@ -1,17 +1,32 @@ +import { GlCollapse } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { useFakeDate } from 'helpers/fake_date'; +import { stubTransition } from 'helpers/stub_transition'; +import { formatDate } from '~/lib/utils/datetime_utility'; +import { __, s__ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import Deployment from '~/environments/components/deployment.vue'; +import Commit from '~/environments/components/commit.vue'; import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue'; import { resolvedEnvironment } from './graphql/mock_data'; describe('~/environments/components/deployment.vue', () => { + useFakeDate(2022, 0, 8, 16); + + let deployment; let wrapper; + beforeEach(() => { + deployment = resolvedEnvironment.lastDeployment; + }); + const createWrapper = ({ propsData = {} } = {}) => mountExtended(Deployment, { propsData: { - deployment: resolvedEnvironment.lastDeployment, + deployment, ...propsData, }, + stubs: { transition: stubTransition() }, }); afterEach(() => { @@ -21,9 +36,229 @@ describe('~/environments/components/deployment.vue', () => { describe('status', () => { it('should pass the deployable status to the badge', () => { wrapper = createWrapper(); - expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe( - resolvedEnvironment.lastDeployment.status, - ); + expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe(deployment.status); + }); + }); + + describe('latest', () => { + it('should show a badge if the deployment is latest', () => { + wrapper = createWrapper({ propsData: { latest: true } }); + + const badge = wrapper.findByText(s__('Deployment|Latest Deployed')); + + expect(badge.exists()).toBe(true); + }); + + it('should not show a badge if the deployment is not latest', () => { + wrapper = createWrapper(); + + const badge = wrapper.findByText(s__('Deployment|Latest Deployed')); + + expect(badge.exists()).toBe(false); + }); + }); + + describe('iid', () => { + const findIid = () => wrapper.findByTitle(s__('Deployment|Deployment ID')); + const findDeploymentIcon = () => wrapper.findComponent({ ref: 'deployment-iid-icon' }); + + describe('is present', () => { + beforeEach(() => { + wrapper = createWrapper(); + }); + + it('should show the iid', () => { + const iid = findIid(); + expect(iid.exists()).toBe(true); + }); + + it('should show an icon for the iid', () => { + const deploymentIcon = findDeploymentIcon(); + expect(deploymentIcon.props('name')).toBe('deployments'); + }); + }); + + describe('is not present', () => { + beforeEach(() => { + wrapper = createWrapper({ propsData: { deployment: { ...deployment, iid: '' } } }); + }); + + it('should not show the iid', () => { + const iid = findIid(); + expect(iid.exists()).toBe(false); + }); + + it('should not show an icon for the iid', () => { + const deploymentIcon = findDeploymentIcon(); + expect(deploymentIcon.exists()).toBe(false); + }); + }); + }); + + describe('shortSha', () => { + describe('is present', () => { + beforeEach(() => { + wrapper = createWrapper(); + }); + + it('shows the short SHA for the commit of the deployment', () => { + const sha = wrapper.findByTitle(__('Commit SHA')); + + expect(sha.exists()).toBe(true); + expect(sha.text()).toBe(deployment.commit.shortId); + }); + + it('shows the commit icon', () => { + const icon = wrapper.findComponent({ ref: 'deployment-commit-icon' }); + expect(icon.props('name')).toBe('commit'); + }); + + it('shows a copy button for the sha', () => { + const button = wrapper.findComponent(ClipboardButton); + expect(button.props()).toMatchObject({ + text: deployment.commit.shortId, + title: __('Copy commit SHA'), + }); + }); + }); + + describe('is not present', () => { + it('does not show the short SHA for the commit of the deployment', () => { + wrapper = createWrapper({ + propsData: { + deployment: { + ...deployment, + commit: null, + }, + }, + }); + const sha = wrapper.findByTestId('deployment-commit-sha'); + expect(sha.exists()).toBe(false); + }); + }); + }); + + describe('created at time', () => { + describe('is present', () => { + it('shows the timestamp the deployment was deployed at', () => { + wrapper = createWrapper(); + const date = wrapper.findByTitle(formatDate(deployment.createdAt)); + + expect(date.text()).toBe('1 day ago'); + }); + }); + describe('is not present', () => { + it('does not show the timestamp', () => { + wrapper = createWrapper({ propsData: { deployment: { ...deployment, createdAt: null } } }); + const date = wrapper.findByTitle(formatDate(deployment.createdAt)); + + expect(date.exists()).toBe(false); + }); + }); + }); + + describe('commit message', () => { + describe('with commit', () => { + beforeEach(() => { + wrapper = createWrapper(); + }); + + it('shows the commit component', () => { + const commit = wrapper.findComponent(Commit); + expect(commit.props('commit')).toBe(deployment.commit); + }); + }); + + describe('without a commit', () => { + it('displays nothing', () => { + const noCommit = { + ...deployment, + commit: null, + }; + wrapper = createWrapper({ propsData: { deployment: noCommit } }); + + const commit = wrapper.findComponent(Commit); + expect(commit.exists()).toBe(false); + }); + }); + }); + + describe('collapse', () => { + let collapse; + let button; + + beforeEach(() => { + wrapper = createWrapper(); + collapse = wrapper.findComponent(GlCollapse); + button = wrapper.findComponent({ ref: 'details-toggle' }); + }); + + it('is collapsed by default', () => { + expect(collapse.attributes('visible')).toBeUndefined(); + expect(button.props('icon')).toBe('expand-down'); + expect(button.text()).toBe(__('Show details')); + }); + + it('opens on click', async () => { + await button.trigger('click'); + + expect(button.text()).toBe(__('Hide details')); + expect(button.props('icon')).toBe('expand-up'); + expect(collapse.attributes('visible')).toBe('visible'); + + const username = wrapper.findByRole('link', { name: `@${deployment.user.username}` }); + + expect(username.attributes('href')).toBe(deployment.user.path); + const job = wrapper.findByRole('link', { name: deployment.deployable.name }); + expect(job.attributes('href')).toBe(deployment.deployable.buildPath); + const apiBadge = wrapper.findByText(__('API')); + expect(apiBadge.exists()).toBe(false); + + const branchLabel = wrapper.findByText(__('Branch')); + expect(branchLabel.exists()).toBe(true); + const tagLabel = wrapper.findByText(__('Tag')); + expect(tagLabel.exists()).toBe(false); + const ref = wrapper.findByRole('link', { name: deployment.ref.name }); + expect(ref.attributes('href')).toBe(deployment.ref.refPath); + }); + }); + + describe('with tagged deployment', () => { + beforeEach(async () => { + wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } }); + await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); + }); + + it('shows tag instead of branch', () => { + const refLabel = wrapper.findByText(__('Tag')); + expect(refLabel.exists()).toBe(true); + }); + }); + + describe('with API deployment', () => { + beforeEach(async () => { + wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } }); + await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); + }); + + it('shows API instead of a job name', () => { + const apiBadge = wrapper.findByText(__('API')); + expect(apiBadge.exists()).toBe(true); + }); + }); + describe('without a job path', () => { + beforeEach(async () => { + wrapper = createWrapper({ + propsData: { + deployment: { ...deployment, deployable: { name: deployment.deployable.name } }, + }, + }); + await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); + }); + + it('shows a span instead of a link', () => { + const job = wrapper.findByTitle(deployment.deployable.name); + expect(job.attributes('href')).toBeUndefined(); }); }); }); diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js index 1b68a692db8..336c207428e 100644 --- a/spec/frontend/environments/environment_actions_spec.js +++ b/spec/frontend/environments/environment_actions_spec.js @@ -1,6 +1,6 @@ import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { TEST_HOST } from 'helpers/test_constants'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; @@ -108,7 +108,7 @@ describe('EnvironmentActions Component', () => { jest.spyOn(window, 'confirm').mockImplementation(() => confirm); findDropdownItem(scheduledJobAction).vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); }; beforeEach(() => { diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js index a9a58071e12..669c974ea4f 100644 --- a/spec/frontend/environments/environment_pin_spec.js +++ b/spec/frontend/environments/environment_pin_spec.js @@ -1,5 +1,9 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import cancelAutoStopMutation from '~/environments/graphql/mutations/cancel_auto_stop.mutation.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; import PinComponent from '~/environments/components/environment_pin.vue'; import eventHub from '~/environments/event_hub'; @@ -18,28 +22,66 @@ describe('Pin Component', () => { const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop'; - beforeEach(() => { - factory({ - propsData: { - autoStopUrl, - }, + describe('without graphql', () => { + beforeEach(() => { + factory({ + propsData: { + autoStopUrl, + }, + }); }); - }); - afterEach(() => { - wrapper.destroy(); - }); + afterEach(() => { + wrapper.destroy(); + }); - it('should render the component with descriptive text', () => { - expect(wrapper.text()).toBe('Prevent auto-stopping'); + it('should render the component with descriptive text', () => { + expect(wrapper.text()).toBe('Prevent auto-stopping'); + }); + + it('should emit onPinClick when clicked', () => { + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + const item = wrapper.find(GlDropdownItem); + + item.vm.$emit('click'); + + expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl); + }); }); - it('should emit onPinClick when clicked', () => { - const eventHubSpy = jest.spyOn(eventHub, '$emit'); - const item = wrapper.find(GlDropdownItem); + describe('with graphql', () => { + Vue.use(VueApollo); + let mockApollo; - item.vm.$emit('click'); + beforeEach(() => { + mockApollo = createMockApollo(); + factory({ + propsData: { + autoStopUrl, + graphql: true, + }, + apolloProvider: mockApollo, + }); + }); - expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl); + afterEach(() => { + wrapper.destroy(); + }); + + it('should render the component with descriptive text', () => { + expect(wrapper.text()).toBe('Prevent auto-stopping'); + }); + + it('should emit onPinClick when clicked', () => { + jest.spyOn(mockApollo.defaultClient, 'mutate'); + const item = wrapper.find(GlDropdownItem); + + item.vm.$emit('click'); + + expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({ + mutation: cancelAutoStopMutation, + variables: { autoStopUrl }, + }); + }); }); }); diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js index 1851163ac68..c7582e4b06d 100644 --- a/spec/frontend/environments/environment_table_spec.js +++ b/spec/frontend/environments/environment_table_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue'; import DeployBoard from '~/environments/components/deploy_board.vue'; import EnvironmentTable from '~/environments/components/environments_table.vue'; @@ -181,7 +182,7 @@ describe('Environment table', () => { }); wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({ weight: 40, diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js index cd05ecbfb53..92d1820681c 100644 --- a/spec/frontend/environments/environments_app_spec.js +++ b/spec/frontend/environments/environments_app_spec.js @@ -1,6 +1,7 @@ import { GlTabs } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import Container from '~/environments/components/container.vue'; import DeployBoard from '~/environments/components/deploy_board.vue'; @@ -186,14 +187,13 @@ describe('Environment', () => { expect(wrapper.find('.folder-icon[data-testid="chevron-right-icon"]').exists()).toBe(false); }); - it('should close an opened folder', () => { + it('should close an opened folder', async () => { expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(true); // close folder wrapper.find('.folder-name').trigger('click'); - wrapper.vm.$nextTick(() => { - expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(false); - }); + await nextTick(); + expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(false); }); it('should show children environments', () => { diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index fce30973547..1b7b35702de 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -1,3 +1,69 @@ +export const rolloutStatus = { + instances: [ + { + status: 'succeeded', + tooltip: 'tanuki-2334 Finished', + podName: 'production-tanuki-1', + stable: false, + }, + { + status: 'succeeded', + tooltip: 'tanuki-2335 Finished', + podName: 'production-tanuki-1', + stable: false, + }, + { + status: 'succeeded', + tooltip: 'tanuki-2336 Finished', + podName: 'production-tanuki-1', + stable: false, + }, + { + status: 'succeeded', + tooltip: 'tanuki-2337 Finished', + podName: 'production-tanuki-1', + stable: false, + }, + { + status: 'succeeded', + tooltip: 'tanuki-2338 Finished', + podName: 'production-tanuki-1', + stable: false, + }, + { + status: 'succeeded', + tooltip: 'tanuki-2339 Finished', + podName: 'production-tanuki-1', + stable: false, + }, + { status: 'succeeded', tooltip: 'tanuki-2340 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2334 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2335 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2336 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2337 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2338 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2339 Finished', podName: 'production-tanuki-1' }, + { status: 'succeeded', tooltip: 'tanuki-2340 Finished', podName: 'production-tanuki-1' }, + { status: 'running', tooltip: 'tanuki-2341 Deploying', podName: 'production-tanuki-1' }, + { status: 'running', tooltip: 'tanuki-2342 Deploying', podName: 'production-tanuki-1' }, + { status: 'running', tooltip: 'tanuki-2343 Deploying', podName: 'production-tanuki-1' }, + { status: 'failed', tooltip: 'tanuki-2344 Failed', podName: 'production-tanuki-1' }, + { status: 'unknown', tooltip: 'tanuki-2345 Ready', podName: 'production-tanuki-1' }, + { status: 'unknown', tooltip: 'tanuki-2346 Ready', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2348 Preparing', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2349 Preparing', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2350 Preparing', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2353 Preparing', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2354 waiting', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2355 waiting', podName: 'production-tanuki-1' }, + { status: 'pending', tooltip: 'tanuki-2356 waiting', podName: 'production-tanuki-1' }, + ], + abortUrl: 'url', + rollbackUrl: 'url', + completion: 100, + status: 'found', + canaryIngress: { canaryWeight: 50 }, +}; export const environmentsApp = { environments: [ { diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js index 6b53dc24f0f..21d7e09bad5 100644 --- a/spec/frontend/environments/graphql/resolvers_spec.js +++ b/spec/frontend/environments/graphql/resolvers_spec.js @@ -173,9 +173,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { it('should post to the auto stop path', async () => { mock.onPost(ENDPOINT).reply(200); - await mockResolvers.Mutation.cancelAutoStop(null, { - environment: { autoStopPath: ENDPOINT }, - }); + await mockResolvers.Mutation.cancelAutoStop(null, { autoStopUrl: ENDPOINT }); expect(mock.history.post).toContainEqual( expect.objectContaining({ url: ENDPOINT, method: 'post' }), diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js index 6823c88a5a1..460263587be 100644 --- a/spec/frontend/environments/new_environment_folder_spec.js +++ b/spec/frontend/environments/new_environment_folder_spec.js @@ -32,6 +32,7 @@ describe('~/environments/components/new_environments_folder.vue', () => { apolloProvider, propsData, stubs: { transition: stubTransition() }, + provide: { helpPagePath: '/help' }, }); beforeEach(async () => { diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js index 244aef5c43b..db596688dad 100644 --- a/spec/frontend/environments/new_environment_item_spec.js +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -2,12 +2,14 @@ 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 { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import { stubTransition } from 'helpers/stub_transition'; -import { __, s__ } from '~/locale'; +import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; +import { __, s__, sprintf } from '~/locale'; import EnvironmentItem from '~/environments/components/new_environment_item.vue'; import Deployment from '~/environments/components/deployment.vue'; -import { resolvedEnvironment } from './graphql/mock_data'; +import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue'; +import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data'; Vue.use(VueApollo); @@ -22,11 +24,19 @@ describe('~/environments/components/new_environment_item.vue', () => { mountExtended(EnvironmentItem, { apolloProvider, propsData: { environment: resolvedEnvironment, ...propsData }, + provide: { helpPagePath: '/help' }, stubs: { transition: stubTransition() }, }); const findDeployment = () => wrapper.findComponent(Deployment); + const expandCollapsedSection = async () => { + const button = wrapper.findByRole('button', { name: __('Expand') }); + await button.trigger('click'); + + return button; + }; + afterEach(() => { wrapper?.destroy(); }); @@ -165,25 +175,92 @@ describe('~/environments/components/new_environment_item.vue', () => { }); 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(), + describe('with autostop', () => { + let environment; + + beforeEach(() => { + environment = { + ...resolvedEnvironment, + autoStopAt: new Date(Date.now() + 100000).toString(), + }; + wrapper = createWrapper({ + propsData: { + environment, + }, + apolloProvider: createApolloProvider(), + }); }); - const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); + it('shows the option to pin the environment if there is an autostop date', () => { + const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); - expect(rollback.exists()).toBe(true); + expect(pin.exists()).toBe(true); + }); + + it('shows when the environment auto stops', () => { + const autoStop = wrapper.findByTitle(formatDate(environment.autoStopAt)); + + expect(autoStop.text()).toBe('in 1 minute'); + }); }); - it('does not show the option to pin the environment if there is no autostop date', () => { - wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + describe('without autostop', () => { + beforeEach(() => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + }); - const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); + it('does not show the option to pin the environment if there is no autostop date', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); - expect(rollback.exists()).toBe(false); + const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); + + expect(pin.exists()).toBe(false); + }); + + it('does not show when the environment auto stops', () => { + const autoStop = wrapper.findByText( + sprintf(s__('Environment|Auto stop %{time}'), { + time: getTimeago().format(resolvedEnvironment.autoStopAt), + }), + ); + + expect(autoStop.exists()).toBe(false); + }); + }); + + describe('with past autostop', () => { + let environment; + + beforeEach(() => { + environment = { + ...resolvedEnvironment, + autoStopAt: new Date(Date.now() - 100000).toString(), + }; + wrapper = createWrapper({ + propsData: { + environment, + }, + apolloProvider: createApolloProvider(), + }); + }); + + it('does not show the option to pin the environment if there is no autostop date', () => { + wrapper = createWrapper({ apolloProvider: createApolloProvider() }); + + const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') }); + + expect(pin.exists()).toBe(false); + }); + + it('does not show when the environment auto stops', () => { + const autoStop = wrapper.findByText( + sprintf(s__('Environment|Auto stop %{time}'), { + time: getTimeago().format(environment.autoStopAt), + }), + ); + + expect(autoStop.exists()).toBe(false); + }); }); }); @@ -258,14 +335,12 @@ describe('~/environments/components/new_environment_item.vue', () => { 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); }); @@ -278,7 +353,7 @@ describe('~/environments/components/new_environment_item.vue', () => { it('opens on click', async () => { expect(findDeployment().isVisible()).toBe(false); - await button.trigger('click'); + const button = await expandCollapsedSection(); expect(button.attributes('aria-label')).toBe(__('Collapse')); expect(collapse.attributes('visible')).toBe('visible'); @@ -338,4 +413,78 @@ describe('~/environments/components/new_environment_item.vue', () => { expect(deployment.exists()).toBe(false); }); }); + + describe('empty state', () => { + it('should link to documentation', async () => { + const environment = { + ...resolvedEnvironment, + lastDeployment: null, + upcomingDeployment: null, + }; + + wrapper = createWrapper({ + propsData: { environment }, + apolloProvider: createApolloProvider(), + }); + + await expandCollapsedSection(); + + const text = s__( + 'Environments|There are no deployments for this environment yet. Learn more about setting up deployments.', + ); + + const emptyState = wrapper.findByText((_content, element) => element.textContent === text); + + const link = extendedWrapper(emptyState).findByRole('link'); + + expect(link.attributes('href')).toBe('/help'); + }); + + it('should not link to the documentation when there are deployments', async () => { + wrapper = createWrapper({ + apolloProvider: createApolloProvider(), + }); + + await expandCollapsedSection(); + + const text = s__( + 'Environments|There are no deployments for this environment yet. Learn more about setting up deployments.', + ); + + const emptyState = wrapper.findByText((_content, element) => element.textContent === text); + + expect(emptyState.exists()).toBe(false); + }); + }); + + describe('deploy boards', () => { + it('should show a deploy board if the environment has a rollout status', async () => { + const environment = { + ...resolvedEnvironment, + rolloutStatus, + }; + + wrapper = createWrapper({ + propsData: { environment }, + apolloProvider: createApolloProvider(), + }); + + await expandCollapsedSection(); + + const deployBoard = wrapper.findComponent(DeployBoardWrapper); + expect(deployBoard.exists()).toBe(true); + expect(deployBoard.props('rolloutStatus')).toBe(rolloutStatus); + }); + + it('should not show a deploy board if the environment has no rollout status', async () => { + wrapper = createWrapper({ + apolloProvider: createApolloProvider(), + }); + + await expandCollapsedSection(); + + const deployBoard = wrapper.findComponent(DeployBoardWrapper); + expect(deployBoard.exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js index c9eccc26694..42e3608109b 100644 --- a/spec/frontend/environments/new_environments_app_spec.js +++ b/spec/frontend/environments/new_environments_app_spec.js @@ -10,6 +10,7 @@ import EnvironmentsApp from '~/environments/components/new_environments_app.vue' import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; import EnvironmentsItem from '~/environments/components/new_environment_item.vue'; import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue'; +import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue'; import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data'; Vue.use(VueApollo); @@ -20,6 +21,8 @@ describe('~/environments/components/new_environments_app.vue', () => { let environmentFolderMock; let paginationMock; let environmentToStopMock; + let environmentToChangeCanaryMock; + let weightMock; const createApolloProvider = () => { const mockResolvers = { @@ -28,6 +31,10 @@ describe('~/environments/components/new_environments_app.vue', () => { folder: environmentFolderMock, pageInfo: paginationMock, environmentToStop: environmentToStopMock, + environmentToDelete: jest.fn().mockResolvedValue(resolvedEnvironment), + environmentToRollback: jest.fn().mockResolvedValue(resolvedEnvironment), + environmentToChangeCanary: environmentToChangeCanaryMock, + weight: weightMock, }, }; @@ -40,6 +47,7 @@ describe('~/environments/components/new_environments_app.vue', () => { newEnvironmentPath: '/environments/new', canCreateEnvironment: true, defaultBranchName: 'main', + helpPagePath: '/help', ...provide, }, apolloProvider, @@ -50,6 +58,8 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentsApp, folder, environmentToStop = {}, + environmentToChangeCanary = {}, + weight = 0, pageInfo = { total: 20, perPage: 5, @@ -64,6 +74,8 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentFolderMock.mockReturnValue(folder); paginationMock.mockReturnValue(pageInfo); environmentToStopMock.mockReturnValue(environmentToStop); + environmentToChangeCanaryMock.mockReturnValue(environmentToChangeCanary); + weightMock.mockReturnValue(weight); const apolloProvider = createApolloProvider(); wrapper = createWrapper({ apolloProvider, provide }); @@ -75,11 +87,13 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentAppMock = jest.fn(); environmentFolderMock = jest.fn(); environmentToStopMock = jest.fn(); + environmentToChangeCanaryMock = jest.fn(); + weightMock = jest.fn(); paginationMock = jest.fn(); }); afterEach(() => { - wrapper?.destroy(); + wrapper.destroy(); }); it('should show all the folders that are fetched', async () => { @@ -149,8 +163,6 @@ describe('~/environments/components/new_environments_app.vue', () => { }, folder: resolvedFolder, }); - await waitForPromises(); - await nextTick(); const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') }); expect(button.exists()).toBe(false); @@ -206,6 +218,19 @@ describe('~/environments/components/new_environments_app.vue', () => { expect(modal.props('environment')).toMatchObject(resolvedEnvironment); }); + + it('should pass the environment to change canary to the canary update modal', async () => { + await createWrapperWithMocked({ + environmentsApp: resolvedEnvironmentsApp, + folder: resolvedFolder, + environmentToChangeCanary: resolvedEnvironment, + weight: 10, + }); + + const modal = wrapper.findComponent(CanaryUpdateModal); + + expect(modal.props('environment')).toMatchObject(resolvedEnvironment); + }); }); describe('pagination', () => { |