diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /spec/frontend | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'spec/frontend')
667 files changed, 10900 insertions, 5552 deletions
diff --git a/spec/frontend/__helpers__/mock_apollo_helper.js b/spec/frontend/__helpers__/mock_apollo_helper.js index bae9f33be87..e0739df7086 100644 --- a/spec/frontend/__helpers__/mock_apollo_helper.js +++ b/spec/frontend/__helpers__/mock_apollo_helper.js @@ -8,7 +8,6 @@ export function createMockClient(handlers = [], resolvers = {}, cacheOptions = { const cache = new InMemoryCache({ possibleTypes, typePolicies, - addTypename: false, ...cacheOptions, }); diff --git a/spec/frontend/__helpers__/mock_dom_observer.js b/spec/frontend/__helpers__/mock_dom_observer.js index bc2646be4c2..8c9c435041e 100644 --- a/spec/frontend/__helpers__/mock_dom_observer.js +++ b/spec/frontend/__helpers__/mock_dom_observer.js @@ -22,14 +22,12 @@ class MockObserver { takeRecords() {} - // eslint-disable-next-line camelcase $_triggerObserve(node, { entry = {}, options = {} } = {}) { if (this.$_hasObserver(node, options)) { this.$_cb([{ target: node, ...entry }]); } } - // eslint-disable-next-line camelcase $_hasObserver(node, options = {}) { return this.$_observers.some( ([obvNode, obvOptions]) => node === obvNode && isMatch(options, obvOptions), diff --git a/spec/frontend/__helpers__/mocks/axios_utils.js b/spec/frontend/__helpers__/mocks/axios_utils.js index b1efd29dc8d..60644c84a57 100644 --- a/spec/frontend/__helpers__/mocks/axios_utils.js +++ b/spec/frontend/__helpers__/mocks/axios_utils.js @@ -1,4 +1,6 @@ import EventEmitter from 'events'; +// eslint-disable-next-line no-restricted-syntax +import { setImmediate } from 'timers'; const axios = jest.requireActual('~/lib/utils/axios_utils').default; diff --git a/spec/frontend/__helpers__/stub_component.js b/spec/frontend/__helpers__/stub_component.js index 96fe3a8bc45..4f9d1ee6f5d 100644 --- a/spec/frontend/__helpers__/stub_component.js +++ b/spec/frontend/__helpers__/stub_component.js @@ -22,6 +22,14 @@ const createStubbedMethods = (methods = {}) => { ); }; +export const RENDER_ALL_SLOTS_TEMPLATE = `<div> + <template v-for="(_, name) in $scopedSlots"> + <div :data-testid="'slot-' + name"> + <slot :name="name" /> + </div> + </template> +</div>`; + export function stubComponent(Component, options = {}) { return { props: Component.props, diff --git a/spec/frontend/__helpers__/timeout.js b/spec/frontend/__helpers__/timeout.js deleted file mode 100644 index 8688625a95e..00000000000 --- a/spec/frontend/__helpers__/timeout.js +++ /dev/null @@ -1,59 +0,0 @@ -const NS_PER_SEC = 1e9; -const NS_PER_MS = 1e6; -const IS_DEBUGGING = process.execArgv.join(' ').includes('--inspect-brk'); - -let testTimeoutNS; - -export const setTestTimeout = (newTimeoutMS) => { - const newTimeoutNS = newTimeoutMS * NS_PER_MS; - // never accept a smaller timeout than the default - if (newTimeoutNS < testTimeoutNS) { - return; - } - - testTimeoutNS = newTimeoutNS; - jest.setTimeout(newTimeoutMS); -}; - -// Allows slow tests to set their own timeout. -// Useful for tests with jQuery, which is very slow in big DOMs. -let temporaryTimeoutNS = null; -export const setTestTimeoutOnce = (newTimeoutMS) => { - const newTimeoutNS = newTimeoutMS * NS_PER_MS; - // never accept a smaller timeout than the default - if (newTimeoutNS < testTimeoutNS) { - return; - } - - temporaryTimeoutNS = newTimeoutNS; -}; - -export const initializeTestTimeout = (defaultTimeoutMS) => { - setTestTimeout(defaultTimeoutMS); - - let testStartTime; - - // https://github.com/facebook/jest/issues/6947 - beforeEach(() => { - testStartTime = process.hrtime(); - }); - - afterEach(() => { - let timeoutNS = testTimeoutNS; - if (Number.isFinite(temporaryTimeoutNS)) { - timeoutNS = temporaryTimeoutNS; - temporaryTimeoutNS = null; - } - - const [seconds, remainingNs] = process.hrtime(testStartTime); - const elapsedNS = seconds * NS_PER_SEC + remainingNs; - - // Disable the timeout error when debugging. It is meaningless because - // debugging always takes longer than the test timeout. - if (elapsedNS > timeoutNS && !IS_DEBUGGING) { - throw new Error( - `Test took too long (${elapsedNS / NS_PER_MS}ms > ${timeoutNS / NS_PER_MS}ms)!`, - ); - } - }); -}; diff --git a/spec/frontend/__helpers__/vue_mount_component_helper.js b/spec/frontend/__helpers__/vue_mount_component_helper.js index 615ff69a01c..ed43355ea5b 100644 --- a/spec/frontend/__helpers__/vue_mount_component_helper.js +++ b/spec/frontend/__helpers__/vue_mount_component_helper.js @@ -1,5 +1,3 @@ -import Vue from 'vue'; - /** * Deprecated. Please do not use. * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445 @@ -33,31 +31,4 @@ export const mountComponentWithStore = (Component, { el, props, store }) => * Deprecated. Please do not use. * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445 */ -export const mountComponentWithSlots = (Component, { props, slots }) => { - const component = new Component({ - propsData: props || {}, - }); - - component.$slots = slots; - - return component.$mount(); -}; - -/** - * Mount a component with the given render method. - * - * ----------------------------- - * Deprecated. Please do not use. - * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445 - * ----------------------------- - * - * This helps with inserting slots that need to be compiled. - */ -export const mountComponentWithRender = (render, el = null) => - mountComponent(Vue.extend({ render }), {}, el); - -/** - * Deprecated. Please do not use. - * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445 - */ export default mountComponent; diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js index 2aae91f8a39..75bd5df8cbf 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper.js @@ -7,6 +7,20 @@ const vNodeContainsText = (vnode, text) => (vnode.children && vnode.children.filter((child) => vNodeContainsText(child, text)).length); /** + * Create a VTU wrapper from an element. + * + * If a Vue instance manages the element, the wrapper is created + * with that Vue instance. + * + * @param {HTMLElement} element + * @param {Object} options + * @returns VTU wrapper + */ +const createWrapperFromElement = (element, options) => + // eslint-disable-next-line no-underscore-dangle + createWrapper(element.__vue__ || element, options || {}); + +/** * Determines whether a `shallowMount` Wrapper contains text * within one of it's slots. This will also work on Wrappers * acquired with `find()`, but only if it's parent Wrapper @@ -85,8 +99,7 @@ export const extendedWrapper = (wrapper) => { if (!elements.length) { return new ErrorWrapper(query); } - - return createWrapper(elements[0], this.options || {}); + return createWrapperFromElement(elements[0], this.options); }, }, }; @@ -104,7 +117,7 @@ export const extendedWrapper = (wrapper) => { ); const wrappers = elements.map((element) => { - const elementWrapper = createWrapper(element, this.options || {}); + const elementWrapper = createWrapperFromElement(element, this.options); elementWrapper.selector = text; return elementWrapper; diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js index 3bb228f94b8..ae180c3b49d 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js @@ -6,6 +6,7 @@ import { WrapperArray as VTUWrapperArray, ErrorWrapper as VTUErrorWrapper, } from '@vue/test-utils'; +import Vue from 'vue'; import { extendedWrapper, shallowMountExtended, @@ -139,9 +140,12 @@ describe('Vue test utils helpers', () => { const text = 'foo bar'; const options = { selector: 'div' }; const mockDiv = document.createElement('div'); + const mockVm = new Vue({ render: (h) => h('div') }).$mount(); let wrapper; beforeEach(() => { + jest.spyOn(vtu, 'createWrapper'); + wrapper = extendedWrapper( shallowMount({ template: `<div>foo bar</div>`, @@ -164,7 +168,6 @@ describe('Vue test utils helpers', () => { describe('when element is found', () => { beforeEach(() => { jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]); - jest.spyOn(vtu, 'createWrapper'); }); it('returns a VTU wrapper', () => { @@ -172,14 +175,27 @@ describe('Vue test utils helpers', () => { expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeUndefined(); }); }); + describe('when a Vue instance element is found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockVm.$el]); + }); + + it('returns a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeInstanceOf(Vue); + }); + }); describe('when multiple elements are found', () => { beforeEach(() => { const mockSpan = document.createElement('span'); jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]); - jest.spyOn(vtu, 'createWrapper'); }); it('returns the first element as a VTU wrapper', () => { @@ -187,6 +203,24 @@ describe('Vue test utils helpers', () => { expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeUndefined(); + }); + }); + + describe('when multiple Vue instances are found', () => { + beforeEach(() => { + const mockVm2 = new Vue({ render: (h) => h('span') }).$mount(); + jest + .spyOn(testingLibrary, expectedQuery) + .mockImplementation(() => [mockVm.$el, mockVm2.$el]); + }); + + it('returns the first element as a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeInstanceOf(Vue); }); }); @@ -211,12 +245,17 @@ describe('Vue test utils helpers', () => { ${'findAllByAltText'} | ${'queryAllByAltText'} `('$findMethod', ({ findMethod, expectedQuery }) => { const text = 'foo bar'; - const options = { selector: 'div' }; + const options = { selector: 'li' }; const mockElements = [ document.createElement('li'), document.createElement('li'), document.createElement('li'), ]; + const mockVms = [ + new Vue({ render: (h) => h('li') }).$mount(), + new Vue({ render: (h) => h('li') }).$mount(), + new Vue({ render: (h) => h('li') }).$mount(), + ]; let wrapper; beforeEach(() => { @@ -245,9 +284,13 @@ describe('Vue test utils helpers', () => { ); }); - describe('when elements are found', () => { + describe.each` + case | mockResult | isVueInstance + ${'HTMLElements'} | ${mockElements} | ${false} + ${'Vue instance elements'} | ${mockVms} | ${true} + `('when $case are found', ({ mockResult, isVueInstance }) => { beforeEach(() => { - jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements); + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockResult); }); it('returns a VTU wrapper array', () => { @@ -257,7 +300,9 @@ describe('Vue test utils helpers', () => { expect( result.wrappers.every( (resultWrapper) => - resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options, + resultWrapper instanceof VTUWrapper && + resultWrapper.vm instanceof Vue === isVueInstance && + resultWrapper.options === wrapper.options, ), ).toBe(true); expect(result.length).toBe(3); diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js index ab2637d6024..bdd5a0a9034 100644 --- a/spec/frontend/__helpers__/vuex_action_helper.js +++ b/spec/frontend/__helpers__/vuex_action_helper.js @@ -1,5 +1,7 @@ -/** - * Helper for testing action with expected mutations inspired in +// eslint-disable-next-line no-restricted-syntax +import { setImmediate } from 'timers'; + +/** Helper for testing action with expected mutations inspired in * https://vuex.vuejs.org/en/testing.html * * @param {(Function|Object)} action to be tested, or object of named parameters diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js index 5bb2b3b26e2..182aea9c1c5 100644 --- a/spec/frontend/__helpers__/vuex_action_helper_spec.js +++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js @@ -76,7 +76,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])( const promise = testAction(() => {}, null, {}, assertion.mutations, assertion.actions); - originalExpect(promise instanceof Promise).toBeTruthy(); + originalExpect(promise instanceof Promise).toBe(true); return promise; }); diff --git a/spec/frontend/__helpers__/wait_for_promises.js b/spec/frontend/__helpers__/wait_for_promises.js index 753c3c5d92b..5a15b8b74b5 100644 --- a/spec/frontend/__helpers__/wait_for_promises.js +++ b/spec/frontend/__helpers__/wait_for_promises.js @@ -1,4 +1,2 @@ -export default () => - new Promise((resolve) => { - requestAnimationFrame(resolve); - }); +// eslint-disable-next-line no-restricted-syntax +export default () => new Promise(jest.requireActual('timers').setImmediate); diff --git a/spec/frontend/__helpers__/web_worker_transformer.js b/spec/frontend/__helpers__/web_worker_transformer.js index 5b2f7d77947..767ab3f5675 100644 --- a/spec/frontend/__helpers__/web_worker_transformer.js +++ b/spec/frontend/__helpers__/web_worker_transformer.js @@ -6,7 +6,7 @@ const babelJestTransformer = require('babel-jest'); // [1]: https://webpack.js.org/loaders/worker-loader/ module.exports = { process: (contentArg, filename, ...args) => { - const { code: content } = babelJestTransformer.process(contentArg, filename, ...args); + const { code: content } = babelJestTransformer.default.process(contentArg, filename, ...args); return `const { FakeWebWorker } = require("helpers/web_worker_fake"); module.exports = class JestTransformedWorker extends FakeWebWorker { diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js index 18b7df32f9b..384f9993150 100644 --- a/spec/frontend/__mocks__/monaco-editor/index.js +++ b/spec/frontend/__mocks__/monaco-editor/index.js @@ -15,4 +15,3 @@ jest.mock('monaco-editor/esm/vs/language/typescript/tsMode'); jest.mock('monaco-yaml/lib/esm/yamlMode'); export * from 'monaco-editor/esm/vs/editor/editor.api'; -export default global.monaco; diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap index 36003154b58..2bd2b17a12d 100644 --- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap +++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap @@ -11,22 +11,17 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi arialabel="" autocomplete="" container="" + data-qa-selector="expiry_date_field" + defaultdate="Wed Aug 05 2020 00:00:00 GMT+0000 (Greenwich Mean Time)" displayfield="true" firstday="0" + inputid="personal_access_token_expires_at" inputlabel="Enter date" + inputname="personal_access_token[expires_at]" mindate="Mon Jul 06 2020 00:00:00 GMT+0000 (Greenwich Mean Time)" placeholder="YYYY-MM-DD" + showclearbutton="true" theme="" - > - <gl-form-input-stub - autocomplete="off" - class="datepicker gl-datepicker-input" - data-qa-selector="expiry_date_field" - id="personal_access_token_expires_at" - inputmode="none" - name="personal_access_token[expires_at]" - placeholder="YYYY-MM-DD" - /> - </gl-datepicker-stub> + /> </gl-form-group-stub> `; diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js index cb899d10ba7..646dc0d703f 100644 --- a/spec/frontend/access_tokens/components/expires_at_field_spec.js +++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { GlDatepicker } from '@gitlab/ui'; import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; +import { getDateInFuture } from '~/lib/utils/datetime_utility'; describe('~/access_tokens/components/expires_at_field', () => { let wrapper; @@ -49,4 +50,12 @@ describe('~/access_tokens/components/expires_at_field', () => { expect(findDatepicker().props('maxDate')).toStrictEqual(maxDate); }); + + it('should set the default expiration date to be 30 days', () => { + const today = new Date(); + const future = getDateInFuture(today, 30); + createComponent(); + + expect(findDatepicker().props('defaultDate')).toStrictEqual(future); + }); }); diff --git a/spec/frontend/access_tokens/components/projects_field_spec.js b/spec/frontend/access_tokens/components/projects_field_spec.js deleted file mode 100644 index 1c4fe7bb168..00000000000 --- a/spec/frontend/access_tokens/components/projects_field_spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import { nextTick } from 'vue'; -import { within, fireEvent } from '@testing-library/dom'; -import { mount } from '@vue/test-utils'; -import ProjectsField from '~/access_tokens/components/projects_field.vue'; -import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; - -describe('ProjectsField', () => { - let wrapper; - - const createComponent = ({ inputAttrsValue = '' } = {}) => { - wrapper = mount(ProjectsField, { - propsData: { - inputAttrs: { - id: 'projects', - name: 'projects', - value: inputAttrsValue, - }, - }, - }); - }; - - const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text); - const queryByText = (text) => within(wrapper.element).queryByText(text); - const findAllProjectsRadio = () => queryByLabelText('All projects'); - const findSelectedProjectsRadio = () => queryByLabelText('Selected projects'); - const findProjectsTokenSelector = () => wrapper.findComponent(ProjectsTokenSelector); - const findHiddenInput = () => wrapper.find('input[type="hidden"]'); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('renders label and sub-label', () => { - createComponent(); - - expect(queryByText('Projects')).not.toBe(null); - expect(queryByText('Set access permissions for this token.')).not.toBe(null); - }); - - describe('when `inputAttrs.value` is empty', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders "All projects" radio as checked', () => { - expect(findAllProjectsRadio().checked).toBe(true); - }); - - it('renders "Selected projects" radio as unchecked', () => { - expect(findSelectedProjectsRadio().checked).toBe(false); - }); - - it('sets `projects-token-selector` `initialProjectIds` prop to an empty array', () => { - expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual([]); - }); - }); - - describe('when `inputAttrs.value` is a comma separated list of project IDs', () => { - beforeEach(() => { - createComponent({ inputAttrsValue: '1,2' }); - }); - - it('renders "All projects" radio as unchecked', () => { - expect(findAllProjectsRadio().checked).toBe(false); - }); - - it('renders "Selected projects" radio as checked', () => { - expect(findSelectedProjectsRadio().checked).toBe(true); - }); - - it('sets `projects-token-selector` `initialProjectIds` prop to an array of project IDs', () => { - expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual(['1', '2']); - }); - }); - - it('renders `projects-token-selector` component', () => { - createComponent(); - - expect(findProjectsTokenSelector().exists()).toBe(true); - }); - - it('renders hidden input with correct `name` and `id` attributes', () => { - createComponent(); - - expect(findHiddenInput().attributes()).toEqual( - expect.objectContaining({ - id: 'projects', - name: 'projects', - }), - ); - }); - - describe('when `projects-token-selector` is focused', () => { - beforeEach(() => { - createComponent(); - - findProjectsTokenSelector().vm.$emit('focus'); - }); - - it('auto selects the "Selected projects" radio', () => { - expect(findSelectedProjectsRadio().checked).toBe(true); - }); - - describe('when `projects-token-selector` is changed', () => { - beforeEach(() => { - findProjectsTokenSelector().vm.$emit('input', [ - { - id: 1, - }, - { - id: 2, - }, - ]); - }); - - it('updates the hidden input value to a comma separated list of project IDs', () => { - expect(findHiddenInput().attributes('value')).toBe('1,2'); - }); - - describe('when radio is changed back to "All projects"', () => { - it('removes the hidden input value', async () => { - fireEvent.change(findAllProjectsRadio()); - await nextTick(); - - expect(findHiddenInput().attributes('value')).toBe(''); - }); - }); - }); - }); -}); diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js deleted file mode 100644 index 40aaf16d41f..00000000000 --- a/spec/frontend/access_tokens/components/projects_token_selector_spec.js +++ /dev/null @@ -1,266 +0,0 @@ -import { - GlAvatar, - GlAvatarLabeled, - GlIntersectionObserver, - GlToken, - GlTokenSelector, - GlLoadingIcon, -} from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import produce from 'immer'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; - -import getProjectsQueryResponse from 'test_fixtures/graphql/projects/access_tokens/get_projects.query.graphql.json'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; -import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; - -describe('ProjectsTokenSelector', () => { - const getProjectsQueryResponsePage2 = produce( - getProjectsQueryResponse, - (getProjectsQueryResponseDraft) => { - /* eslint-disable no-param-reassign */ - getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false; - getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null; - getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1); - getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100'; - /* eslint-enable no-param-reassign */ - }, - ); - - const runDebounce = () => jest.runAllTimers(); - - const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects; - const project1 = projects[0]; - const project2 = projects[1]; - - let wrapper; - - let resolveGetProjectsQuery; - let resolveGetInitialProjectsQuery; - const getProjectsQueryRequestHandler = jest.fn( - ({ ids }) => - new Promise((resolve) => { - if (ids) { - resolveGetInitialProjectsQuery = resolve; - } else { - resolveGetProjectsQuery = resolve; - } - }), - ); - - const createComponent = ({ - propsData = {}, - apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]), - resolveQueries = true, - } = {}) => { - Vue.use(VueApollo); - - wrapper = extendedWrapper( - mount(ProjectsTokenSelector, { - apolloProvider, - propsData: { - selectedProjects: [], - initialProjectIds: [], - ...propsData, - }, - stubs: ['gl-intersection-observer'], - }), - ); - - runDebounce(); - - if (resolveQueries) { - resolveGetProjectsQuery(getProjectsQueryResponse); - - return waitForPromises(); - } - - return Promise.resolve(); - }; - - const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); - const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]'); - const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); - - it('renders dropdown items with project avatars', async () => { - await createComponent(); - - wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => { - const project = projects[index]; - - expect(avatarLabeledWrapper.attributes()).toEqual( - expect.objectContaining({ - 'entity-id': `${getIdFromGraphQLId(project.id)}`, - 'entity-name': project.name, - ...(project.avatarUrl && { src: project.avatarUrl }), - }), - ); - - expect(avatarLabeledWrapper.props()).toEqual( - expect.objectContaining({ - label: project.name, - subLabel: project.nameWithNamespace, - }), - ); - }); - }); - - it('renders tokens with project avatars', () => { - createComponent({ - propsData: { - selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }], - }, - }); - - const token = wrapper.findComponent(GlToken); - const avatar = token.findComponent(GlAvatar); - - expect(token.text()).toContain(project2.nameWithNamespace); - expect(avatar.attributes('src')).toBe(project2.avatarUrl); - expect(avatar.props()).toEqual( - expect.objectContaining({ - entityId: getIdFromGraphQLId(project2.id), - entityName: project2.name, - }), - ); - }); - - describe('when `enter` key is pressed', () => { - it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => { - createComponent(); - - const event = { - preventDefault: jest.fn(), - }; - - findTokenSelectorInput().trigger('keydown.enter', event); - - expect(event.preventDefault).toHaveBeenCalled(); - }); - }); - - describe('when text input is typed in', () => { - const searchTerm = 'foo bar'; - - beforeEach(async () => { - await createComponent(); - - await findTokenSelectorInput().setValue(searchTerm); - runDebounce(); - }); - - it('makes GraphQL request with `search` variable set', async () => { - expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ - search: searchTerm, - after: null, - first: 20, - ids: null, - }); - }); - - it('sets loading state while waiting for GraphQL request to resolve', async () => { - expect(findTokenSelector().props('loading')).toBe(true); - - resolveGetProjectsQuery(getProjectsQueryResponse); - await waitForPromises(); - - expect(findTokenSelector().props('loading')).toBe(false); - }); - }); - - describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => { - beforeEach(async () => { - await createComponent(); - - findIntersectionObserver().vm.$emit('appear'); - }); - - it('makes GraphQL request with `after` variable set', async () => { - expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ - after: pageInfo.endCursor, - first: 20, - search: '', - ids: null, - }); - }); - - it('displays loading icon while waiting for GraphQL request to resolve', async () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - - resolveGetProjectsQuery(getProjectsQueryResponsePage2); - await waitForPromises(); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - }); - }); - - describe('when there is not a next page of projects', () => { - it('does not render `GlIntersectionObserver`', async () => { - createComponent({ resolveQueries: false }); - - resolveGetProjectsQuery(getProjectsQueryResponsePage2); - await waitForPromises(); - - expect(findIntersectionObserver().exists()).toBe(false); - }); - }); - - describe('when `GlTokenSelector` emits `input` event', () => { - it('emits `input` event used by `v-model`', () => { - findTokenSelector().vm.$emit('input', project1); - - expect(wrapper.emitted('input')[0]).toEqual([project1]); - }); - }); - - describe('when `GlTokenSelector` emits `focus` event', () => { - it('emits `focus` event', () => { - const event = { fakeEvent: 'foo' }; - findTokenSelector().vm.$emit('focus', event); - - expect(wrapper.emitted('focus')[0]).toEqual([event]); - }); - }); - - describe('when `initialProjectIds` is an empty array', () => { - it('does not request initial projects', async () => { - await createComponent(); - - expect(getProjectsQueryRequestHandler).toHaveBeenCalledTimes(1); - expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith( - expect.objectContaining({ - ids: null, - }), - ); - }); - }); - - describe('when `initialProjectIds` is an array of project IDs', () => { - it('requests those projects and emits `input` event with result', async () => { - await createComponent({ - propsData: { - initialProjectIds: [getIdFromGraphQLId(project1.id), getIdFromGraphQLId(project2.id)], - }, - }); - - resolveGetInitialProjectsQuery(getProjectsQueryResponse); - await waitForPromises(); - - expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith({ - after: '', - first: null, - search: '', - ids: [project1.id, project2.id], - }); - expect(wrapper.emitted('input')[0][0]).toEqual([ - { ...project1, id: getIdFromGraphQLId(project1.id) }, - { ...project2, id: getIdFromGraphQLId(project2.id) }, - ]); - }); - }); -}); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index b6119f1d167..0c611a4a512 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -8,13 +8,11 @@ import { initAccessTokenTableApp, initExpiresAtField, initNewAccessTokenApp, - initProjectsField, initTokensApp, } from '~/access_tokens'; import * as AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue'; -import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; +import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; import * as NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue'; -import * as ProjectsField from '~/access_tokens/components/projects_field.vue'; import * as TokensApp from '~/access_tokens/components/tokens_app.vue'; import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants'; import { __, sprintf } from '~/locale'; @@ -115,49 +113,28 @@ describe('access tokens', () => { }); }); - describe.each` - initFunction | mountSelector | fieldName | expectedComponent - ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField} - ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField} - `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => { + describe('initExpiresAtField', () => { describe('when mount element exists', () => { - const FakeComponent = Vue.component('FakeComponent', { - props: ['inputAttrs'], - render: () => null, - }); - - const nameAttribute = `access_tokens[${fieldName}]`; - const idAttribute = `access_tokens_${fieldName}`; + const nameAttribute = 'access_tokens[expires_at]'; + const idAttribute = 'access_tokens_expires_at'; beforeEach(() => { - window.gon = { features: { personalAccessTokensScopedToProjects: true } }; - setHTMLFixture( - `<div class="${mountSelector}"> + `<div class="js-access-tokens-expires-at"> <input - name="${nameAttribute}" - data-js-name="${fieldName}" - id="${idAttribute}" + name="access_tokens[expires_at]" + data-js-name="expiresAt" + id="access_tokens_expires_at" placeholder="Foo bar" value="1,2" /> </div>`, ); - - // Mock component so we don't have to deal with mocking Apollo - // eslint-disable-next-line no-param-reassign - expectedComponent.default = FakeComponent; - }); - - afterEach(() => { - delete window.gon; }); it('mounts component and sets `inputAttrs` prop', async () => { - const vueInstance = await initFunction(); - - wrapper = createWrapper(vueInstance); - const component = wrapper.findComponent(FakeComponent); + wrapper = createWrapper(initExpiresAtField()); + const component = wrapper.findComponent(ExpiresAtField); expect(component.exists()).toBe(true); expect(component.props('inputAttrs')).toEqual({ @@ -171,7 +148,7 @@ describe('access tokens', () => { describe('when mount element does not exist', () => { it('returns `null`', () => { - expect(initFunction()).toBe(null); + expect(initExpiresAtField()).toBe(null); }); }); }); diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js index 9b93fd26fa0..bffadbde087 100644 --- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js +++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js @@ -87,7 +87,7 @@ describe('AddContextCommitsModal', () => { it('enabled ok button when atleast one row is selected', async () => { wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; await nextTick(); - expect(findModal().attributes('ok-disabled')).toBeFalsy(); + expect(findModal().attributes('ok-disabled')).toBe(undefined); }); }); @@ -102,7 +102,7 @@ describe('AddContextCommitsModal', () => { it('an enabled ok button when atleast one row is selected', async () => { wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; await nextTick(); - expect(findModal().attributes('ok-disabled')).toBeFalsy(); + expect(findModal().attributes('ok-disabled')).toBe(undefined); }); it('a disabled ok button in first tab, when row is selected in second tab', () => { diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js index d6c5c5f963a..534af2a3033 100644 --- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js +++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js @@ -129,7 +129,7 @@ describe('DevopsScore', () => { }); it('displays the correct badge', () => { - const badge = findUsageCol().find(GlBadge); + const badge = findUsageCol().findComponent(GlBadge); expect(badge.exists()).toBe(true); expect(badge.props('variant')).toBe('muted'); diff --git a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js index ae9b6f57ee0..eecc21e206b 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js @@ -24,7 +24,7 @@ describe('Signup Form', () => { const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`); const findHiddenInput = () => findByTestId('input'); - const findCheckbox = () => wrapper.find(GlFormCheckbox); + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findCheckboxLabel = () => findByTestId('label'); const findHelpText = () => findByTestId('helpText'); diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js index 31a0c2b07e4..411126d0c89 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js @@ -28,7 +28,7 @@ describe('Signup Form', () => { const findForm = () => wrapper.findByTestId('form'); const findInputCsrf = () => findForm().find('[name="authenticity_token"]'); - const findFormSubmitButton = () => findForm().find(GlButton); + const findFormSubmitButton = () => findForm().findComponent(GlButton); const findDenyListRawRadio = () => queryByLabelText('Enter denylist manually'); const findDenyListFileRadio = () => queryByLabelText('Upload denylist file'); @@ -36,7 +36,7 @@ describe('Signup Form', () => { const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group'); const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group'); const findUserCapInput = () => wrapper.findByTestId('user-cap-input'); - const findModal = () => wrapper.find(GlModal); + const findModal = () => wrapper.findComponent(GlModal); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js index bac542e72fb..190f0eb94a0 100644 --- a/spec/frontend/admin/statistics_panel/components/app_spec.js +++ b/spec/frontend/admin/statistics_panel/components/app_spec.js @@ -41,7 +41,7 @@ describe('Admin statistics app', () => { store.dispatch('requestStatistics'); createComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js index b758c15a91a..4967753b91c 100644 --- a/spec/frontend/admin/users/components/actions/actions_spec.js +++ b/spec/frontend/admin/users/components/actions/actions_spec.js @@ -12,7 +12,7 @@ import { paths } from '../../mock_data'; describe('Action components', () => { let wrapper; - const findDropdownItem = () => wrapper.find(GlDropdownItem); + const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); const initComponent = ({ component, props } = {}) => { wrapper = shallowMount(component, { diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js index 65b13e3a40d..913732aae42 100644 --- a/spec/frontend/admin/users/components/app_spec.js +++ b/spec/frontend/admin/users/components/app_spec.js @@ -28,7 +28,7 @@ describe('AdminUsersApp component', () => { }); it('renders the admin users table with props', () => { - expect(wrapper.find(AdminUsersTable).props()).toEqual({ + expect(wrapper.findComponent(AdminUsersTable).props()).toEqual({ users, paths, }); diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js index 09a345ac826..70ed9eeb3e1 100644 --- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js +++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js @@ -17,7 +17,7 @@ describe('Delete user modal', () => { const findButton = (variant, category) => wrapper - .findAll(GlButton) + .findAllComponents(GlButton) .filter((w) => w.attributes('variant') === variant && w.attributes('category') === category) .at(0); const findForm = () => wrapper.find('form'); @@ -87,8 +87,8 @@ describe('Delete user modal', () => { }); it('has disabled buttons', () => { - expect(findPrimaryButton().attributes('disabled')).toBeTruthy(); - expect(findSecondaryButton().attributes('disabled')).toBeTruthy(); + expect(findPrimaryButton().attributes('disabled')).toBe('true'); + expect(findSecondaryButton().attributes('disabled')).toBe('true'); }); }); @@ -105,8 +105,8 @@ describe('Delete user modal', () => { }); it('has disabled buttons', () => { - expect(findPrimaryButton().attributes('disabled')).toBeTruthy(); - expect(findSecondaryButton().attributes('disabled')).toBeTruthy(); + expect(findPrimaryButton().attributes('disabled')).toBe('true'); + expect(findSecondaryButton().attributes('disabled')).toBe('true'); }); }); @@ -123,8 +123,8 @@ describe('Delete user modal', () => { }); it('has enabled buttons', () => { - expect(findPrimaryButton().attributes('disabled')).toBeFalsy(); - expect(findSecondaryButton().attributes('disabled')).toBeFalsy(); + expect(findPrimaryButton().attributes('disabled')).toBeUndefined(); + expect(findSecondaryButton().attributes('disabled')).toBeUndefined(); }); describe('when primary action is clicked', () => { diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js index e04c43ae3f2..ffc05e744c8 100644 --- a/spec/frontend/admin/users/components/user_actions_spec.js +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -83,7 +83,7 @@ describe('AdminUserActions component', () => { }); it.each(CONFIRMATION_ACTIONS)('renders an action component item for "%s"', (action) => { - const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]); + const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]); expect(component.props('username')).toBe(user.name); expect(component.props('path')).toBe(userPaths[action]); @@ -119,7 +119,7 @@ describe('AdminUserActions component', () => { }); it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => { - const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]); + const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]); expect(component.props('username')).toBe(user.name); expect(component.props('paths')).toEqual(userPaths); diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js index 8bbfb89bec1..94fac875fbe 100644 --- a/spec/frontend/admin/users/components/user_avatar_spec.js +++ b/spec/frontend/admin/users/components/user_avatar_spec.js @@ -12,10 +12,10 @@ describe('AdminUserAvatar component', () => { const user = users[0]; const adminUserPath = paths.adminUser; - const findNote = () => wrapper.find(GlIcon); - const findAvatar = () => wrapper.find(GlAvatarLabeled); + const findNote = () => wrapper.findComponent(GlIcon); + const findAvatar = () => wrapper.findComponent(GlAvatarLabeled); const findUserLink = () => wrapper.find('.js-user-link'); - const findAllBadges = () => wrapper.findAll(GlBadge); + const findAllBadges = () => wrapper.findAllComponents(GlBadge); const findTooltip = () => getBinding(findNote().element, 'gl-tooltip'); const initComponent = (props = {}) => { diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js index ad1c45495b5..fe07f0fce00 100644 --- a/spec/frontend/admin/users/components/users_table_spec.js +++ b/spec/frontend/admin/users/components/users_table_spec.js @@ -30,10 +30,10 @@ describe('AdminUsersTable component', () => { const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]); const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`); - const findUserGroupCountLoader = (id) => findUserGroupCount(id).find(GlSkeletonLoader); + const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader); const getCellByLabel = (trIdx, label) => { return wrapper - .find(GlTable) + .findComponent(GlTable) .find('tbody') .findAll('tr') .at(trIdx) @@ -72,7 +72,7 @@ describe('AdminUsersTable component', () => { }); it('renders the user actions', () => { - expect(wrapper.find(AdminUserActions).exists()).toBe(true); + expect(wrapper.findComponent(AdminUserActions).exists()).toBe(true); }); it.each` @@ -81,7 +81,7 @@ describe('AdminUsersTable component', () => { ${AdminUserDate} | ${'Created on'} ${AdminUserDate} | ${'Last activity'} `('renders the component for column $label', ({ component, label }) => { - expect(getCellByLabel(0, label).find(component).exists()).toBe(true); + expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true); }); }); diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js index 961fa96acdd..b51858d5129 100644 --- a/spec/frontend/admin/users/index_spec.js +++ b/spec/frontend/admin/users/index_spec.js @@ -8,7 +8,7 @@ describe('initAdminUsersApp', () => { let wrapper; let el; - const findApp = () => wrapper.find(AdminUsersApp); + const findApp = () => wrapper.findComponent(AdminUsersApp); beforeEach(() => { el = document.createElement('div'); @@ -36,7 +36,7 @@ describe('initAdminUserActions', () => { let wrapper; let el; - const findUserActions = () => wrapper.find(UserActions); + const findUserActions = () => wrapper.findComponent(UserActions); beforeEach(() => { el = document.createElement('div'); diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js new file mode 100644 index 00000000000..e14ead0b8eb --- /dev/null +++ b/spec/frontend/api/groups_api_spec.js @@ -0,0 +1,46 @@ +import MockAdapter from 'axios-mock-adapter'; +import httpStatus from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; +import { updateGroup } from '~/api/groups_api'; + +const mockApiVersion = 'v4'; +const mockUrlRoot = '/gitlab'; + +describe('GroupsApi', () => { + let originalGon; + let mock; + + const dummyGon = { + api_version: mockApiVersion, + relative_url_root: mockUrlRoot, + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + originalGon = window.gon; + window.gon = { ...dummyGon }; + }); + + afterEach(() => { + mock.restore(); + window.gon = originalGon; + }); + + describe('updateGroup', () => { + const mockGroupId = '99'; + const mockData = { attr: 'value' }; + const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`; + + beforeEach(() => { + mock.onPut(expectedUrl).reply(({ data }) => { + return [httpStatus.OK, { id: mockGroupId, ...JSON.parse(data) }]; + }); + }); + + it('updates group', async () => { + const res = await updateGroup(mockGroupId, mockData); + + expect(res.data).toMatchObject({ id: mockGroupId, ...mockData }); + }); + }); +}); diff --git a/spec/frontend/attention_requests/components/navigation_popover_spec.js b/spec/frontend/attention_requests/components/navigation_popover_spec.js deleted file mode 100644 index e4d53d5dbdb..00000000000 --- a/spec/frontend/attention_requests/components/navigation_popover_spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlPopover, GlButton, GlSprintf, GlIcon } from '@gitlab/ui'; -import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import NavigationPopover from '~/attention_requests/components/navigation_popover.vue'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; - -let wrapper; -let dismiss; - -function createComponent(provideData = {}, shouldShowCallout = true) { - wrapper = shallowMount(NavigationPopover, { - provide: { - message: ['Test'], - observerElSelector: '.js-test', - observerElToggledClass: 'show', - featureName: 'attention_requests', - popoverTarget: '.js-test-popover', - ...provideData, - }, - stubs: { - UserCalloutDismisser: makeMockUserCalloutDismisser({ - dismiss, - shouldShowCallout, - }), - GlSprintf, - }, - }); -} - -describe('Attention requests navigation popover', () => { - beforeEach(() => { - setHTMLFixture('<div><div class="js-test-popover"></div><div class="js-test"></div></div>'); - dismiss = jest.fn(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - resetHTMLFixture(); - }); - - it('hides popover if callout is disabled', () => { - createComponent({}, false); - - expect(wrapper.findComponent(GlPopover).exists()).toBe(false); - }); - - it('shows popover if callout is enabled', () => { - createComponent(); - - expect(wrapper.findComponent(GlPopover).exists()).toBe(true); - }); - - it.each` - isDesktop | device | expectedPlacement - ${true} | ${'desktop'} | ${'left'} - ${false} | ${'mobile'} | ${'bottom'} - `( - 'sets popover position to $expectedPlacement on $device', - ({ isDesktop, expectedPlacement }) => { - jest.spyOn(bp, 'isDesktop').mockReturnValue(isDesktop); - - createComponent(); - - expect(wrapper.findComponent(GlPopover).props('placement')).toBe(expectedPlacement); - }, - ); - - it('calls dismiss when clicking action button', () => { - createComponent(); - - wrapper - .findComponent(GlButton) - .vm.$emit('click', { preventDefault() {}, stopPropagation() {} }); - - expect(dismiss).toHaveBeenCalled(); - }); - - it('shows icon in text', () => { - createComponent({ showAttentionIcon: true, message: ['%{strongStart}Test%{strongEnd}'] }); - - const icon = wrapper.findComponent(GlIcon); - - expect(icon.exists()).toBe(true); - expect(icon.props('name')).toBe('attention'); - }); -}); diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js index f50db6ab210..f98e0a4c64a 100644 --- a/spec/frontend/batch_comments/components/review_bar_spec.js +++ b/spec/frontend/batch_comments/components/review_bar_spec.js @@ -6,6 +6,8 @@ import createStore from '../create_batch_comments_store'; describe('Batch comments review bar component', () => { let store; let wrapper; + let addEventListenerSpy; + let removeEventListenerSpy; const createComponent = (propsData = {}) => { store = createStore(); @@ -18,25 +20,58 @@ describe('Batch comments review bar component', () => { beforeEach(() => { document.body.className = ''; + + addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); }); afterEach(() => { + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); wrapper.destroy(); }); - it('it adds review-bar-visible class to body when review bar is mounted', async () => { - expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); + describe('when mounted', () => { + it('it adds review-bar-visible class to body', async () => { + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); + + createComponent(); + + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true); + }); - createComponent(); + it('it adds a blocking handler to the `beforeunload` window event', () => { + expect(addEventListenerSpy).not.toBeCalled(); - expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true); + createComponent(); + + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + expect(addEventListenerSpy).toBeCalledWith('beforeunload', expect.any(Function), { + capture: true, + }); + }); }); - it('it removes review-bar-visible class to body when review bar is destroyed', async () => { - createComponent(); + describe('before destroyed', () => { + it('it removes review-bar-visible class to body', async () => { + createComponent(); - wrapper.destroy(); + wrapper.destroy(); - expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); + }); + + it('it removes the blocking handler from the `beforeunload` window event', () => { + createComponent(); + + expect(removeEventListenerSpy).not.toBeCalled(); + + wrapper.destroy(); + + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + expect(removeEventListenerSpy).toBeCalledWith('beforeunload', expect.any(Function), { + capture: true, + }); + }); }); }); diff --git a/spec/frontend/behaviors/components/json_table_spec.js b/spec/frontend/behaviors/components/json_table_spec.js new file mode 100644 index 00000000000..42b4a051d4d --- /dev/null +++ b/spec/frontend/behaviors/components/json_table_spec.js @@ -0,0 +1,162 @@ +import { GlTable, GlFormInput } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { merge } from 'lodash'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; +import JSONTable from '~/behaviors/components/json_table.vue'; + +const TEST_FIELDS = [ + 'A', + { + key: 'B', + label: 'Second', + sortable: true, + other: 'foo', + }, + { + key: 'C', + label: 'Third', + }, + 'D', +]; +const TEST_ITEMS = [ + { A: 1, B: 'lorem', C: 2, D: null, E: 'dne' }, + { A: 2, B: 'ipsum', C: 2, D: null, E: 'dne' }, + { A: 3, B: 'dolar', C: 2, D: null, E: 'dne' }, +]; + +describe('behaviors/components/json_table', () => { + let wrapper; + + const buildWrapper = ({ + fields = [], + items = [], + filter = undefined, + caption = undefined, + } = {}) => { + wrapper = shallowMountExtended(JSONTable, { + propsData: { + fields, + items, + hasFilter: filter, + caption, + }, + stubs: { + GlTable: merge(stubComponent(GlTable), { + props: { + fields: { + type: Array, + required: true, + }, + items: { + type: Array, + required: true, + }, + }, + template: RENDER_ALL_SLOTS_TEMPLATE, + }), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findTable = () => wrapper.findComponent(GlTable); + const findTableCaption = () => wrapper.findByTestId('slot-table-caption'); + const findFilterInput = () => wrapper.findComponent(GlFormInput); + + describe('default', () => { + beforeEach(() => { + buildWrapper(); + }); + + it('renders gltable', () => { + expect(findTable().props()).toEqual({ + fields: [], + items: [], + }); + expect(findTable().attributes()).toMatchObject({ + filter: '', + 'show-empty': '', + }); + }); + + it('does not render filter input', () => { + expect(findFilterInput().exists()).toBe(false); + }); + + it('renders caption', () => { + expect(findTableCaption().text()).toBe('Generated with JSON data'); + }); + }); + + describe('with filter', () => { + beforeEach(() => { + buildWrapper({ + filter: true, + }); + }); + + it('renders filter input', () => { + expect(findFilterInput().attributes()).toMatchObject({ + value: '', + placeholder: 'Type to search', + }); + }); + + it('when input is changed, updates table filter', async () => { + findFilterInput().vm.$emit('input', 'New value!'); + + await nextTick(); + + expect(findTable().attributes('filter')).toBe('New value!'); + }); + }); + + describe('with fields', () => { + beforeEach(() => { + buildWrapper({ + fields: TEST_FIELDS, + items: TEST_ITEMS, + }); + }); + + it('passes cleaned fields and items to table', () => { + expect(findTable().props()).toEqual({ + fields: [ + 'A', + { + key: 'B', + label: 'Second', + sortable: true, + }, + { + key: 'C', + label: 'Third', + sortable: false, + }, + 'D', + ], + items: TEST_ITEMS, + }); + }); + }); + + describe('with full mount', () => { + beforeEach(() => { + wrapper = mountExtended(JSONTable, { + propsData: { + fields: [], + items: [], + }, + }); + }); + + // We want to make sure all the props are passed down nicely in integration + it('renders table without errors', () => { + expect(findTable().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js index 8842ad636ec..722327e94ba 100644 --- a/spec/frontend/behaviors/gl_emoji_spec.js +++ b/spec/frontend/behaviors/gl_emoji_spec.js @@ -121,7 +121,7 @@ describe('gl_emoji', () => { window.gon.emoji_sprites_css_path = testPath; expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null); - expect(window.gon.emoji_sprites_css_added).toBeFalsy(); + expect(window.gon.emoji_sprites_css_added).toBe(undefined); markupToDomElement( '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>', diff --git a/spec/frontend/behaviors/markdown/render_json_table_spec.js b/spec/frontend/behaviors/markdown/render_json_table_spec.js new file mode 100644 index 00000000000..488492479f3 --- /dev/null +++ b/spec/frontend/behaviors/markdown/render_json_table_spec.js @@ -0,0 +1,119 @@ +import { nextTick } from 'vue'; +import { renderJSONTable } from '~/behaviors/markdown/render_json_table'; + +describe('behaviors/markdown/render_json_table', () => { + let element; + + const TEST_DATA = { + fields: [ + { label: 'Field 1', key: 'a' }, + { label: 'F 2', key: 'b' }, + { label: 'F 3', key: 'c' }, + ], + items: [ + { + a: '1', + b: 'b', + c: 'c', + }, + { + a: '2', + b: 'd', + c: 'e', + }, + ], + }; + const TEST_LABELS = TEST_DATA.fields.map((x) => x.label); + + const tableAsData = (table) => ({ + head: Array.from(table.querySelectorAll('thead th')).map((td) => td.textContent), + body: Array.from(table.querySelectorAll('tbody > tr')).map((tr) => + Array.from(tr.querySelectorAll('td')).map((x) => x.textContent), + ), + }); + + const createTestSubject = async (json) => { + if (element) { + throw new Error('element has already been initialized'); + } + + const parent = document.createElement('div'); + const pre = document.createElement('pre'); + + pre.textContent = json; + parent.appendChild(pre); + + document.body.appendChild(parent); + renderJSONTable([parent]); + + element = parent; + + jest.runAllTimers(); + + await nextTick(); + }; + + const findPres = () => document.querySelectorAll('pre'); + const findTables = () => document.querySelectorAll('table'); + const findAlerts = () => document.querySelectorAll('.gl-alert'); + const findInputs = () => document.querySelectorAll('.gl-form-input'); + + afterEach(() => { + document.body.innerHTML = ''; + element = null; + }); + + describe('default', () => { + beforeEach(async () => { + await createTestSubject(JSON.stringify(TEST_DATA, null, 2)); + }); + + it('removes pre', () => { + expect(findPres()).toHaveLength(0); + }); + + it('replaces pre with table', () => { + const tables = findTables(); + + expect(tables).toHaveLength(1); + expect(tableAsData(tables[0])).toEqual({ + head: TEST_LABELS, + body: [ + ['1', 'b', 'c'], + ['2', 'd', 'e'], + ], + }); + }); + + it('does not show filter', () => { + expect(findInputs()).toHaveLength(0); + }); + }); + + describe('with invalid json', () => { + beforeEach(() => { + createTestSubject('funky but not json'); + }); + + it('preserves pre', () => { + expect(findPres()).toHaveLength(1); + }); + + it('shows alert', () => { + const alerts = findAlerts(); + + expect(alerts).toHaveLength(1); + expect(alerts[0].textContent).toMatchInterpolatedText('Unable to parse JSON'); + }); + }); + + describe('with filter set', () => { + beforeEach(() => { + createTestSubject(JSON.stringify({ ...TEST_DATA, filter: true })); + }); + + it('shows filter', () => { + expect(findInputs()).toHaveLength(1); + }); + }); +}); diff --git a/spec/frontend/blob/3d_viewer/mesh_object_spec.js b/spec/frontend/blob/3d_viewer/mesh_object_spec.js index 60be285039f..3014af073f5 100644 --- a/spec/frontend/blob/3d_viewer/mesh_object_spec.js +++ b/spec/frontend/blob/3d_viewer/mesh_object_spec.js @@ -5,7 +5,7 @@ describe('Mesh object', () => { it('defaults to non-wireframe material', () => { const object = new MeshObject(new BoxGeometry(10, 10, 10)); - expect(object.material.wireframe).toBeFalsy(); + expect(object.material.wireframe).toBe(false); }); it('changes to wirefame material', () => { @@ -13,7 +13,7 @@ describe('Mesh object', () => { object.changeMaterial('wireframe'); - expect(object.material.wireframe).toBeTruthy(); + expect(object.material.wireframe).toBe(true); }); it('scales object down', () => { diff --git a/spec/frontend/blob/blob_links_tracking_spec.js b/spec/frontend/blob/blob_links_tracking_spec.js new file mode 100644 index 00000000000..22e087bc180 --- /dev/null +++ b/spec/frontend/blob/blob_links_tracking_spec.js @@ -0,0 +1,60 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import addBlobLinksTracking from '~/blob/blob_links_tracking'; +import Tracking from '~/tracking'; + +describe('Blob links Tracking', () => { + const eventName = 'click_link'; + const label = 'file_line_action'; + + const eventsToTrack = [ + { selector: '.file-line-blame', property: 'blame' }, + { selector: '.file-line-num', property: 'link' }, + ]; + + const [blameLinkClickEvent, numLinkClickEvent] = eventsToTrack; + + beforeEach(() => { + setHTMLFixture(` + <div id="blob-content-holder"> + <div class="line-links diff-line-num"> + <a href="#L5" class="file-line-blame"></a> + <a id="L5" href="#L5" data-line-number="5" class="file-line-num">5</a> + </div> + <pre id="LC5">Line 5 content</pre> + </div> + `); + addBlobLinksTracking('#blob-content-holder', eventsToTrack); + jest.spyOn(Tracking, 'event'); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('tracks blame link click event', () => { + const blameButton = document.querySelector(blameLinkClickEvent.selector); + blameButton.click(); + + expect(Tracking.event).toHaveBeenCalledWith(undefined, eventName, { + label, + property: blameLinkClickEvent.property, + }); + }); + + it('tracks num link click event', () => { + const numLinkButton = document.querySelector(numLinkClickEvent.selector); + numLinkButton.click(); + + expect(Tracking.event).toHaveBeenCalledWith(undefined, eventName, { + label, + property: numLinkClickEvent.property, + }); + }); + + it("doesn't fire tracking if the user clicks on any element that is not a link", () => { + const codeLine = document.querySelector('#LC5'); + codeLine.click(); + + expect(Tracking.event).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js index 8450c6b9332..788ee0a86ab 100644 --- a/spec/frontend/blob/components/blob_content_spec.js +++ b/spec/frontend/blob/components/blob_content_spec.js @@ -36,20 +36,20 @@ describe('Blob Content component', () => { describe('rendering', () => { it('renders loader if `loading: true`', () => { createComponent({ loading: true }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find(BlobContentError).exists()).toBe(false); - expect(wrapper.find(RichViewer).exists()).toBe(false); - expect(wrapper.find(SimpleViewer).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(BlobContentError).exists()).toBe(false); + expect(wrapper.findComponent(RichViewer).exists()).toBe(false); + expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false); }); it('renders error if there is any in the viewer', () => { const renderError = 'Oops'; const viewer = { ...SimpleViewerMock, renderError }; createComponent({}, viewer); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(BlobContentError).exists()).toBe(true); - expect(wrapper.find(RichViewer).exists()).toBe(false); - expect(wrapper.find(SimpleViewer).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(BlobContentError).exists()).toBe(true); + expect(wrapper.findComponent(RichViewer).exists()).toBe(false); + expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false); }); it.each` @@ -60,7 +60,7 @@ describe('Blob Content component', () => { 'renders $type viewer when activeViewer is $type and no loading or error detected', ({ mock, viewer }) => { createComponent({}, mock); - expect(wrapper.find(viewer).exists()).toBe(true); + expect(wrapper.findComponent(viewer).exists()).toBe(true); }, ); @@ -70,13 +70,13 @@ describe('Blob Content component', () => { ${RichBlobContentMock.richData} | ${RichViewerMock} | ${RichViewer} `('renders correct content that is passed to the component', ({ content, mock, viewer }) => { createComponent({ content }, mock); - expect(wrapper.find(viewer).html()).toContain(content); + expect(wrapper.findComponent(viewer).html()).toContain(content); }); }); describe('functionality', () => { describe('render error', () => { - const findErrorEl = () => wrapper.find(BlobContentError); + const findErrorEl = () => wrapper.findComponent(BlobContentError); const renderError = BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id; const viewer = { ...SimpleViewerMock, renderError }; diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js index 9fc2356c018..5017b624292 100644 --- a/spec/frontend/blob/components/blob_edit_content_spec.js +++ b/spec/frontend/blob/components/blob_edit_content_spec.js @@ -69,7 +69,7 @@ describe('Blob Header Editing', () => { }); it('initialises Source Editor', () => { - const el = wrapper.find({ ref: 'editor' }).element; + const el = wrapper.findComponent({ ref: 'editor' }).element; expect(utils.initSourceEditor).toHaveBeenCalledWith({ el, blobPath: fileName, diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js index b1ce0e9a4c5..c84b5896348 100644 --- a/spec/frontend/blob/components/blob_edit_header_spec.js +++ b/spec/frontend/blob/components/blob_edit_header_spec.js @@ -16,7 +16,7 @@ describe('Blob Header Editing', () => { }); }; const findDeleteButton = () => - wrapper.findAll(GlButton).wrappers.find((x) => x.text() === 'Delete file'); + wrapper.findAllComponents(GlButton).wrappers.find((x) => x.text() === 'Delete file'); beforeEach(() => { createComponent(); @@ -32,7 +32,7 @@ describe('Blob Header Editing', () => { }); it('contains a form input field', () => { - expect(wrapper.find(GlFormInput).exists()).toBe(true); + expect(wrapper.findComponent(GlFormInput).exists()).toBe(true); }); it('does not show delete button', () => { @@ -42,7 +42,7 @@ describe('Blob Header Editing', () => { describe('functionality', () => { it('emits input event when the blob name is changed', async () => { - const inputComponent = wrapper.find(GlFormInput); + const inputComponent = wrapper.findComponent(GlFormInput); const newValue = 'bar.txt'; // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js index aa538facae2..0f015715dc2 100644 --- a/spec/frontend/blob/components/blob_header_default_actions_spec.js +++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js @@ -30,8 +30,8 @@ describe('Blob Header Default Actions', () => { beforeEach(() => { createComponent(); - btnGroup = wrapper.find(GlButtonGroup); - buttons = wrapper.findAll(GlButton); + btnGroup = wrapper.findComponent(GlButtonGroup); + buttons = wrapper.findAllComponents(GlButton); }); afterEach(() => { @@ -69,9 +69,9 @@ describe('Blob Header Default Actions', () => { createComponent({ activeViewer: RICH_BLOB_VIEWER, }); - buttons = wrapper.findAll(GlButton); + buttons = wrapper.findAllComponents(GlButton); - expect(buttons.at(0).attributes('disabled')).toBeTruthy(); + expect(buttons.at(0).attributes('disabled')).toBe('true'); }); it('does not render the copy button if a rendering error is set', () => { diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js index 8220b598ff6..8c32cba1ba4 100644 --- a/spec/frontend/blob/components/blob_header_filepath_spec.js +++ b/spec/frontend/blob/components/blob_header_filepath_spec.js @@ -25,7 +25,7 @@ describe('Blob Header Filepath', () => { wrapper.destroy(); }); - const findBadge = () => wrapper.find(GlBadge); + const findBadge = () => wrapper.findComponent(GlBadge); describe('rendering', () => { it('matches the snapshot', () => { @@ -46,7 +46,7 @@ describe('Blob Header Filepath', () => { it('renders copy-to-clipboard icon that copies path of the Blob', () => { createComponent(); - const btn = wrapper.find(ClipboardButton); + const btn = wrapper.findComponent(ClipboardButton); expect(btn.exists()).toBe(true); expect(btn.vm.text).toBe(MockBlob.path); }); diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js index ee42c2387ae..46740958090 100644 --- a/spec/frontend/blob/components/blob_header_spec.js +++ b/spec/frontend/blob/components/blob_header_spec.js @@ -31,7 +31,7 @@ describe('Blob Header Default Actions', () => { }); describe('rendering', () => { - const findDefaultActions = () => wrapper.find(DefaultActions); + const findDefaultActions = () => wrapper.findComponent(DefaultActions); const slots = { prepend: 'Foo Prepend', @@ -45,17 +45,17 @@ describe('Blob Header Default Actions', () => { it('renders all components', () => { createComponent(); - expect(wrapper.find(TableContents).exists()).toBe(true); - expect(wrapper.find(ViewerSwitcher).exists()).toBe(true); + expect(wrapper.findComponent(TableContents).exists()).toBe(true); + expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true); expect(findDefaultActions().exists()).toBe(true); - expect(wrapper.find(BlobFilepath).exists()).toBe(true); + expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true); }); it('does not render viewer switcher if the blob has only the simple viewer', () => { createComponent({ richViewer: null, }); - expect(wrapper.find(ViewerSwitcher).exists()).toBe(false); + expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false); }); it('does not render viewer switcher if a corresponding prop is passed', () => { @@ -66,7 +66,7 @@ describe('Blob Header Default Actions', () => { hideViewerSwitcher: true, }, ); - expect(wrapper.find(ViewerSwitcher).exists()).toBe(false); + expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false); }); it('does not render default actions is corresponding prop is passed', () => { @@ -77,7 +77,7 @@ describe('Blob Header Default Actions', () => { hideDefaultActions: true, }, ); - expect(wrapper.find(DefaultActions).exists()).toBe(false); + expect(wrapper.findComponent(DefaultActions).exists()).toBe(false); }); Object.keys(slots).forEach((slot) => { diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js index 91baaf3ea69..1eac0733646 100644 --- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js +++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js @@ -35,8 +35,8 @@ describe('Blob Header Viewer Switcher', () => { beforeEach(() => { createComponent(); - btnGroup = wrapper.find(GlButtonGroup); - buttons = wrapper.findAll(GlButton); + btnGroup = wrapper.findComponent(GlButtonGroup); + buttons = wrapper.findAllComponents(GlButton); }); it('renders gl-button-group component', () => { @@ -58,7 +58,7 @@ describe('Blob Header Viewer Switcher', () => { function factory(propsData = {}) { createComponent(propsData); - buttons = wrapper.findAll(GlButton); + buttons = wrapper.findAllComponents(GlButton); simpleBtn = buttons.at(0); richBtn = buttons.at(1); diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js index 93406db2675..ea4badc03fb 100644 --- a/spec/frontend/blob/notebook/notebook_viever_spec.js +++ b/spec/frontend/blob/notebook/notebook_viever_spec.js @@ -31,10 +31,10 @@ describe('iPython notebook renderer', () => { wrapper = shallowMount(component, { propsData: { endpoint, relativeRawPath } }); }; - const findLoading = () => wrapper.find(GlLoadingIcon); - const findNotebookLab = () => wrapper.find(NotebookLab); - const findLoadErrorMessage = () => wrapper.find({ ref: 'loadErrorMessage' }); - const findParseErrorMessage = () => wrapper.find({ ref: 'parsingErrorMessage' }); + const findLoading = () => wrapper.findComponent(GlLoadingIcon); + const findNotebookLab = () => wrapper.findComponent(NotebookLab); + const findLoadErrorMessage = () => wrapper.findComponent({ ref: 'loadErrorMessage' }); + const findParseErrorMessage = () => wrapper.findComponent({ ref: 'parsingErrorMessage' }); beforeEach(() => { mock = new MockAdapter(axios); diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js index e332ea49fa6..23227df6357 100644 --- a/spec/frontend/blob/pdf/pdf_viewer_spec.js +++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js @@ -18,9 +18,9 @@ describe('PDF renderer', () => { }); }; - const findLoading = () => wrapper.find(GlLoadingIcon); - const findPdfLab = () => wrapper.find(PdfLab); - const findLoadError = () => wrapper.find({ ref: 'loadError' }); + const findLoading = () => wrapper.findComponent(GlLoadingIcon); + const findPdfLab = () => wrapper.findComponent(PdfLab); + const findLoadError = () => wrapper.findComponent({ ref: 'loadError' }); beforeEach(() => { mountComponent(); diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js index 750dd8f0a72..81b38cfc278 100644 --- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js +++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js @@ -52,7 +52,7 @@ describe('PipelineTourSuccessModal', () => { }); it('renders the path from the commit cookie for back to the merge request button', () => { - const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' }); + const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' }); expect(goToMrBtn.attributes('href')).toBe(expectedMrPath); }); @@ -67,16 +67,16 @@ describe('PipelineTourSuccessModal', () => { }); it('renders the path from projectMergeRequestsPath for back to the merge request button', () => { - const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' }); + const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' }); expect(goToMrBtn.attributes('href')).toBe(expectedMrPath); }); }); it('has expected structure', () => { - const modal = wrapper.find(GlModal); - const sprintf = modal.find(GlSprintf); - const emoji = modal.find(GlEmoji); + const modal = wrapper.findComponent(GlModal); + const sprintf = modal.findComponent(GlSprintf); + const emoji = modal.findComponent(GlEmoji); expect(wrapper.text()).toContain("That's it, well done!"); expect(sprintf.exists()).toBe(true); @@ -84,7 +84,7 @@ describe('PipelineTourSuccessModal', () => { }); it('renders the link for codeQualityLink', () => { - expect(wrapper.find(GlLink).attributes('href')).toBe('/code-quality-link'); + expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/code-quality-link'); }); it('calls to remove cookie', () => { @@ -103,7 +103,7 @@ describe('PipelineTourSuccessModal', () => { it('send an event when go to pipelines is clicked', () => { trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); - const goToBtn = wrapper.find({ ref: 'goToPipelines' }); + const goToBtn = wrapper.findComponent({ ref: 'goToPipelines' }); triggerEvent(goToBtn.element); expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', { @@ -115,7 +115,7 @@ describe('PipelineTourSuccessModal', () => { it('sends an event when back to the merge request is clicked', () => { trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); - const goToBtn = wrapper.find({ ref: 'goToMergeRequest' }); + const goToBtn = wrapper.findComponent({ ref: 'goToMergeRequest' }); triggerEvent(goToBtn.element); expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', { diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js index 5e1922a24f4..e8d1f724c4b 100644 --- a/spec/frontend/blob/sketch/index_spec.js +++ b/spec/frontend/blob/sketch/index_spec.js @@ -69,7 +69,7 @@ describe('Sketch viewer', () => { const img = document.querySelector('#js-sketch-viewer img'); expect(img).not.toBeNull(); - expect(img.classList.contains('img-fluid')).toBeTruthy(); + expect(img.classList.contains('img-fluid')).toBe(true); }); it('renders link to image', () => { diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js index 7e13994f2b7..6b329dc078a 100644 --- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js +++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js @@ -98,7 +98,7 @@ describe('Suggest gitlab-ci.yml Popover', () => { const expectedAction = 'click_button'; const expectedProperty = 'owner'; const expectedValue = '10'; - const dismissButton = wrapper.find(GlButton); + const dismissButton = wrapper.findComponent(GlButton); trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); triggerEvent(dismissButton.element); diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index c6de3ee69f3..985902b4a3b 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -238,7 +238,7 @@ describe('Board card component', () => { }); it('renders assignee', () => { - expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(true); + expect(wrapper.find('.board-card-assignee .gl-avatar').exists()).toBe(true); }); it('sets title', () => { @@ -336,7 +336,7 @@ describe('Board card component', () => { }); it('renders all three assignees', () => { - expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(3); + expect(wrapper.findAll('.board-card-assignee .gl-avatar').length).toEqual(3); }); describe('more than three assignees', () => { @@ -362,7 +362,7 @@ describe('Board card component', () => { }); it('renders two assignees', () => { - expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2); + expect(wrapper.findAll('.board-card-assignee .gl-avatar').length).toEqual(2); }); it('renders 99+ avatar counter', async () => { diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index fd9d2b6823d..9b0c0b93ffb 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -56,7 +56,7 @@ describe('Board list component', () => { }); it('renders issues', () => { - expect(wrapper.findAll(BoardCard).length).toBe(1); + expect(wrapper.findAllComponents(BoardCard).length).toBe(1); }); it('sets data attribute with issue id', () => { diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js index 3b26ca57d6f..0b3c6cb24c4 100644 --- a/spec/frontend/boards/components/board_add_new_column_form_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -60,8 +60,8 @@ describe('Board card layout', () => { }); const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); - const findSearchInput = () => wrapper.find(GlSearchBoxByType); - const findSearchLabel = () => wrapper.find(GlFormGroup); + const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findSearchLabelFormGroup = () => wrapper.findComponent(GlFormGroup); const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); const submitButton = () => wrapper.findByTestId('addNewColumnButton'); const findDropdown = () => wrapper.findComponent(GlDropdown); @@ -121,10 +121,17 @@ describe('Board card layout', () => { mountComponent(props); - expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel); + expect(findSearchLabelFormGroup().attributes('label')).toEqual(props.searchLabel); expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder); }); + it('does not show the dropdown as invalid by default', () => { + mountComponent(); + + expect(findSearchLabelFormGroup().attributes('state')).toBe('true'); + expect(findDropdown().props('toggleClass')).not.toContain('gl-inset-border-1-red-400!'); + }); + it('emits filter event on input', () => { mountComponent(); @@ -137,13 +144,13 @@ describe('Board card layout', () => { }); describe('Add list button', () => { - it('is disabled if no item is selected', () => { + it('is enabled by default', () => { mountComponent(); - expect(submitButton().props('disabled')).toBe(true); + expect(submitButton().props('disabled')).toBe(false); }); - it('emits add-list event on click', () => { + it('emits add-list event on click when an ID is selected', () => { mountComponent({ selectedId: mockLabelList.label.id, }); @@ -152,5 +159,16 @@ describe('Board card layout', () => { expect(wrapper.emitted('add-list')).toEqual([[]]); }); + + it('does not emit the add-list event on click and shows the dropdown as invalid when no ID is selected', async () => { + mountComponent(); + + await submitButton().vm.$emit('click'); + + expect(findSearchLabelFormGroup().attributes('state')).toBeUndefined(); + expect(findDropdown().props('toggleClass')).toContain('gl-inset-border-1-red-400!'); + + expect(wrapper.emitted('add-list')).toBeUndefined(); + }); }); }); diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js index 7dd02bf1d35..354eb7bff16 100644 --- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js @@ -39,7 +39,7 @@ describe('BoardAddNewColumnTrigger', () => { }); it('renders an enabled button', () => { - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); expect(button.props('disabled')).toBe(false); }); @@ -47,7 +47,7 @@ describe('BoardAddNewColumnTrigger', () => { describe('when button is disabled', () => { it('shows the tooltip', async () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); await nextTick(); diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js index 7a5c49bd488..cf4ba07da16 100644 --- a/spec/frontend/boards/components/board_blocked_icon_spec.js +++ b/spec/frontend/boards/components/board_blocked_icon_spec.js @@ -23,9 +23,9 @@ describe('BoardBlockedIcon', () => { let wrapper; let mockApollo; - const findGlIcon = () => wrapper.find(GlIcon); - const findGlPopover = () => wrapper.find(GlPopover); - const findGlLink = () => wrapper.find(GlLink); + const findGlIcon = () => wrapper.findComponent(GlIcon); + const findGlPopover = () => wrapper.findComponent(GlPopover); + const findGlLink = () => wrapper.findComponent(GlLink); const findPopoverTitle = () => wrapper.findByTestId('popover-title'); const findIssuableTitle = () => wrapper.findByTestId('issuable-title'); const findHiddenBlockingCount = () => wrapper.findByTestId('hidden-blocking-count'); @@ -114,7 +114,7 @@ describe('BoardBlockedIcon', () => { it('should display a loading spinner while loading', () => { createWrapper({ loading: true }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('should not query for blocking issuables by default', async () => { diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 17a5383a31e..bb1e63a581e 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -88,7 +88,7 @@ describe('Board card', () => { createStore({ initialState: { isShowingLabels: true } }); mountComponent({ mountFn: mount, stubs: {} }); - wrapper.find(GlLabel).trigger('mouseup'); + wrapper.findComponent(GlLabel).trigger('mouseup'); expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(0); }); diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index 368c7d561f8..7e35c39cd48 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -108,7 +108,7 @@ describe('BoardContentSidebar', () => { createStore({ mockGetters: { isSidebarOpen: () => false } }); createComponent(); - expect(wrapper.findComponent(GlDrawer).exists()).toBe(false); + expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false); }); it('applies an open attribute', () => { diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index f535679b8a0..97d9e08f5d4 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -67,12 +67,12 @@ describe('BoardContent', () => { }); it('renders BoardContentSidebar', () => { - expect(wrapper.find(BoardContentSidebar).exists()).toBe(true); + expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true); }); it('does not display EpicsSwimlanes component', () => { - expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false); - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); }); @@ -82,7 +82,7 @@ describe('BoardContent', () => { }); it('does not render BoardContentSidebar', () => { - expect(wrapper.find(BoardContentSidebar).exists()).toBe(false); + expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(false); }); }); @@ -92,7 +92,7 @@ describe('BoardContent', () => { }); it('renders draggable component', () => { - expect(wrapper.find(Draggable).exists()).toBe(true); + expect(wrapper.findComponent(Draggable).exists()).toBe(true); }); }); @@ -102,7 +102,7 @@ describe('BoardContent', () => { }); it('does not render draggable component', () => { - expect(wrapper.find(Draggable).exists()).toBe(false); + expect(wrapper.findComponent(Draggable).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 2f9677680eb..50901f3fe84 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -83,7 +83,7 @@ describe('Board List Header Component', () => { const isCollapsed = () => wrapper.vm.list.collapsed; - const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' }); + const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' }); const findTitle = () => wrapper.find('.board-title'); const findCaret = () => wrapper.findByTestId('board-title-caret'); diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js index 86cebc8a719..f4e9901aad2 100644 --- a/spec/frontend/boards/components/board_new_item_spec.js +++ b/spec/frontend/boards/components/board_new_item_spec.js @@ -44,7 +44,7 @@ describe('BoardNewItem', () => { it('finds an enabled create button', async () => { expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true); - wrapper.find(GlFormInput).vm.$emit('input', 'hello'); + wrapper.findComponent(GlFormInput).vm.$emit('input', 'hello'); await nextTick(); expect(wrapper.findByTestId('create-button').props('disabled')).toBe(false); @@ -53,7 +53,7 @@ describe('BoardNewItem', () => { describe('when the user types in a string with only spaces', () => { it('disables the Create Issue button', async () => { - wrapper.find(GlFormInput).vm.$emit('input', ' '); + wrapper.findComponent(GlFormInput).vm.$emit('input', ' '); await nextTick(); @@ -93,7 +93,7 @@ describe('BoardNewItem', () => { titleInput().setValue('Foo'); await glForm().trigger('submit'); - expect(wrapper.emitted('form-submit')).toBeTruthy(); + expect(wrapper.emitted('form-submit')).toHaveLength(1); expect(wrapper.emitted('form-submit')[0]).toEqual([ { title: 'Foo', @@ -131,7 +131,7 @@ describe('BoardNewItem', () => { await glForm().trigger('reset'); expect(titleInput().element.value).toBe(''); - expect(wrapper.emitted('form-cancel')).toBeTruthy(); + expect(wrapper.emitted('form-cancel')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 7f40c426b30..4171a6236de 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -57,10 +57,10 @@ describe('BoardSettingsSidebar', () => { }), ); }; - const findLabel = () => wrapper.find(GlLabel); - const findDrawer = () => wrapper.find(GlDrawer); - const findModal = () => wrapper.find(GlModal); - const findRemoveButton = () => wrapper.find(GlButton); + const findLabel = () => wrapper.findComponent(GlLabel); + const findDrawer = () => wrapper.findComponent(GlDrawer); + const findModal = () => wrapper.findComponent(GlModal); + const findRemoveButton = () => wrapper.findComponent(GlButton); afterEach(() => { jest.restoreAllMocks(); @@ -71,7 +71,7 @@ describe('BoardSettingsSidebar', () => { it('finds a MountingPortal component', () => { createComponent(); - expect(wrapper.find(MountingPortal).props()).toMatchObject({ + expect(wrapper.findComponent(MountingPortal).props()).toMatchObject({ mountTo: '#js-right-sidebar-portal', append: true, name: 'board-settings-sidebar', @@ -93,7 +93,7 @@ describe('BoardSettingsSidebar', () => { await nextTick(); - expect(wrapper.find(GlDrawer).exists()).toBe(false); + expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false); }); it('closes the sidebar when emitting the correct event', async () => { @@ -103,7 +103,7 @@ describe('BoardSettingsSidebar', () => { await nextTick(); - expect(wrapper.find(GlDrawer).exists()).toBe(false); + expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false); }); }); @@ -150,7 +150,7 @@ describe('BoardSettingsSidebar', () => { it('does not render GlDrawer', () => { createComponent({ sidebarType: '' }); - expect(findDrawer().exists()).toBe(false); + expect(findDrawer().props('open')).toBe(false); }); }); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index d91e81fe4d0..f3be66db36f 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -53,7 +53,7 @@ describe('BoardsSelector', () => { }; const fillSearchBox = (filterTerm) => { - const searchBox = wrapper.find({ ref: 'searchBox' }); + const searchBox = wrapper.findComponent({ ref: 'searchBox' }); const searchBoxInput = searchBox.find('input'); searchBoxInput.setValue(filterTerm); searchBoxInput.trigger('input'); diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js index 075fe225ec2..2bbd3797abf 100644 --- a/spec/frontend/boards/components/new_board_button_spec.js +++ b/spec/frontend/boards/components/new_board_button_spec.js @@ -53,13 +53,13 @@ describe('NewBoardButton', () => { it('renders nothing when `canAdminBoard` is `false`', () => { wrapper = createComponent({ canAdminBoard: false }); - expect(wrapper.find(GlButton).exists()).toBe(false); + expect(wrapper.findComponent(GlButton).exists()).toBe(false); }); it('renders nothing when `multipleIssueBoardsAvailable` is `false`', () => { wrapper = createComponent({ multipleIssueBoardsAvailable: false }); - expect(wrapper.find(GlButton).exists()).toBe(false); + expect(wrapper.findComponent(GlButton).exists()).toBe(false); }); it('emits `showBoardModal` when button is clicked', () => { @@ -67,7 +67,7 @@ describe('NewBoardButton', () => { wrapper = createComponent(); - wrapper.find(GlButton).vm.$emit('click', { preventDefault: () => {} }); + wrapper.findComponent(GlButton).vm.$emit('click', { preventDefault: () => {} }); expect(eventHub.$emit).toHaveBeenCalledWith('showBoardModal', 'new'); }); diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js index 0c76c711b3a..5e2222ac3d7 100644 --- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js +++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js @@ -6,7 +6,7 @@ import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vu describe('boards sidebar remove issue', () => { let wrapper; - const findLoader = () => wrapper.find(GlLoadingIcon); + const findLoader = () => wrapper.findComponent(GlLoadingIcon); const findEditButton = () => wrapper.find('[data-testid="edit-button"]'); const findTitle = () => wrapper.find('[data-testid="title"]'); const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js index 7c8996be0b8..5c435643425 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js @@ -47,7 +47,7 @@ describe('BoardSidebarTimeTracker', () => { (timeTrackingLimitToHours) => { createComponent({ provide: { timeTrackingLimitToHours } }); - expect(wrapper.find(IssuableTimeTracker).props()).toEqual({ + expect(wrapper.findComponent(IssuableTimeTracker).props()).toEqual({ limitToHours: timeTrackingLimitToHours, showCollapsed: false, issuableId: '1', diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js index 5364d929c38..cc1e5de15c1 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js @@ -46,10 +46,10 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { }); }; - const findForm = () => wrapper.find(GlForm); - const findAlert = () => wrapper.find(GlAlert); - const findFormInput = () => wrapper.find(GlFormInput); - const findEditableItem = () => wrapper.find(BoardEditableItem); + const findForm = () => wrapper.findComponent(GlForm); + const findAlert = () => wrapper.findComponent(GlAlert); + const findFormInput = () => wrapper.findComponent(GlFormInput); + const findEditableItem = () => wrapper.findComponent(BoardEditableItem); const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]'); const findTitle = () => wrapper.find('[data-testid="item-title"]'); const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index c45cd545155..7ff34ffdf9e 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -10,7 +10,6 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import ProjectSelect from '~/boards/components/project_select.vue'; import defaultState from '~/boards/stores/state'; -import waitForPromises from 'helpers/wait_for_promises'; import { mockList, mockActiveGroupProjects } from './mock_data'; @@ -23,9 +22,9 @@ describe('ProjectSelect component', () => { const findLabel = () => wrapper.find("[data-testid='header-label']"); const findGlDropdown = () => wrapper.findComponent(GlDropdown); const findGlDropdownLoadingIcon = () => - findGlDropdown().find('button:first-child').find(GlLoadingIcon); - const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType); - const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem); + findGlDropdown().find('button:first-child').findComponent(GlLoadingIcon); + const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); + const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstGlDropdownItem = () => findGlDropdownItems().at(0); const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']"); const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']"); @@ -133,7 +132,7 @@ describe('ProjectSelect component', () => { const dropdownToggle = findGlDropdown().find('.dropdown-toggle'); await dropdownToggle.trigger('click'); - await waitForPromises(); + jest.runOnlyPendingTimers(); await nextTick(); const searchInput = findGlDropdown().findComponent(GlFormInput).element; diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 7d79993a0ee..1606ca09d8f 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -518,17 +518,6 @@ describe('Board Store Mutations', () => { expect(state.boardItemsByListId[payload.listId]).toEqual(listState); }); - - it("updates the list's items count", () => { - expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); - - mutations.ADD_BOARD_ITEM_TO_LIST(state, { - itemId: mockIssue2.id, - listId: mockList.id, - }); - - expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2); - }); }); describe('REMOVE_BOARD_ITEM_FROM_LIST', () => { @@ -536,8 +525,7 @@ describe('Board Store Mutations', () => { setBoardsListsState(); }); - it("removes an item from a list and updates the list's items count", () => { - expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); + it('removes an item from a list', () => { expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue.id); mutations.REMOVE_BOARD_ITEM_FROM_LIST(state, { @@ -546,7 +534,6 @@ describe('Board Store Mutations', () => { }); expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue.id); - expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(0); }); }); diff --git a/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js b/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js index 08d031a4fa7..2263d2bbeed 100644 --- a/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js +++ b/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import CaptchaModal from '~/captcha/captcha_modal.vue'; import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved'; @@ -15,7 +16,7 @@ describe('waitForCaptchaToBeSolved', () => { it('opens a modal, resolves with captcha response on success', async () => { CaptchaModal.mounted.mockImplementationOnce(function mounted() { - requestAnimationFrame(() => { + return nextTick().then(() => { this.$emit('receivedCaptchaResponse', response); this.$emit('hidden'); }); @@ -36,7 +37,7 @@ describe('waitForCaptchaToBeSolved', () => { it("opens a modal, rejects with error in case the captcha isn't solved", async () => { CaptchaModal.mounted.mockImplementationOnce(function mounted() { - requestAnimationFrame(() => { + return nextTick().then(() => { this.$emit('receivedCaptchaResponse', null); this.$emit('hidden'); }); diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js new file mode 100644 index 00000000000..920ceaefb70 --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js @@ -0,0 +1,178 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { resolvers } from '~/ci_variable_list/graphql/resolvers'; + +import ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue'; +import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue'; +import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; +import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql'; + +import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql'; +import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql'; +import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql'; + +import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants'; + +import { mockAdminVariables, newVariable } from '../mocks'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +const mockProvide = { + endpoint: '/variables', +}; + +describe('Ci Admin Variable list', () => { + let wrapper; + + let mockApollo; + let mockVariables; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findCiTable = () => wrapper.findComponent(GlTable); + const findCiSettings = () => wrapper.findComponent(ciVariableSettings); + + // eslint-disable-next-line consistent-return + const createComponentWithApollo = async ({ isLoading = false } = {}) => { + const handlers = [[getAdminVariables, mockVariables]]; + + mockApollo = createMockApollo(handlers, resolvers); + + wrapper = shallowMount(ciAdminVariables, { + provide: mockProvide, + apolloProvider: mockApollo, + stubs: { ciVariableSettings, ciVariableTable }, + }); + + if (!isLoading) { + return waitForPromises(); + } + }; + + beforeEach(() => { + mockVariables = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('while queries are being fetch', () => { + beforeEach(() => { + createComponentWithApollo({ isLoading: true }); + }); + + it('shows a loading icon', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findCiTable().exists()).toBe(false); + }); + }); + + describe('when queries are resolved', () => { + describe('successfuly', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockAdminVariables); + + await createComponentWithApollo(); + }); + + it('passes down the expected environments as props', () => { + expect(findCiSettings().props('environments')).toEqual([]); + }); + + it('passes down the expected variables as props', () => { + expect(findCiSettings().props('variables')).toEqual( + mockAdminVariables.data.ciVariables.nodes, + ); + }); + + it('createFlash was not called', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + }); + + describe('with an error for variables', () => { + beforeEach(async () => { + mockVariables.mockRejectedValue(); + + await createComponentWithApollo(); + }); + + it('calls createFlash with the expected error message', () => { + expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText }); + }); + }); + }); + + describe('mutations', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockAdminVariables); + + await createComponentWithApollo(); + }); + it.each` + actionName | mutation | event + ${'add'} | ${addAdminVariable} | ${'add-variable'} + ${'update'} | ${updateAdminVariable} | ${'update-variable'} + ${'delete'} | ${deleteAdminVariable} | ${'delete-variable'} + `( + 'calls the right mutation when user performs $actionName variable', + async ({ event, mutation }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(); + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation, + variables: { + endpoint: mockProvide.endpoint, + variable: newVariable, + }, + }); + }, + ); + + it.each` + actionName | event | mutationName + ${'add'} | ${'add-variable'} | ${'addAdminVariable'} + ${'update'} | ${'update-variable'} | ${'updateAdminVariable'} + ${'delete'} | ${'delete-variable'} | ${'deleteAdminVariable'} + `( + 'throws with the specific graphql error if present when user performs $actionName variable', + async ({ event, mutationName }) => { + const graphQLErrorMessage = 'There is a problem with this graphQL action'; + jest + .spyOn(wrapper.vm.$apollo, 'mutate') + .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } }); + await findCiSettings().vm.$emit(event, newVariable); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage }); + }, + ); + + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable', + async ({ event }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => { + throw new Error(); + }); + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText }); + }, + ); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js new file mode 100644 index 00000000000..e9966576cab --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -0,0 +1,139 @@ +import { GlDropdown, GlDropdownItem, GlIcon, GlSearchBoxByType } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { allEnvironments } from '~/ci_variable_list/constants'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; + +describe('Ci environments dropdown', () => { + let wrapper; + + const envs = ['dev', 'prod', 'staging']; + const defaultProps = { environments: envs, selectedEnvironmentScope: '' }; + + const findDropdownText = () => wrapper.findComponent(GlDropdown).text(); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); + const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = ({ props = {}, searchTerm = '' } = {}) => { + wrapper = mount(CiEnvironmentsDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + }); + + findSearchBox().vm.$emit('input', searchTerm); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No environments found', () => { + beforeEach(() => { + createComponent({ searchTerm: 'stable' }); + }); + + it('renders create button with search term if environments do not contain search term', () => { + expect(findAllDropdownItems()).toHaveLength(2); + expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable'); + }); + + it('renders empty results message', () => { + expect(findDropdownItemByIndex(0).text()).toBe('No matching results'); + }); + }); + + describe('Search term is empty', () => { + beforeEach(() => { + createComponent({ props: { environments: envs } }); + }); + + it('renders all environments when search term is empty', () => { + expect(findAllDropdownItems()).toHaveLength(3); + expect(findDropdownItemByIndex(0).text()).toBe(envs[0]); + expect(findDropdownItemByIndex(1).text()).toBe(envs[1]); + expect(findDropdownItemByIndex(2).text()).toBe(envs[2]); + }); + + it('should not display active checkmark on the inactive stage', () => { + expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true); + }); + }); + + describe('when `*` is the value of selectedEnvironmentScope props', () => { + const wildcardScope = '*'; + + beforeEach(() => { + createComponent({ props: { selectedEnvironmentScope: wildcardScope } }); + }); + + it('shows the `All environments` text and not the wildcard', () => { + expect(findDropdownText()).toContain(allEnvironments.text); + expect(findDropdownText()).not.toContain(wildcardScope); + }); + }); + + describe('Environments found', () => { + const currentEnv = envs[2]; + + beforeEach(async () => { + createComponent({ searchTerm: currentEnv }); + await nextTick(); + }); + + it('renders only the environment searched for', () => { + expect(findAllDropdownItems()).toHaveLength(1); + expect(findDropdownItemByIndex(0).text()).toBe(currentEnv); + }); + + it('should not display create button', () => { + const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create')); + expect(environments).toHaveLength(0); + expect(findAllDropdownItems()).toHaveLength(1); + }); + + it('should not display empty results message', () => { + expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false); + }); + + it('should clear the search term when showing the dropdown', () => { + wrapper.findComponent(GlDropdown).trigger('click'); + + expect(findSearchBox().text()).toBe(''); + }); + + describe('Custom events', () => { + describe('when clicking on an environment', () => { + const itemIndex = 0; + + beforeEach(() => { + createComponent(); + }); + + it('should emit `select-environment` if an environment is clicked', async () => { + await nextTick(); + + await findDropdownItemByIndex(itemIndex).vm.$emit('click'); + + expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]); + }); + }); + + describe('when creating a new environment from a search term', () => { + const search = 'new-env'; + beforeEach(() => { + createComponent({ searchTerm: search }); + }); + + it('should emit createClicked if an environment is clicked', async () => { + await nextTick(); + findDropdownItemByIndex(1).vm.$emit('click'); + expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js new file mode 100644 index 00000000000..e45656acfd8 --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js @@ -0,0 +1,183 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { resolvers } from '~/ci_variable_list/graphql/resolvers'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; + +import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue'; +import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue'; +import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; +import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql'; + +import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql'; +import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql'; +import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql'; + +import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants'; + +import { mockGroupVariables, newVariable } from '../mocks'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +const mockProvide = { + endpoint: '/variables', + groupPath: '/namespace/group', + groupId: 1, +}; + +describe('Ci Group Variable list', () => { + let wrapper; + + let mockApollo; + let mockVariables; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findCiTable = () => wrapper.findComponent(GlTable); + const findCiSettings = () => wrapper.findComponent(ciVariableSettings); + + // eslint-disable-next-line consistent-return + const createComponentWithApollo = async ({ isLoading = false } = {}) => { + const handlers = [[getGroupVariables, mockVariables]]; + + mockApollo = createMockApollo(handlers, resolvers); + + wrapper = shallowMount(ciGroupVariables, { + provide: mockProvide, + apolloProvider: mockApollo, + stubs: { ciVariableSettings, ciVariableTable }, + }); + + if (!isLoading) { + return waitForPromises(); + } + }; + + beforeEach(() => { + mockVariables = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('while queries are being fetch', () => { + beforeEach(() => { + createComponentWithApollo({ isLoading: true }); + }); + + it('shows a loading icon', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findCiTable().exists()).toBe(false); + }); + }); + + describe('when queries are resolved', () => { + describe('successfuly', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + + await createComponentWithApollo(); + }); + + it('passes down the expected environments as props', () => { + expect(findCiSettings().props('environments')).toEqual([]); + }); + + it('passes down the expected variables as props', () => { + expect(findCiSettings().props('variables')).toEqual( + mockGroupVariables.data.group.ciVariables.nodes, + ); + }); + + it('createFlash was not called', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + }); + + describe('with an error for variables', () => { + beforeEach(async () => { + mockVariables.mockRejectedValue(); + + await createComponentWithApollo(); + }); + + it('calls createFlash with the expected error message', () => { + expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText }); + }); + }); + }); + + describe('mutations', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + + await createComponentWithApollo(); + }); + it.each` + actionName | mutation | event + ${'add'} | ${addGroupVariable} | ${'add-variable'} + ${'update'} | ${updateGroupVariable} | ${'update-variable'} + ${'delete'} | ${deleteGroupVariable} | ${'delete-variable'} + `( + 'calls the right mutation when user performs $actionName variable', + async ({ event, mutation }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(); + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation, + variables: { + endpoint: mockProvide.endpoint, + fullPath: mockProvide.groupPath, + groupId: convertToGraphQLId('Group', mockProvide.groupId), + variable: newVariable, + }, + }); + }, + ); + + it.each` + actionName | event | mutationName + ${'add'} | ${'add-variable'} | ${'addGroupVariable'} + ${'update'} | ${'update-variable'} | ${'updateGroupVariable'} + ${'delete'} | ${'delete-variable'} | ${'deleteGroupVariable'} + `( + 'throws with the specific graphql error if present when user performs $actionName variable', + async ({ event, mutationName }) => { + const graphQLErrorMessage = 'There is a problem with this graphQL action'; + jest + .spyOn(wrapper.vm.$apollo, 'mutate') + .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } }); + await findCiSettings().vm.$emit(event, newVariable); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage }); + }, + ); + + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable', + async ({ event }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => { + throw new Error(); + }); + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText }); + }, + ); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js new file mode 100644 index 00000000000..e5019e3261e --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -0,0 +1,383 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; +import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import { + ADD_VARIABLE_ACTION, + AWS_ACCESS_KEY_ID, + EDIT_VARIABLE_ACTION, + EVENT_LABEL, + EVENT_ACTION, + ENVIRONMENT_SCOPE_LINK_TITLE, + instanceString, +} from '~/ci_variable_list/constants'; +import { mockVariablesWithScopes } from '../mocks'; +import ModalStub from '../stubs'; + +describe('Ci variable modal', () => { + let wrapper; + let trackingSpy; + + const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; + const mockVariables = mockVariablesWithScopes(instanceString); + + const defaultProvide = { + awsLogoSvgPath: '/logo', + awsTipCommandsLink: '/tips', + awsTipDeployLink: '/deploy', + awsTipLearnLink: '/learn-link', + containsVariableReferenceLink: '/reference', + environmentScopeLink: '/help/environments', + isProtectedByDefault: false, + maskedEnvironmentVariablesLink: '/variables-link', + maskableRegex, + protectedEnvironmentVariablesLink: '/protected-link', + }; + + const defaultProps = { + areScopedVariablesAvailable: true, + environments: [], + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + variable: [], + }; + + const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => { + wrapper = mountFn(CiVariableModal, { + attachTo: document.body, + provide: { ...defaultProvide, ...provide }, + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal: ModalStub, + }, + }); + }; + + const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); + const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference'); + const findModal = () => wrapper.find(ModalStub); + const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip'); + const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn'); + const deleteVariableButton = () => + findModal() + .findAll(GlButton) + .wrappers.find((button) => button.props('variant') === 'danger'); + const findProtectedVariableCheckbox = () => + wrapper.findByTestId('ci-variable-protected-checkbox'); + const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox'); + const findValueField = () => wrapper.find('#ci-variable-value'); + const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link'); + const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Adding a variable', () => { + describe('when no key/value pair are present', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the submit button as disabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBe('true'); + }); + }); + + describe('when a key/value pair is present', () => { + beforeEach(() => { + createComponent({ props: { selectedVariable: mockVariables[0] } }); + }); + + it('shows the submit button as enabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); + }); + }); + + describe('events', () => { + const [currentVariable] = mockVariables; + + beforeEach(() => { + createComponent({ props: { selectedVariable: currentVariable } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('Dispatches `add-variable` action on submit', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]); + }); + + it('Dispatches the `hideModal` event when dismissing', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + }); + }); + + describe('when protected by default', () => { + describe('when adding a new variable', () => { + beforeEach(() => { + createComponent({ provide: { isProtectedByDefault: true } }); + findModal().vm.$emit('shown'); + }); + + it('updates the protected value to true', () => { + expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe( + 'true', + ); + }); + }); + + describe('when editing a variable', () => { + beforeEach(() => { + createComponent({ + provide: { isProtectedByDefault: false }, + props: { + selectedVariable: {}, + mode: EDIT_VARIABLE_ACTION, + }, + }); + findModal().vm.$emit('shown'); + }); + + it('keeps the value as false', async () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeUndefined(); + }); + }); + }); + + describe('Adding a new non-AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariables; + createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } }); + }); + + it('does not show AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(false); + }); + }); + + describe('Adding a new AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariables; + const AWSKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } }); + }); + + it('shows AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(true); + }); + }); + + describe('Reference warning when adding a variable', () => { + describe('with a $ character', () => { + beforeEach(() => { + const [variable] = mockVariables; + const variableWithDollarSign = { + ...variable, + value: 'valueWith$', + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variableWithDollarSign }, + }); + }); + + it(`renders the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(true); + }); + }); + + describe('without a $ character', () => { + beforeEach(() => { + const [variable] = mockVariables; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variable }, + }); + }); + + it(`does not render the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(false); + }); + }); + }); + + describe('Editing a variable', () => { + const [variable] = mockVariables; + + beforeEach(() => { + createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('button text is Update variable when updating', () => { + expect(findAddorUpdateButton().text()).toBe('Update variable'); + }); + + it('Update variable button dispatches updateVariable with correct variable', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('update-variable')).toEqual([[variable]]); + }); + + it('Propagates the `hideModal` event', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + + it('dispatches `delete-variable` with correct variable to delete', () => { + deleteVariableButton().vm.$emit('click'); + expect(wrapper.emitted('delete-variable')).toEqual([[variable]]); + }); + }); + + describe('Environment scope', () => { + describe('when feature is available', () => { + it('renders the environment dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: true, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(true); + expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); + }); + + it('renders a link to documentation on scopes', () => { + createComponent({ mountFn: mountExtended }); + + const link = findEnvScopeLink(); + + expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE); + expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink); + }); + }); + + describe('when feature is not available', () => { + it('disables the dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: false, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(findEnvScopeInput().attributes('readonly')).toBe('readonly'); + }); + }); + }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the mask state is invalid', () => { + beforeEach(async () => { + const [variable] = mockVariables; + const invalidMaskVariable = { + ...variable, + value: 'd:;', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidMaskVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findMaskedVariableCheckbox().trigger('click'); + }); + + it('disables the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled'); + }); + + it('shows the correct error text', () => { + expect(findModal().text()).toContain(maskError); + }); + + it('sends the correct tracking event', () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: ';', + }); + }); + }); + + describe.each` + value | masked | eventSent | trackingErrorProperty + ${'secretValue'} | ${false} | ${0} | ${null} + ${'short'} | ${true} | ${0} | ${null} + ${'dollar$ign'} | ${false} | ${1} | ${'$'} + ${'dollar$ign'} | ${true} | ${1} | ${'$'} + ${'unsupported|char'} | ${true} | ${1} | ${'|'} + ${'unsupported|char'} | ${false} | ${0} | ${null} + `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => { + beforeEach(async () => { + const [variable] = mockVariables; + const invalidKeyVariable = { + ...variable, + value: '', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidKeyVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findValueField().vm.$emit('input', value); + if (masked) { + await findMaskedVariableCheckbox().trigger('click'); + } + }); + + it(`${ + eventSent > 0 ? 'sends the correct' : 'does not send the' + } variable validation tracking event with ${value}`, () => { + expect(trackingSpy).toHaveBeenCalledTimes(eventSent); + + if (eventSent > 0) { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: trackingErrorProperty, + }); + } + }); + }); + + describe('when masked variable has acceptable value', () => { + beforeEach(() => { + const [variable] = mockVariables; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: '12345678', + masked: true, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validMaskandKeyVariable }, + }); + }); + + it('does not disable the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js new file mode 100644 index 00000000000..5c77ce71b41 --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js @@ -0,0 +1,128 @@ +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue'; +import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; +import { + ADD_VARIABLE_ACTION, + EDIT_VARIABLE_ACTION, + projectString, +} from '~/ci_variable_list/constants'; +import { mapEnvironmentNames } from '~/ci_variable_list/utils'; + +import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks'; + +describe('Ci variable table', () => { + let wrapper; + + const defaultProps = { + areScopedVariablesAvailable: true, + environments: mapEnvironmentNames(mockEnvs), + isLoading: false, + variables: mockVariablesWithScopes(projectString), + }; + + const findCiVariableTable = () => wrapper.findComponent(ciVariableTable); + const findCiVariableModal = () => wrapper.findComponent(ciVariableModal); + + const createComponent = () => { + wrapper = shallowMount(CiVariableSettings, { + propsData: { + ...defaultProps, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('props passing', () => { + it('passes props down correctly to the ci table', () => { + expect(findCiVariableTable().props()).toEqual({ + isLoading: defaultProps.isLoading, + variables: defaultProps.variables, + }); + }); + + it('passes props down correctly to the ci modal', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + expect(findCiVariableModal().props()).toEqual({ + areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable, + environments: defaultProps.environments, + variables: defaultProps.variables, + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + }); + }); + }); + + describe('modal mode', () => { + it('passes down ADD mode when receiving an empty variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION); + }); + + it('passes down EDIT mode when receiving a variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable', newVariable); + await nextTick(); + + expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION); + }); + }); + + describe('variable modal', () => { + it('is hidden by default', () => { + expect(findCiVariableModal().exists()).toBe(false); + }); + + it('shows modal when adding a new variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + expect(findCiVariableModal().exists()).toBe(true); + }); + + it('shows modal when updating a variable', async () => { + findCiVariableTable().vm.$emit('set-selected-variable', newVariable); + await nextTick(); + + expect(findCiVariableModal().exists()).toBe(true); + }); + + it('hides modal when receiving the event from the modal', async () => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + findCiVariableModal().vm.$emit('hideModal'); + await nextTick(); + + expect(findCiVariableModal().exists()).toBe(false); + }); + }); + + describe('variable events', () => { + it.each` + eventName + ${'add-variable'} + ${'update-variable'} + ${'delete-variable'} + `('bubbles up the $eventName event', async ({ eventName }) => { + findCiVariableTable().vm.$emit('set-selected-variable'); + await nextTick(); + + findCiVariableModal().vm.$emit(eventName, newVariable); + await nextTick(); + + expect(wrapper.emitted(eventName)).toEqual([[newVariable]]); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js new file mode 100644 index 00000000000..8a4c35173ec --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js @@ -0,0 +1,98 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; +import { projectString } from '~/ci_variable_list/constants'; +import { mockVariables } from '../mocks'; + +describe('Ci variable table', () => { + let wrapper; + + const defaultProps = { + isLoading: false, + variables: mockVariables(projectString), + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = mountExtended(CiVariableTable, { + attachTo: document.body, + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const findRevealButton = () => wrapper.findByText('Reveal values'); + const findAddButton = () => wrapper.findByLabelText('Add'); + const findEditButton = () => wrapper.findByLabelText('Edit'); + const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.'); + const findHiddenValues = () => wrapper.findAll('[data-testid="hiddenValue"]'); + const findRevealedValues = () => wrapper.findAll('[data-testid="revealedValue"]'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('When table is empty', () => { + beforeEach(() => { + createComponent({ props: { variables: [] } }); + }); + + it('displays empty message', () => { + expect(findEmptyVariablesPlaceholder().exists()).toBe(true); + }); + + it('hides the reveal button', () => { + expect(findRevealButton().exists()).toBe(false); + }); + }); + + describe('When table has variables', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not display the empty message', () => { + expect(findEmptyVariablesPlaceholder().exists()).toBe(false); + }); + + it('displays the reveal button', () => { + expect(findRevealButton().exists()).toBe(true); + }); + + it('displays the correct amount of variables', async () => { + expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length); + }); + }); + + describe('Table click actions', () => { + beforeEach(() => { + createComponent(); + }); + + it('reveals secret values when button is clicked', async () => { + expect(findHiddenValues()).toHaveLength(defaultProps.variables.length); + expect(findRevealedValues()).toHaveLength(0); + + await findRevealButton().trigger('click'); + + expect(findHiddenValues()).toHaveLength(0); + expect(findRevealedValues()).toHaveLength(defaultProps.variables.length); + }); + + it('dispatches `setSelectedVariable` with correct variable to edit', async () => { + await findEditButton().trigger('click'); + + expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]); + }); + + it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => { + await findAddButton().trigger('click'); + + expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js index 42c6501dcce..6681ab91a4a 100644 --- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js @@ -58,7 +58,7 @@ describe('Ci variable modal', () => { }); it('button is disabled when no key/value pair are present', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + expect(findAddorUpdateButton().attributes('disabled')).toBe('true'); }); }); @@ -71,7 +71,7 @@ describe('Ci variable modal', () => { }); it('button is enabled when key/value pair are present', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); }); it('Add variable button dispatches addVariable action', () => { @@ -249,7 +249,7 @@ describe('Ci variable modal', () => { }); it('disables the submit button', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled'); }); it('shows the correct error text', () => { @@ -316,7 +316,7 @@ describe('Ci variable modal', () => { }); it('does not disable the submit button', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); }); }); }); diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js new file mode 100644 index 00000000000..89ba77858dc --- /dev/null +++ b/spec/frontend/ci_variable_list/mocks.js @@ -0,0 +1,109 @@ +import { variableTypes, groupString, instanceString } from '~/ci_variable_list/constants'; + +export const devName = 'dev'; +export const prodName = 'prod'; + +export const mockVariables = (kind) => { + return [ + { + __typename: `Ci${kind}Variable`, + id: 1, + key: 'my-var', + masked: false, + protected: true, + value: 'env_val', + variableType: variableTypes.variableType, + }, + { + __typename: `Ci${kind}Variable`, + id: 2, + key: 'secret', + masked: true, + protected: false, + value: 'the_secret_value', + variableType: variableTypes.fileType, + }, + ]; +}; + +export const mockVariablesWithScopes = (kind) => + mockVariables(kind).map((variable) => { + return { ...variable, environmentScope: '*' }; + }); + +const createDefaultVars = ({ withScope = true, kind } = {}) => { + let base = mockVariables(kind); + + if (withScope) { + base = mockVariablesWithScopes(kind); + } + + return { + __typename: `Ci${kind}VariableConnection`, + nodes: base, + }; +}; + +const defaultEnvs = { + __typename: 'EnvironmentConnection', + nodes: [ + { + __typename: 'Environment', + id: 1, + name: prodName, + }, + { + __typename: 'Environment', + id: 2, + name: devName, + }, + ], +}; + +export const mockEnvs = defaultEnvs.nodes; + +export const mockProjectEnvironments = { + data: { + project: { + __typename: 'Project', + id: 1, + environments: defaultEnvs, + }, + }, +}; + +export const mockProjectVariables = { + data: { + project: { + __typename: 'Project', + id: 1, + ciVariables: createDefaultVars(), + }, + }, +}; + +export const mockGroupVariables = { + data: { + group: { + __typename: 'Group', + id: 1, + ciVariables: createDefaultVars({ kind: groupString }), + }, + }, +}; + +export const mockAdminVariables = { + data: { + ciVariables: createDefaultVars({ withScope: false, kind: instanceString }), + }, +}; + +export const newVariable = { + id: 3, + environmentScope: 'new', + key: 'AWS_RANDOM_THING', + masked: true, + protected: false, + value: 'devops', + variableType: variableTypes.variableType, +}; diff --git a/spec/frontend/ci_variable_list/utils_spec.js b/spec/frontend/ci_variable_list/utils_spec.js new file mode 100644 index 00000000000..081c399792f --- /dev/null +++ b/spec/frontend/ci_variable_list/utils_spec.js @@ -0,0 +1,78 @@ +import { + createJoinedEnvironments, + convertEnvironmentScope, + mapEnvironmentNames, +} from '~/ci_variable_list/utils'; +import { allEnvironments } from '~/ci_variable_list/constants'; + +describe('utils', () => { + const environments = ['dev', 'prod']; + const newEnvironments = ['staging']; + + describe('createJoinedEnvironments', () => { + it('returns only `environments` if `variables` argument is undefined', () => { + const variables = undefined; + + expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments); + }); + + it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => { + const envScope1 = 'new1'; + const envScope2 = 'new2'; + + const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; + + expect(createJoinedEnvironments(variables, environments, [])).toEqual([ + environments[0], + envScope1, + envScope2, + environments[1], + ]); + }); + + it('returns combined list with new environments included', () => { + const variables = undefined; + + expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([ + ...environments, + ...newEnvironments, + ]); + }); + + it('removes duplicate environments', () => { + const envScope1 = environments[0]; + const envScope2 = 'new2'; + + const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; + + expect(createJoinedEnvironments(variables, environments, [])).toEqual([ + environments[0], + envScope2, + environments[1], + ]); + }); + }); + + describe('convertEnvironmentScope', () => { + it('converts the * to the `All environments` text', () => { + expect(convertEnvironmentScope('*')).toBe(allEnvironments.text); + }); + + it('returns the environment as is if not the *', () => { + expect(convertEnvironmentScope('prod')).toBe('prod'); + }); + }); + + describe('mapEnvironmentNames', () => { + const envName = 'dev'; + const envName2 = 'prod'; + + const nodes = [ + { name: envName, otherProp: {} }, + { name: envName2, otherProp: {} }, + ]; + it('flatten a nodes array with only their names', () => { + expect(mapEnvironmentNames(nodes)).toEqual([envName, envName2]); + }); + }); +}); diff --git a/spec/frontend/clusters/agents/components/activity_history_item_spec.js b/spec/frontend/clusters/agents/components/activity_history_item_spec.js index 100a280d0cc..68f6f11aa8f 100644 --- a/spec/frontend/clusters/agents/components/activity_history_item_spec.js +++ b/spec/frontend/clusters/agents/components/activity_history_item_spec.js @@ -23,7 +23,7 @@ describe('ActivityHistoryItem', () => { }; const findHistoryItem = () => wrapper.findComponent(HistoryItem); - const findTimeAgo = () => wrapper.find(TimeAgoTooltip); + const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js index ad48afe10b6..0d10801e80e 100644 --- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js +++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js @@ -55,7 +55,7 @@ describe('CreateTokenModal', () => { const findAgentInstructions = () => findModal().findComponent(AgentToken); const findButtonByVariant = (variant) => findModal() - .findAll(GlButton) + .findAllComponents(GlButton) .wrappers.find((button) => button.props('variant') === variant); const findActionButton = () => findButtonByVariant('confirm'); const findCancelButton = () => wrapper.findByTestId('agent-token-close-button'); diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js index 6caeaf5c192..334615f1818 100644 --- a/spec/frontend/clusters/agents/components/token_table_spec.js +++ b/spec/frontend/clusters/agents/components/token_table_spec.js @@ -136,8 +136,8 @@ describe('ClusterAgentTokenTable', () => { const token = tokens.at(lineNumber); expect(token.text()).toContain(description); - expect(token.find(GlTruncate).exists()).toBe(truncatesText); - expect(token.find(GlTooltip).exists()).toBe(hasTooltip); + expect(token.findComponent(GlTruncate).exists()).toBe(truncatesText); + expect(token.findComponent(GlTooltip).exists()).toBe(hasTooltip); }, ); diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index b5345ea8915..ad2aa4acbaf 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -1,7 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; -import { setTestTimeout } from 'helpers/timeout'; import Clusters from '~/clusters/clusters_bundle'; import axios from '~/lib/utils/axios_utils'; import initProjectSelectDropdown from '~/project_select'; @@ -12,8 +11,6 @@ jest.mock('~/project_select'); useMockLocationHelper(); describe('Clusters', () => { - setTestTimeout(1000); - let cluster; let mock; @@ -60,9 +57,9 @@ describe('Clusters', () => { it('should show the creating container', () => { cluster.updateContainer(null, 'creating'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(false); + expect(cluster.successContainer.classList.contains('hidden')).toBe(true); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(true); expect(window.location.reload).not.toHaveBeenCalled(); }); @@ -70,9 +67,9 @@ describe('Clusters', () => { cluster.updateContainer(null, 'creating'); cluster.updateContainer('creating', 'creating'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(false); + expect(cluster.successContainer.classList.contains('hidden')).toBe(true); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(true); expect(window.location.reload).not.toHaveBeenCalled(); }); }); @@ -83,9 +80,9 @@ describe('Clusters', () => { cluster.updateContainer(null, 'creating'); cluster.updateContainer('creating', 'created'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true); + expect(cluster.successContainer.classList.contains('hidden')).toBe(true); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(true); expect(window.location.reload).toHaveBeenCalled(); expect(cluster.setClusterNewlyCreated).toHaveBeenCalledWith(true); }); @@ -97,9 +94,9 @@ describe('Clusters', () => { cluster.updateContainer(null, 'created'); cluster.updateContainer('created', 'created'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true); + expect(cluster.successContainer.classList.contains('hidden')).toBe(false); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(true); expect(window.location.reload).not.toHaveBeenCalled(); expect(cluster.setClusterNewlyCreated).toHaveBeenCalledWith(false); }); @@ -111,9 +108,9 @@ describe('Clusters', () => { cluster.updateContainer(null, 'created'); cluster.updateContainer('created', 'created'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); - expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true); + expect(cluster.successContainer.classList.contains('hidden')).toBe(true); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(true); expect(window.location.reload).not.toHaveBeenCalled(); expect(cluster.setClusterNewlyCreated).not.toHaveBeenCalled(); }); @@ -123,11 +120,11 @@ describe('Clusters', () => { it('should show the error container', () => { cluster.updateContainer(null, 'errored', 'this is an error'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.successContainer.classList.contains('hidden')).toBe(true); - expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy(); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(false); expect(cluster.errorReasonContainer.textContent).toContain('this is an error'); }); @@ -135,11 +132,11 @@ describe('Clusters', () => { it('should show `error` banner when previously `creating`', () => { cluster.updateContainer('creating', 'errored'); - expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true); - expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy(); + expect(cluster.successContainer.classList.contains('hidden')).toBe(true); - expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy(); + expect(cluster.errorContainer.classList.contains('hidden')).toBe(false); }); }); diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js index f9df70b9f87..ef39c90aaef 100644 --- a/spec/frontend/clusters/components/new_cluster_spec.js +++ b/spec/frontend/clusters/components/new_cluster_spec.js @@ -12,9 +12,9 @@ describe('NewCluster', () => { await nextTick(); }; - const findDescription = () => wrapper.find(GlSprintf); + const findDescription = () => wrapper.findComponent(GlSprintf); - const findLink = () => wrapper.find(GlLink); + const findLink = () => wrapper.findComponent(GlLink); beforeEach(() => { return createWrapper(); diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js index 67d442bfdc5..b17886a5826 100644 --- a/spec/frontend/clusters/forms/components/integration_form_spec.js +++ b/spec/frontend/clusters/forms/components/integration_form_spec.js @@ -32,8 +32,8 @@ describe('ClusterIntegrationForm', () => { wrapper = null; }; - const findSubmitButton = () => wrapper.find(GlButton); - const findGlToggle = () => wrapper.find(GlToggle); + const findSubmitButton = () => wrapper.findComponent(GlButton); + const findGlToggle = () => wrapper.findComponent(GlToggle); afterEach(() => { destroyWrapper(); diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js index 29884675b24..964dd005a27 100644 --- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js +++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js @@ -150,7 +150,6 @@ describe('InstallAgentModal', () => { }); it("doesn't render agent installation instructions", () => { - expect(findModal().text()).not.toContain(i18n.basicInstallTitle); expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false); expect(findModal().findComponent(GlAlert).exists()).toBe(false); }); diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js index 9b01af1e585..71ee12cf02d 100644 --- a/spec/frontend/commit/pipelines/pipelines_table_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js @@ -1,4 +1,4 @@ -import { GlEmptyState, GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui'; +import { GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; @@ -8,7 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import Api from '~/api'; import PipelinesTable from '~/commit/pipelines/pipelines_table.vue'; import httpStatusCodes from '~/lib/utils/http_status'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { TOAST_MESSAGE } from '~/pipelines/constants'; import axios from '~/lib/utils/axios_utils'; @@ -26,10 +26,12 @@ describe('Pipelines table in Commits and Merge requests', () => { const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button'); const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile'); const findLoadingState = () => wrapper.findComponent(GlLoadingIcon); - const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findErrorEmptyState = () => wrapper.findByTestId('pipeline-error-empty-state'); + const findEmptyState = () => wrapper.findByTestId('pipeline-empty-state'); const findTable = () => wrapper.findComponent(GlTableLite); const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); const findModal = () => wrapper.findComponent(GlModal); + const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link'); const createComponent = (props = {}) => { wrapper = extendedWrapper( @@ -73,7 +75,18 @@ describe('Pipelines table in Commits and Merge requests', () => { it('should render the empty state', () => { expect(findTableRows()).toHaveLength(0); expect(findLoadingState().exists()).toBe(false); - expect(findEmptyState().exists()).toBe(false); + expect(findErrorEmptyState().exists()).toBe(false); + expect(findEmptyState().exists()).toBe(true); + }); + + it('should render correct empty state content', () => { + expect(findRunPipelineBtn().exists()).toBe(true); + expect(findMrPipelinesDocsLink().attributes('href')).toBe( + '/help/ci/pipelines/merge_request_pipelines.md#prerequisites', + ); + expect(findEmptyState().text()).toContain( + 'To run a merge request pipeline, the jobs in the CI/CD configuration file must be configured to run in merge request pipelines.', + ); }); }); @@ -90,7 +103,7 @@ describe('Pipelines table in Commits and Merge requests', () => { expect(findTable().exists()).toBe(true); expect(findTableRows()).toHaveLength(1); expect(findLoadingState().exists()).toBe(false); - expect(findEmptyState().exists()).toBe(false); + expect(findErrorEmptyState().exists()).toBe(false); }); describe('with pagination', () => { @@ -226,12 +239,14 @@ describe('Pipelines table in Commits and Merge requests', () => { describe('failure', () => { const permissionsMsg = 'You do not have permission to run a pipeline on this branch.'; + const defaultMsg = + 'An error occurred while trying to run a new pipeline for this merge request.'; it.each` status | message - ${httpStatusCodes.BAD_REQUEST} | ${permissionsMsg} + ${httpStatusCodes.BAD_REQUEST} | ${defaultMsg} ${httpStatusCodes.UNAUTHORIZED} | ${permissionsMsg} - ${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${'An error occurred while trying to run a new pipeline for this merge request.'} + ${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${defaultMsg} `('displays permissions error message', async ({ status, message }) => { const response = { response: { status } }; @@ -243,7 +258,13 @@ describe('Pipelines table in Commits and Merge requests', () => { await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith({ message }); + expect(createAlert).toHaveBeenCalledWith({ + message, + primaryButton: { + text: 'Learn more', + link: '/help/ci/pipelines/merge_request_pipelines.md', + }, + }); }); }); }); @@ -293,7 +314,7 @@ describe('Pipelines table in Commits and Merge requests', () => { }); it('should render error state', () => { - expect(findEmptyState().text()).toBe( + expect(findErrorEmptyState().text()).toBe( 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', ); }); diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js index ba6d8da9584..93204deb68c 100644 --- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js @@ -182,7 +182,7 @@ describe('content_editor/components/bubble_menus/link', () => { it('updates prosemirror doc with new link', async () => { expect(tiptapEditor.getHTML()).toBe( - '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google" canonicalsrc="https://google.com">PDF File</a></p>', + '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google">PDF File</a></p>', ); }); diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js index 8839caea80e..fada4f06743 100644 --- a/spec/frontend/content_editor/components/bubble_menus/media_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js @@ -14,7 +14,7 @@ import { } from '../../test_constants'; const TIPTAP_IMAGE_HTML = `<p> - <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png"> + <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon"> </p>`; const TIPTAP_AUDIO_HTML = `<p> diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 9ee3b017831..0ba2672100b 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -19,6 +19,7 @@ describe('ContentEditor', () => { const findEditorElement = () => wrapper.findByTestId('content-editor'); const findEditorContent = () => wrapper.findComponent(EditorContent); + const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); const createWrapper = (propsData = {}) => { renderMarkdown = jest.fn(); @@ -119,4 +120,17 @@ describe('ContentEditor', () => { expect(wrapper.findComponent(FormattingBubbleMenu).exists()).toBe(true); }); + + it.each` + event + ${'loading'} + ${'loadingSuccess'} + ${'loadingError'} + `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => { + createWrapper(); + + findEditorStateObserver().vm.$emit(event); + + expect(wrapper.emitted(event)).toHaveLength(1); + }); }); diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js index 351fd967719..62fec8d4e72 100644 --- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js +++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js @@ -37,16 +37,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => { }); describe.each` - name | contentType | command | params - ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']} - ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']} - ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']} - ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']} - ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']} - ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]} - ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]} - ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]} - `('when option $label is clicked', ({ name, command, contentType, params }) => { + name | contentType | command | params + ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']} + ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']} + ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']} + ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']} + ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']} + ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]} + ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]} + ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]} + ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]} + `('when option $name is clicked', ({ name, command, contentType, params }) => { let commands; let btn; diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js index 2acb6e14ce0..8f194ff32e2 100644 --- a/spec/frontend/content_editor/components/top_toolbar_spec.js +++ b/spec/frontend/content_editor/components/top_toolbar_spec.js @@ -32,7 +32,7 @@ describe('content_editor/components/top_toolbar', () => { ${'link'} | ${{}} ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }} ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }} - ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a task list', editorCommand: 'toggleTaskList' }} + ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }} ${'image'} | ${{}} ${'table'} | ${{}} ${'more'} | ${{}} diff --git a/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap new file mode 100644 index 00000000000..fb091419ad9 --- /dev/null +++ b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`content/components/wrappers/table_of_contents collects all headings and renders a nested list of headings 1`] = ` +<div + class="table-of-contents gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5 gl-p-4!" + data-testid="table-of-contents" +> + + Table of contents + + <li> + <a + href="#" + > + + Heading 1 + + </a> + + <ul> + <li> + <a + href="#" + > + + Heading 1.1 + + </a> + + <ul> + <li> + <a + href="#" + > + + Heading 1.1.1 + + </a> + + <!----> + </li> + </ul> + </li> + <li> + <a + href="#" + > + + Heading 1.2 + + </a> + + <ul> + <li> + <a + href="#" + > + + Heading 1.2.1 + + </a> + + <!----> + </li> + </ul> + </li> + <li> + <a + href="#" + > + + Heading 1.3 + + </a> + + <!----> + </li> + <li> + <a + href="#" + > + + Heading 1.4 + + </a> + + <ul> + <li> + <a + href="#" + > + + Heading 1.4.1 + + </a> + + <!----> + </li> + </ul> + </li> + </ul> + </li> + <li> + <a + href="#" + > + + Heading 2 + + </a> + + <!----> + </li> +</div> +`; diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js index 6017a145a87..1fdddce3962 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js @@ -1,12 +1,12 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { NodeViewWrapper } from '@tiptap/vue-2'; -import { selectedRect as getSelectedRect } from 'prosemirror-tables'; +import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils'; -jest.mock('prosemirror-tables'); +jest.mock('@_ueberdosis/prosemirror-tables'); describe('content/components/wrappers/table_cell_base', () => { let wrapper; diff --git a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js new file mode 100644 index 00000000000..bfda89a8b09 --- /dev/null +++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js @@ -0,0 +1,84 @@ +import { nextTick } from 'vue'; +import { NodeViewWrapper } from '@tiptap/vue-2'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import Heading from '~/content_editor/extensions/heading'; +import Diagram from '~/content_editor/extensions/diagram'; +import TableOfContentsWrapper from '~/content_editor/components/wrappers/table_of_contents.vue'; +import { createTestEditor, createDocBuilder, emitEditorEvent } from '../../test_utils'; + +describe('content/components/wrappers/table_of_contents', () => { + let wrapper; + let tiptapEditor; + let contentEditor; + let eventHub; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ extensions: [Heading, Diagram] }); + contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') }; + eventHub = eventHubFactory(); + }; + + const createWrapper = async () => { + wrapper = mountExtended(TableOfContentsWrapper, { + propsData: { + editor: tiptapEditor, + node: { + attrs: {}, + }, + }, + stubs: { + NodeViewWrapper: stubComponent(NodeViewWrapper), + }, + provide: { + contentEditor, + tiptapEditor, + eventHub, + }, + }); + }; + + beforeEach(async () => { + buildEditor(); + createWrapper(); + + const { + builders: { heading, doc }, + } = createDocBuilder({ + tiptapEditor, + names: { + heading: { nodeType: Heading.name }, + }, + }); + + const initialDoc = doc( + heading({ level: 1 }, 'Heading 1'), + heading({ level: 2 }, 'Heading 1.1'), + heading({ level: 3 }, 'Heading 1.1.1'), + heading({ level: 2 }, 'Heading 1.2'), + heading({ level: 3 }, 'Heading 1.2.1'), + heading({ level: 2 }, 'Heading 1.3'), + heading({ level: 2 }, 'Heading 1.4'), + heading({ level: 3 }, 'Heading 1.4.1'), + heading({ level: 1 }, 'Heading 2'), + ); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + await emitEditorEvent({ event: 'update', tiptapEditor }); + await nextTick(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a node-view-wrapper as a ul element', () => { + expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('ul'); + }); + + it('collects all headings and renders a nested list of headings', () => { + expect(wrapper.findComponent(NodeViewWrapper).element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js index 256f7bad309..f73b0143fd9 100644 --- a/spec/frontend/content_editor/extensions/image_spec.js +++ b/spec/frontend/content_editor/extensions/image_spec.js @@ -35,7 +35,7 @@ describe('content_editor/extensions/image', () => { tiptapEditor.commands.setContent(initialDoc.toJSON()); expect(tiptapEditor.getHTML()).toEqual( - '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image" data-canonical-src="uploads/image.jpg"></p>', + '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>', ); }); }); diff --git a/spec/frontend/content_editor/markdown_processing_spec_helper.js b/spec/frontend/content_editor/markdown_processing_spec_helper.js index 41442dd8388..228d009e42c 100644 --- a/spec/frontend/content_editor/markdown_processing_spec_helper.js +++ b/spec/frontend/content_editor/markdown_processing_spec_helper.js @@ -2,7 +2,6 @@ import fs from 'fs'; import jsYaml from 'js-yaml'; import { memoize } from 'lodash'; import { createContentEditor } from '~/content_editor'; -import { setTestTimeoutOnce } from 'helpers/timeout'; const getFocusedMarkdownExamples = memoize( () => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [], @@ -76,9 +75,6 @@ export const describeMarkdownProcessing = (description, markdownYamlPath) => { } it(exampleName, async () => { - if (name === 'frontmatter_toml') { - setTestTimeoutOnce(2000); - } await testSerializesHtmlToMarkdownForElement(example); }); }); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index 48adceaab58..7ae0a7c13c1 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -5,6 +5,7 @@ import Code from '~/content_editor/extensions/code'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; import FootnoteDefinition from '~/content_editor/extensions/footnote_definition'; import FootnoteReference from '~/content_editor/extensions/footnote_reference'; +import Frontmatter from '~/content_editor/extensions/frontmatter'; import HardBreak from '~/content_editor/extensions/hard_break'; import HTMLNodes from '~/content_editor/extensions/html_nodes'; import Heading from '~/content_editor/extensions/heading'; @@ -15,6 +16,7 @@ import Link from '~/content_editor/extensions/link'; import ListItem from '~/content_editor/extensions/list_item'; import OrderedList from '~/content_editor/extensions/ordered_list'; import Paragraph from '~/content_editor/extensions/paragraph'; +import ReferenceDefinition from '~/content_editor/extensions/reference_definition'; import Sourcemap from '~/content_editor/extensions/sourcemap'; import Strike from '~/content_editor/extensions/strike'; import Table from '~/content_editor/extensions/table'; @@ -37,6 +39,7 @@ const tiptapEditor = createTestEditor({ CodeBlockHighlight, FootnoteDefinition, FootnoteReference, + Frontmatter, HardBreak, Heading, HorizontalRule, @@ -45,6 +48,7 @@ const tiptapEditor = createTestEditor({ Link, ListItem, OrderedList, + ReferenceDefinition, Sourcemap, Strike, Table, @@ -69,6 +73,7 @@ const { div, footnoteDefinition, footnoteReference, + frontmatter, hardBreak, heading, horizontalRule, @@ -78,6 +83,7 @@ const { listItem, orderedList, pre, + referenceDefinition, strike, table, tableRow, @@ -96,6 +102,7 @@ const { codeBlock: { nodeType: CodeBlockHighlight.name }, footnoteDefinition: { nodeType: FootnoteDefinition.name }, footnoteReference: { nodeType: FootnoteReference.name }, + frontmatter: { nodeType: Frontmatter.name }, hardBreak: { nodeType: HardBreak.name }, heading: { nodeType: Heading.name }, horizontalRule: { nodeType: HorizontalRule.name }, @@ -105,6 +112,7 @@ const { listItem: { nodeType: ListItem.name }, orderedList: { nodeType: OrderedList.name }, paragraph: { nodeType: Paragraph.name }, + referenceDefinition: { nodeType: ReferenceDefinition.name }, strike: { nodeType: Strike.name }, table: { nodeType: Table.name }, tableCell: { nodeType: TableCell.name }, @@ -253,7 +261,12 @@ describe('Client side Markdown processing', () => { expectedDoc: doc( paragraph( source('<img src="bar" alt="foo" />'), - image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), + image({ + ...source('<img src="bar" alt="foo" />'), + alt: 'foo', + canonicalSrc: 'bar', + src: 'bar', + }), ), ), }, @@ -271,7 +284,12 @@ describe('Client side Markdown processing', () => { ), paragraph( source('<img src="bar" alt="foo" />'), - image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), + image({ + ...source('<img src="bar" alt="foo" />'), + alt: 'foo', + src: 'bar', + canonicalSrc: 'bar', + }), ), ), }, @@ -284,6 +302,7 @@ describe('Client side Markdown processing', () => { { ...source('[GitLab](https://gitlab.com "Go to GitLab")'), href: 'https://gitlab.com', + canonicalSrc: 'https://gitlab.com', title: 'Go to GitLab', }, 'GitLab', @@ -302,6 +321,7 @@ describe('Client side Markdown processing', () => { { ...source('[GitLab](https://gitlab.com "Go to GitLab")'), href: 'https://gitlab.com', + canonicalSrc: 'https://gitlab.com', title: 'Go to GitLab', }, 'GitLab', @@ -318,6 +338,7 @@ describe('Client side Markdown processing', () => { link( { ...source('www.commonmark.org'), + canonicalSrc: 'http://www.commonmark.org', href: 'http://www.commonmark.org', }, 'www.commonmark.org', @@ -334,6 +355,7 @@ describe('Client side Markdown processing', () => { link( { ...source('www.commonmark.org/help'), + canonicalSrc: 'http://www.commonmark.org/help', href: 'http://www.commonmark.org/help', }, 'www.commonmark.org/help', @@ -351,6 +373,7 @@ describe('Client side Markdown processing', () => { link( { ...source('hello+xyz@mail.example'), + canonicalSrc: 'mailto:hello+xyz@mail.example', href: 'mailto:hello+xyz@mail.example', }, 'hello+xyz@mail.example', @@ -369,6 +392,7 @@ describe('Client side Markdown processing', () => { { sourceMapKey: null, sourceMarkdown: null, + canonicalSrc: 'https://gitlab.com', href: 'https://gitlab.com', }, 'https://gitlab.com', @@ -398,6 +422,7 @@ hard line break`, image({ ...source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'), alt: 'GitLab Logo', + canonicalSrc: 'https://gitlab.com/logo.png', src: 'https://gitlab.com/logo.png', title: 'GitLab Logo', }), @@ -591,7 +616,12 @@ two paragraph( source('List item with an image ![bar](foo.png)'), 'List item with an image', - image({ ...source('![bar](foo.png)'), alt: 'bar', src: 'foo.png' }), + image({ + ...source('![bar](foo.png)'), + alt: 'bar', + canonicalSrc: 'foo.png', + src: 'foo.png', + }), ), ), ), @@ -940,8 +970,17 @@ Paragraph paragraph( source('[![moon](moon.jpg)](/uri)'), link( - { ...source('[![moon](moon.jpg)](/uri)'), href: '/uri' }, - image({ ...source('![moon](moon.jpg)'), src: 'moon.jpg', alt: 'moon' }), + { + ...source('[![moon](moon.jpg)](/uri)'), + canonicalSrc: '/uri', + href: '/uri', + }, + image({ + ...source('![moon](moon.jpg)'), + canonicalSrc: 'moon.jpg', + src: 'moon.jpg', + alt: 'moon', + }), ), ), ), @@ -971,12 +1010,26 @@ Paragraph source('~[moon](moon.jpg) and [sun](sun.jpg)~'), strike( source('~[moon](moon.jpg) and [sun](sun.jpg)~'), - link({ ...source('[moon](moon.jpg)'), href: 'moon.jpg' }, 'moon'), + link( + { + ...source('[moon](moon.jpg)'), + canonicalSrc: 'moon.jpg', + href: 'moon.jpg', + }, + 'moon', + ), ), strike(source('~[moon](moon.jpg) and [sun](sun.jpg)~'), ' and '), strike( source('~[moon](moon.jpg) and [sun](sun.jpg)~'), - link({ ...source('[sun](sun.jpg)'), href: 'sun.jpg' }, 'sun'), + link( + { + ...source('[sun](sun.jpg)'), + href: 'sun.jpg', + canonicalSrc: 'sun.jpg', + }, + 'sun', + ), ), ), ), @@ -1079,6 +1132,107 @@ _world_. ), ), }, + { + markdown: ` +[GitLab][gitlab-url] + +[gitlab-url]: https://gitlab.com "GitLab" + + `, + expectedDoc: doc( + paragraph( + source('[GitLab][gitlab-url]'), + link( + { + ...source('[GitLab][gitlab-url]'), + href: 'https://gitlab.com', + canonicalSrc: 'gitlab-url', + title: 'GitLab', + isReference: true, + }, + 'GitLab', + ), + ), + referenceDefinition( + { + ...source('[gitlab-url]: https://gitlab.com "GitLab"'), + identifier: 'gitlab-url', + url: 'https://gitlab.com', + title: 'GitLab', + }, + '[gitlab-url]: https://gitlab.com "GitLab"', + ), + ), + }, + { + markdown: ` +![GitLab Logo][gitlab-logo] + +[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo" + + `, + expectedDoc: doc( + paragraph( + source('![GitLab Logo][gitlab-logo]'), + image({ + ...source('![GitLab Logo][gitlab-logo]'), + src: 'https://gitlab.com/gitlab-logo.png', + canonicalSrc: 'gitlab-logo', + alt: 'GitLab Logo', + title: 'GitLab Logo', + isReference: true, + }), + ), + referenceDefinition( + { + ...source('[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"'), + identifier: 'gitlab-logo', + url: 'https://gitlab.com/gitlab-logo.png', + title: 'GitLab Logo', + }, + '[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"', + ), + ), + }, + { + markdown: ` +--- +title: 'layout' +--- + `, + expectedDoc: doc( + frontmatter( + { ...source("---\ntitle: 'layout'\n---"), language: 'yaml' }, + "title: 'layout'", + ), + ), + }, + { + markdown: ` ++++ +title: 'layout' ++++ + `, + expectedDoc: doc( + frontmatter( + { ...source("+++\ntitle: 'layout'\n+++"), language: 'toml' }, + "title: 'layout'", + ), + ), + }, + { + markdown: ` +;;; +{ title: 'layout' } +;;; + `, + expectedDoc: doc( + frontmatter( + { ...source(";;;\n{ title: 'layout' }\n;;;"), language: 'json' }, + "{ title: 'layout' }", + ), + ), + }, ]; const runOnly = examples.find((example) => example.only === true); @@ -1090,7 +1244,7 @@ _world_. const trimmed = markdown.trim(); const document = await deserialize(trimmed); - expect(expectedDoc).not.toBeFalsy(); + expect(expectedDoc).not.toBe(false); expect(document.toJSON()).toEqual(expectedDoc.toJSON()); expect(serialize(document)).toEqual(expectedMarkdown ?? trimmed); }, @@ -1155,4 +1309,72 @@ body { expect(tiptapEditor.getHTML()).toEqual(expectedHtml); }, ); + + describe('attribute sanitization', () => { + // eslint-disable-next-line no-script-url + const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');"; + // eslint-disable-next-line no-script-url + const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');"; + + const docWithImageFactory = (urlInput, urlOutput) => { + const input = `<img src="${urlInput}">`; + + return { + input, + expectedDoc: doc( + paragraph( + source(input), + image({ + ...source(input), + src: urlOutput, + canonicalSrc: urlOutput, + }), + ), + ), + }; + }; + + const docWithLinkFactory = (urlInput, urlOutput) => { + const input = `<a href="${urlInput}">foo</a>`; + + return { + input, + expectedDoc: doc( + paragraph( + source(input), + link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'), + ), + ), + }; + }; + + it.each` + desc | urlInput | urlOutput + ${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null} + ${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null} + ${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null} + ${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null} + ${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null} + ${'protocol-based JS injection: hex encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long hex encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: hex encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null} + ${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null} + ${'protocol-based JS injection: spaces and entities'} | ${" javascript:alert('XSS');"} | ${null} + ${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null} + ${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"} + ${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"} + ${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"} + `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => { + const exampleFactories = [docWithImageFactory, docWithLinkFactory]; + + exampleFactories.forEach(async (exampleFactory) => { + const { input, expectedDoc } = exampleFactory(urlInput, urlOutput); + const document = await deserialize(input); + + expect(document.toJSON()).toEqual(expectedDoc.toJSON()); + }); + }); + }); }); diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js index 116a26cf7d5..4a57c7b1942 100644 --- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js +++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js @@ -16,6 +16,7 @@ import FigureCaption from '~/content_editor/extensions/figure_caption'; import FootnoteDefinition from '~/content_editor/extensions/footnote_definition'; import FootnoteReference from '~/content_editor/extensions/footnote_reference'; import FootnotesSection from '~/content_editor/extensions/footnotes_section'; +import Frontmatter from '~/content_editor/extensions/frontmatter'; import HardBreak from '~/content_editor/extensions/hard_break'; import Heading from '~/content_editor/extensions/heading'; import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; @@ -26,6 +27,7 @@ import Italic from '~/content_editor/extensions/italic'; import Link from '~/content_editor/extensions/link'; import ListItem from '~/content_editor/extensions/list_item'; import OrderedList from '~/content_editor/extensions/ordered_list'; +import ReferenceDefinition from '~/content_editor/extensions/reference_definition'; import Strike from '~/content_editor/extensions/strike'; import Table from '~/content_editor/extensions/table'; import TableCell from '~/content_editor/extensions/table_cell'; @@ -51,6 +53,7 @@ const tiptapEditor = createTestEditor({ FootnoteDefinition, FootnoteReference, FootnotesSection, + Frontmatter, Figure, FigureCaption, HardBreak, @@ -63,6 +66,7 @@ const tiptapEditor = createTestEditor({ Link, ListItem, OrderedList, + ReferenceDefinition, Strike, Table, TableCell, diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 509cda3046c..0e5281be9bf 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -24,6 +24,7 @@ import Link from '~/content_editor/extensions/link'; import ListItem from '~/content_editor/extensions/list_item'; import OrderedList from '~/content_editor/extensions/ordered_list'; import Paragraph from '~/content_editor/extensions/paragraph'; +import ReferenceDefinition from '~/content_editor/extensions/reference_definition'; import Sourcemap from '~/content_editor/extensions/sourcemap'; import Strike from '~/content_editor/extensions/strike'; import Table from '~/content_editor/extensions/table'; @@ -63,6 +64,7 @@ const tiptapEditor = createTestEditor({ Link, ListItem, OrderedList, + ReferenceDefinition, Sourcemap, Strike, Table, @@ -104,6 +106,7 @@ const { listItem, orderedList, paragraph, + referenceDefinition, strike, table, tableCell, @@ -139,6 +142,7 @@ const { listItem: { nodeType: ListItem.name }, orderedList: { nodeType: OrderedList.name }, paragraph: { nodeType: Paragraph.name }, + referenceDefinition: { nodeType: ReferenceDefinition.name }, strike: { markType: Strike.name }, table: { nodeType: Table.name }, tableCell: { nodeType: TableCell.name }, @@ -243,6 +247,37 @@ describe('markdownSerializer', () => { ).toBe('[download file](file.zip "click here to download")'); }); + it('correctly serializes link references', () => { + expect( + serialize( + paragraph( + link( + { + href: 'gitlab-url', + isReference: true, + }, + 'GitLab', + ), + ), + ), + ).toBe('[GitLab][gitlab-url]'); + }); + + it('correctly serializes image references', () => { + expect( + serialize( + paragraph( + image({ + canonicalSrc: 'gitlab-url', + src: 'image.svg', + alt: 'GitLab', + isReference: true, + }), + ), + ), + ).toBe('![GitLab][gitlab-url]'); + }); + it('correctly serializes strikethrough', () => { expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~'); }); @@ -1163,6 +1198,38 @@ Oranges are orange [^1] ); }); + it('correctly serializes reference definition', () => { + expect( + serialize( + referenceDefinition('[gitlab]: https://gitlab.com'), + referenceDefinition('[foobar]: foobar.com'), + ), + ).toBe( + ` +[gitlab]: https://gitlab.com +[foobar]: foobar.com`.trimLeft(), + ); + }); + + it('correctly adds a space between a reference definition and a block content', () => { + expect( + serialize( + paragraph('paragraph'), + referenceDefinition('[gitlab]: https://gitlab.com'), + referenceDefinition('[foobar]: foobar.com'), + heading({ level: 2 }, 'heading'), + ), + ).toBe( + ` +paragraph + +[gitlab]: https://gitlab.com +[foobar]: foobar.com + +## heading`.trimLeft(), + ); + }); + const defaultEditAction = (initialContent) => { tiptapEditor.chain().setContent(initialContent.toJSON()).insertContent(' modified').run(); }; @@ -1177,42 +1244,49 @@ Oranges are orange [^1] }; it.each` - mark | markdown | modifiedMarkdown | editAction - ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction} - ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction} - ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction} - ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction} - ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction} - ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction} - ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction} - ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction} - ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction} - ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction} - ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction} - ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction} - ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction} - ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction} - ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction} - ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction} - ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction} - ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction} - ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction} - ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction} - ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction} - ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction} - ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction} - ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction} - ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction} - ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction} - ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction} - ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction} - ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction} - ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction} + mark | markdown | modifiedMarkdown | editAction + ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction} + ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction} + ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction} + ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction} + ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction} + ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction} + ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction} + ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction} + ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction} + ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction} + ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction} + ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction} + ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction} + ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction} + ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction} + ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction} + ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction} + ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction} + ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction} + ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction} + ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction} + ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction} + ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction} + ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction} + ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction} + ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction} + ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction} + ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction} + ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction} + ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction} `( - 'preserves original $mark syntax when sourceMarkdown is available for $content', + 'preserves original $mark syntax when sourceMarkdown is available for $markdown', async ({ markdown, modifiedMarkdown, editAction }) => { const { document } = await remarkMarkdownDeserializer().deserialize({ schema: tiptapEditor.schema, diff --git a/spec/frontend/content_editor/services/table_of_contents_utils_spec.js b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js new file mode 100644 index 00000000000..7f63c2171c2 --- /dev/null +++ b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js @@ -0,0 +1,96 @@ +import Heading from '~/content_editor/extensions/heading'; +import { toTree, getHeadings } from '~/content_editor/services/table_of_contents_utils'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/services/table_of_content_utils', () => { + describe('toTree', () => { + it('should fills in gaps in heading levels and convert headings to a tree', () => { + expect( + toTree([ + { level: 3, text: '3' }, + { level: 2, text: '2' }, + ]), + ).toEqual([ + expect.objectContaining({ + level: 1, + text: '', + subHeadings: [ + expect.objectContaining({ + level: 2, + text: '', + subHeadings: [expect.objectContaining({ level: 3, text: '3', subHeadings: [] })], + }), + expect.objectContaining({ level: 2, text: '2', subHeadings: [] }), + ], + }), + ]); + }); + }); + + describe('getHeadings', () => { + const tiptapEditor = createTestEditor({ + extensions: [Heading], + }); + + const { + builders: { heading, doc }, + } = createDocBuilder({ + tiptapEditor, + names: { + heading: { nodeType: Heading.name }, + }, + }); + + it('gets all headings as a tree in a tiptap document', () => { + const initialDoc = doc( + heading({ level: 1 }, 'Heading 1'), + heading({ level: 2 }, 'Heading 1.1'), + heading({ level: 3 }, 'Heading 1.1.1'), + heading({ level: 2 }, 'Heading 1.2'), + heading({ level: 3 }, 'Heading 1.2.1'), + heading({ level: 2 }, 'Heading 1.3'), + heading({ level: 2 }, 'Heading 1.4'), + heading({ level: 3 }, 'Heading 1.4.1'), + heading({ level: 1 }, 'Heading 2'), + ); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + expect(getHeadings(tiptapEditor)).toEqual([ + expect.objectContaining({ + level: 1, + text: 'Heading 1', + subHeadings: [ + expect.objectContaining({ + level: 2, + text: 'Heading 1.1', + subHeadings: [ + expect.objectContaining({ level: 3, text: 'Heading 1.1.1', subHeadings: [] }), + ], + }), + expect.objectContaining({ + level: 2, + text: 'Heading 1.2', + subHeadings: [ + expect.objectContaining({ level: 3, text: 'Heading 1.2.1', subHeadings: [] }), + ], + }), + expect.objectContaining({ level: 2, text: 'Heading 1.3', subHeadings: [] }), + expect.objectContaining({ + level: 2, + text: 'Heading 1.4', + subHeadings: [ + expect.objectContaining({ level: 3, text: 'Heading 1.4.1', subHeadings: [] }), + ], + }), + ], + }), + expect.objectContaining({ + level: 1, + text: 'Heading 2', + subHeadings: [], + }), + ]); + }); + }); +}); diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js index 5e1743701e4..e49b553e4b5 100644 --- a/spec/frontend/crm/contact_form_wrapper_spec.js +++ b/spec/frontend/crm/contact_form_wrapper_spec.js @@ -56,8 +56,9 @@ describe('Customer relations contact form wrapper', () => { ${'edit'} | ${'Edit contact'} | ${'Contact has been updated.'} | ${updateContactMutation} | ${contacts[0].id} ${'create'} | ${'New contact'} | ${'Contact has been added.'} | ${createContactMutation} | ${null} `('in $mode mode', ({ mode, title, successMessage, mutation, existingId }) => { + const isEditMode = mode === 'edit'; + beforeEach(() => { - const isEditMode = mode === 'edit'; mountComponent({ isEditMode }); return waitForPromises(); @@ -82,7 +83,7 @@ describe('Customer relations contact form wrapper', () => { }); it('renders correct fields prop', () => { - expect(findContactForm().props('fields')).toEqual([ + const fields = [ { name: 'firstName', label: 'First name', required: true }, { name: 'lastName', label: 'Last name', required: true }, { name: 'email', label: 'Email', required: true }, @@ -98,7 +99,9 @@ describe('Customer relations contact form wrapper', () => { ], }, { name: 'description', label: 'Description' }, - ]); + ]; + if (isEditMode) fields.push({ name: 'active', label: 'Active', required: true, bool: true }); + expect(findContactForm().props('fields')).toEqual(fields); }); it('renders correct title prop', () => { diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js index 3a6989a00f1..7aaaf480c44 100644 --- a/spec/frontend/crm/contacts_root_spec.js +++ b/spec/frontend/crm/contacts_root_spec.js @@ -1,14 +1,16 @@ -import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import ContactsRoot from '~/crm/contacts/components/contacts_root.vue'; import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql'; +import getGroupContactsCountByStateQuery from '~/crm/contacts/components/graphql/get_group_contacts_count_by_state.graphql'; import routes from '~/crm/contacts/routes'; -import { getGroupContactsQueryResponse } from './mock_data'; +import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue'; +import { getGroupContactsQueryResponse, getGroupContactsCountQueryResponse } from './mock_data'; describe('Customer relations contacts root app', () => { Vue.use(VueApollo); @@ -21,24 +23,30 @@ describe('Customer relations contacts root app', () => { const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName }); const findIssuesLinks = () => wrapper.findAllByTestId('issues-link'); const findNewContactButton = () => wrapper.findByTestId('new-contact-button'); - const findError = () => wrapper.findComponent(GlAlert); + const findTable = () => wrapper.findComponent(PaginatedTableWithSearchAndTabs); const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse); + const successCountQueryHandler = jest.fn().mockResolvedValue(getGroupContactsCountQueryResponse); const basePath = '/groups/flightjs/-/crm/contacts'; const mountComponent = ({ queryHandler = successQueryHandler, - mountFunction = shallowMountExtended, + countQueryHandler = successCountQueryHandler, canAdminCrmContact = true, + textQuery = null, } = {}) => { - fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]); - wrapper = mountFunction(ContactsRoot, { + fakeApollo = createMockApollo([ + [getGroupContactsQuery, queryHandler], + [getGroupContactsCountByStateQuery, countQueryHandler], + ]); + wrapper = mountExtended(ContactsRoot, { router, provide: { groupFullPath: 'flightjs', groupId: 26, groupIssuesPath: '/issues', canAdminCrmContact, + textQuery, }, apolloProvider: fakeApollo, }); @@ -58,9 +66,33 @@ describe('Customer relations contacts root app', () => { router = null; }); - it('should render loading spinner', () => { + it('should render table with default props and loading state', () => { mountComponent(); + expect(findTable().props()).toMatchObject({ + items: [], + itemsCount: {}, + pageInfo: {}, + statusTabs: [ + { title: 'Active', status: 'ACTIVE', filters: 'active' }, + { title: 'Inactive', status: 'INACTIVE', filters: 'inactive' }, + { title: 'All', status: 'ALL', filters: 'all' }, + ], + showItems: true, + showErrorMsg: false, + trackViewsOptions: { category: 'Customer Relations', action: 'view_contacts_list' }, + i18n: { + emptyText: 'No contacts found', + issuesButtonLabel: 'View issues', + editButtonLabel: 'Edit', + title: 'Customer relations contacts', + newContact: 'New contact', + errorText: 'Something went wrong. Please try again.', + }, + serverErrorMessage: '', + filterSearchKey: 'contacts', + filterSearchTokens: [], + }); expect(findLoadingIcon().exists()).toBe(true); }); @@ -83,7 +115,7 @@ describe('Customer relations contacts root app', () => { mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); await waitForPromises(); - expect(findError().exists()).toBe(true); + expect(wrapper.text()).toContain('Something went wrong. Please try again.'); }); }); @@ -92,11 +124,11 @@ describe('Customer relations contacts root app', () => { mountComponent(); await waitForPromises(); - expect(findError().exists()).toBe(false); + expect(wrapper.text()).not.toContain('Something went wrong. Please try again.'); }); it('renders correct results', async () => { - mountComponent({ mountFunction: mountExtended }); + mountComponent(); await waitForPromises(); expect(findRowByName(/Marty/i)).toHaveLength(1); @@ -105,7 +137,7 @@ describe('Customer relations contacts root app', () => { const issueLink = findIssuesLinks().at(0); expect(issueLink.exists()).toBe(true); - expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16'); + expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=12'); }); }); }); diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js index d39f0795f5f..f0e9150cada 100644 --- a/spec/frontend/crm/form_spec.js +++ b/spec/frontend/crm/form_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui'; +import { GlAlert, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; @@ -78,6 +78,7 @@ describe('Reusable form component', () => { const findSaveButton = () => wrapper.findByTestId('save-button'); const findForm = () => wrapper.find('form'); const findError = () => wrapper.findComponent(GlAlert); + const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at); const mountComponent = (propsData) => { wrapper = shallowMountExtended(Form, { @@ -92,7 +93,7 @@ describe('Reusable form component', () => { }); }; - const mountContact = ({ propsData } = {}) => { + const mountContact = ({ propsData, extraFields = [] } = {}) => { mountComponent({ fields: [ { name: 'firstName', label: 'First name', required: true }, @@ -108,6 +109,7 @@ describe('Reusable form component', () => { { key: 'gid://gitlab/CustomerRelations::Organization/2', value: 'ABC Corp' }, ], }, + ...extraFields, ], getQuery: { query: getGroupContactsQuery, @@ -136,7 +138,8 @@ describe('Reusable form component', () => { mutation: updateContactMutation, existingId: 'gid://gitlab/CustomerRelations::Contact/12', }; - mountContact({ propsData }); + const extraFields = [{ name: 'active', label: 'Active', required: true, bool: true }]; + mountContact({ propsData, extraFields }); }; const mountOrganization = ({ propsData } = {}) => { @@ -285,18 +288,16 @@ describe('Reusable form component', () => { }); it.each` - index | id | componentName | value - ${0} | ${'firstName'} | ${'GlFormInput'} | ${'Marty'} - ${1} | ${'lastName'} | ${'GlFormInput'} | ${'McFly'} - ${2} | ${'email'} | ${'GlFormInput'} | ${'example@gitlab.com'} - ${4} | ${'description'} | ${'GlFormInput'} | ${undefined} - ${3} | ${'phone'} | ${'GlFormInput'} | ${undefined} - ${5} | ${'organizationId'} | ${'GlFormSelect'} | ${'gid://gitlab/CustomerRelations::Organization/2'} + index | id | component | value + ${0} | ${'firstName'} | ${GlFormInput} | ${'Marty'} + ${1} | ${'lastName'} | ${GlFormInput} | ${'McFly'} + ${2} | ${'email'} | ${GlFormInput} | ${'example@gitlab.com'} + ${4} | ${'description'} | ${GlFormInput} | ${undefined} + ${3} | ${'phone'} | ${GlFormInput} | ${undefined} + ${5} | ${'organizationId'} | ${GlFormSelect} | ${'gid://gitlab/CustomerRelations::Organization/2'} `( - 'should render a $componentName for #$id with the value "$value"', - ({ index, id, componentName, value }) => { - const component = componentName === 'GlFormInput' ? GlFormInput : GlFormSelect; - const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at); + 'should render the correct component for #$id with the value "$value"', + ({ index, id, component, value }) => { const findFormElement = () => findFormGroup(index).find(component); expect(findFormElement().attributes('id')).toBe(id); @@ -304,6 +305,14 @@ describe('Reusable form component', () => { }, ); + it('should render a checked GlFormCheckbox for #active', () => { + const activeCheckboxIndex = 6; + const findFormElement = () => findFormGroup(activeCheckboxIndex).find(GlFormCheckbox); + + expect(findFormElement().attributes('id')).toBe('active'); + expect(findFormElement().attributes('checked')).toBe('true'); + }); + it('should include updated values in update mutation', () => { wrapper.find('#firstName').vm.$emit('input', 'Michael'); wrapper @@ -314,6 +323,7 @@ describe('Reusable form component', () => { expect(handler).toHaveBeenCalledWith('updateContact', { input: { + active: true, description: null, email: 'example@gitlab.com', firstName: 'Michael', diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js index 35bc7fb69b4..a2e2e88ac60 100644 --- a/spec/frontend/crm/mock_data.js +++ b/spec/frontend/crm/mock_data.js @@ -13,6 +13,7 @@ export const getGroupContactsQueryResponse = { email: 'example@gitlab.com', phone: null, description: null, + active: true, organization: { __typename: 'CustomerRelationsOrganization', id: 'gid://gitlab/CustomerRelations::Organization/2', @@ -27,6 +28,7 @@ export const getGroupContactsQueryResponse = { email: null, phone: null, description: null, + active: true, organization: null, }, { @@ -37,9 +39,32 @@ export const getGroupContactsQueryResponse = { email: 'jd@gitlab.com', phone: '+44 44 4444 4444', description: 'Vice President', + active: true, organization: null, }, ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: 'eyJsYXN0X25hbWUiOiJMZWRuZXIiLCJpZCI6IjE3OSJ9', + hasPreviousPage: false, + startCursor: 'eyJsYXN0X25hbWUiOiJCYXJ0b24iLCJpZCI6IjE5MyJ9', + }, + }, + }, + }, +}; + +export const getGroupContactsCountQueryResponse = { + data: { + group: { + __typename: 'Group', + id: 'gid://gitlab/Group/26', + contactStateCounts: { + all: 241, + active: 239, + inactive: 2, + __typename: 'ContactStateCountsType', }, }, }, @@ -58,6 +83,7 @@ export const getGroupOrganizationsQueryResponse = { name: 'Test Inc', defaultRate: 100, description: null, + active: true, }, { __typename: 'CustomerRelationsOrganization', @@ -65,6 +91,7 @@ export const getGroupOrganizationsQueryResponse = { name: 'ABC Company', defaultRate: 110, description: 'VIP', + active: true, }, { __typename: 'CustomerRelationsOrganization', @@ -72,6 +99,7 @@ export const getGroupOrganizationsQueryResponse = { name: 'GitLab', defaultRate: 120, description: null, + active: true, }, ], }, @@ -91,6 +119,7 @@ export const createContactMutationResponse = { phone: null, description: null, organization: null, + active: true, }, errors: [], }, @@ -119,6 +148,7 @@ export const updateContactMutationResponse = { phone: null, description: null, organization: null, + active: true, }, errors: [], }, @@ -143,6 +173,7 @@ export const createOrganizationMutationResponse = { name: 'A', defaultRate: null, description: null, + active: true, }, errors: [], }, @@ -168,6 +199,7 @@ export const updateOrganizationMutationResponse = { name: 'A', defaultRate: null, description: null, + active: true, }, errors: [], }, diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js index 1a5a7c6ca5d..9f26b9157e6 100644 --- a/spec/frontend/crm/organization_form_wrapper_spec.js +++ b/spec/frontend/crm/organization_form_wrapper_spec.js @@ -49,7 +49,7 @@ describe('Customer relations organization form wrapper', () => { mountComponent({ isEditMode: true }); const organizationForm = findOrganizationForm(); - expect(organizationForm.props('fields')).toHaveLength(3); + expect(organizationForm.props('fields')).toHaveLength(4); expect(organizationForm.props('title')).toBe('Edit organization'); expect(organizationForm.props('successMessage')).toBe('Organization has been updated.'); expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation); diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js index 7b1ef71da63..ea3da86c7b2 100644 --- a/spec/frontend/cycle_analytics/base_spec.js +++ b/spec/frontend/cycle_analytics/base_spec.js @@ -11,7 +11,6 @@ import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filter import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants'; import initState from '~/cycle_analytics/store/state'; import { - permissions, transformedProjectStagePathData, selectedStage, issueEvents, @@ -34,7 +33,6 @@ let wrapper; const { id: groupId, path: groupPath } = currentGroup; const defaultState = { - permissions, currentGroup, createdBefore, createdAfter, @@ -240,24 +238,6 @@ describe('Value stream analytics component', () => { }); }); - describe('without enough permissions', () => { - beforeEach(() => { - wrapper = createComponent({ - initialState: { - selectedStage, - permissions: { - ...permissions, - [selectedStage.id]: false, - }, - }, - }); - }); - - it('renders the empty stage with `You need permission.` message', () => { - expect(findEmptyStageTitle()).toBe('You need permission.'); - }); - }); - describe('without a selected stage', () => { beforeEach(() => { wrapper = createComponent({ diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js index 1fe1dbbb75c..02666260cdb 100644 --- a/spec/frontend/cycle_analytics/mock_data.js +++ b/spec/frontend/cycle_analytics/mock_data.js @@ -101,30 +101,12 @@ export const selectedStage = { ...issueStage, value: null, active: false, - isUserAllowed: true, emptyStageText: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', slug: 'issue', }; -export const stats = [issueStage, planStage, codeStage, testStage, reviewStage, stagingStage]; - -export const permissions = { - issue: true, - plan: true, - code: true, - test: true, - review: true, - staging: true, -}; - -export const rawData = { - summary, - stats, - permissions, -}; - export const convertedData = { summary: [ { value: '20', title: 'New Issues' }, diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js index e775e941b4c..94b6de85a5c 100644 --- a/spec/frontend/cycle_analytics/store/actions_spec.js +++ b/spec/frontend/cycle_analytics/store/actions_spec.js @@ -153,6 +153,19 @@ describe('Project Value Stream Analytics actions', () => { }); }); }); + + describe('with no value stream stages available', () => { + it('will return SET_NO_ACCESS_ERROR', () => { + state = { ...state, stages: [] }; + testAction({ + action: actions.setInitialStage, + state, + payload: null, + expectedMutations: [{ type: 'SET_NO_ACCESS_ERROR' }], + expectedActions: [], + }); + }); + }); }); describe('updateStageTablePagination', () => { @@ -170,46 +183,6 @@ describe('Project Value Stream Analytics actions', () => { }); }); - describe('fetchCycleAnalyticsData', () => { - beforeEach(() => { - state = { ...defaultState, endpoints: mockEndpoints }; - mock = new MockAdapter(axios); - mock.onGet(mockRequestPath).reply(httpStatusCodes.OK); - }); - - it(`dispatches the 'setSelectedStage' and 'fetchStageData' actions`, () => - testAction({ - action: actions.fetchCycleAnalyticsData, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_CYCLE_ANALYTICS_DATA' }, - { type: 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS' }, - ], - expectedActions: [], - })); - - describe('with a failing request', () => { - beforeEach(() => { - state = { endpoints: mockEndpoints }; - mock = new MockAdapter(axios); - mock.onGet(mockRequestPath).reply(httpStatusCodes.BAD_REQUEST); - }); - - it(`commits the 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR' mutation`, () => - testAction({ - action: actions.fetchCycleAnalyticsData, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_CYCLE_ANALYTICS_DATA' }, - { type: 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR' }, - ], - expectedActions: [], - })); - }); - }); - describe('fetchStageData', () => { const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/; const headers = { @@ -529,14 +502,13 @@ describe('Project Value Stream Analytics actions', () => { }); describe('fetchValueStreamStageData', () => { - it('will dispatch the fetchCycleAnalyticsData, fetchStageData, fetchStageMedians and fetchStageCountValues actions', () => + it('will dispatch the fetchStageData, fetchStageMedians and fetchStageCountValues actions', () => testAction({ action: actions.fetchValueStreamStageData, state, payload: {}, expectedMutations: [], expectedActions: [ - { type: 'fetchCycleAnalyticsData' }, { type: 'fetchStageData' }, { type: 'fetchStageMedians' }, { type: 'fetchStageCountValues' }, diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js index 2670a390e9c..2e9e5d91471 100644 --- a/spec/frontend/cycle_analytics/store/mutations_spec.js +++ b/spec/frontend/cycle_analytics/store/mutations_spec.js @@ -38,31 +38,24 @@ describe('Project Value Stream Analytics mutations', () => { }); it.each` - mutation | stateKey | value - ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]} - ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]} - ${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]} - ${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]} - ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true} - ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'hasError'} | ${false} - ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${'hasError'} | ${false} - ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'isLoading'} | ${false} - ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'hasError'} | ${true} - ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} - ${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false} - ${types.REQUEST_STAGE_DATA} | ${'hasError'} | ${false} - ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'isLoadingStage'} | ${false} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'hasError'} | ${false} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'hasError'} | ${true} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} - ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} - ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} - ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}} - ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}} + mutation | stateKey | value + ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]} + ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]} + ${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]} + ${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]} + ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} + ${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false} + ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]} + ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'isLoadingStage'} | ${false} + ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'selectedStageEvents'} | ${[]} + ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} + ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]} + ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} + ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} + ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} + ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}} + ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}} + ${types.SET_NO_ACCESS_ERROR} | ${'hasNoAccessError'} | ${true} `('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => { mutations[mutation](state); diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js index e3907fdbe15..cee1eec792d 100644 --- a/spec/frontend/design_management/components/delete_button_spec.js +++ b/spec/frontend/design_management/components/delete_button_spec.js @@ -6,8 +6,8 @@ import BatchDeleteButton from '~/design_management/components/delete_button.vue' describe('Batch delete button component', () => { let wrapper; - const findButton = () => wrapper.find(GlButton); - const findModal = () => wrapper.find(GlModal); + const findButton = () => wrapper.findComponent(GlButton); + const findModal = () => wrapper.findComponent(GlModal); function createComponent({ isDeleting = false } = {}, { slots = {} } = {}) { wrapper = shallowMount(BatchDeleteButton, { diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js index 77935fbde11..2091e1e08dd 100644 --- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js @@ -26,13 +26,13 @@ describe('Design discussions component', () => { const originalGon = window.gon; let wrapper; - const findDesignNotes = () => wrapper.findAll(DesignNote); - const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder); - const findReplyForm = () => wrapper.find(DesignReplyForm); - const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget); + const findDesignNotes = () => wrapper.findAllComponents(DesignNote); + const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder); + const findReplyForm = () => wrapper.findComponent(DesignReplyForm); + const findRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget); const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]'); const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]'); - const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]'); const findApolloMutation = () => wrapper.findComponent(ApolloMutation); @@ -307,7 +307,7 @@ describe('Design discussions component', () => { expect( wrapper - .findAll(DesignNote) + .findAllComponents(DesignNote) .wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')), ).toBe(true); }, @@ -351,7 +351,7 @@ describe('Design discussions component', () => { createComponent(); findReplyPlaceholder().vm.$emit('focus'); - expect(wrapper.emitted('open-form')).toBeTruthy(); + expect(wrapper.emitted('open-form')).toHaveLength(1); }); describe('when user is not logged in', () => { diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js index 1f84fde9f7f..28833b4af5c 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js @@ -100,7 +100,7 @@ describe('Design note component', () => { note, }); - expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true); + expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true); }); it('should not render edit icon when user does not have a permission', () => { diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index d2d1fe6b2d8..f7ce742b933 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -15,9 +15,9 @@ describe('Design reply form component', () => { let wrapper; const findTextarea = () => wrapper.find('textarea'); - const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); - const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); - const findModal = () => wrapper.find({ ref: 'cancelCommentModal' }); + const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' }); + const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' }); + const findModal = () => wrapper.findComponent({ ref: 'cancelCommentModal' }); function createComponent(props = {}, mountOptions = {}) { wrapper = mount(DesignReplyForm, { @@ -42,6 +42,18 @@ describe('Design reply form component', () => { expect(findTextarea().element).toEqual(document.activeElement); }); + it('renders "Attach a file or image" button in markdown toolbar', () => { + createComponent(); + + expect(wrapper.find('[data-testid="button-attach-file"]').exists()).toBe(true); + }); + + it('renders file upload progress container', () => { + createComponent(); + + expect(wrapper.find('.comment-toolbar .uploading-container').exists()).toBe(true); + }); + it('renders button text as "Comment" when creating a comment', () => { createComponent(); diff --git a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js index f87228663b6..41129e2b58d 100644 --- a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js +++ b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js @@ -8,10 +8,10 @@ describe('Toggle replies widget component', () => { let wrapper; const findToggleWrapper = () => wrapper.find('[data-testid="toggle-comments-wrapper"]'); - const findIcon = () => wrapper.find(GlIcon); - const findButton = () => wrapper.find(GlButton); - const findAuthorLink = () => wrapper.find(GlLink); - const findTimeAgo = () => wrapper.find(TimeAgoTooltip); + const findIcon = () => wrapper.findComponent(GlIcon); + const findButton = () => wrapper.findComponent(GlButton); + const findAuthorLink = () => wrapper.findComponent(GlLink); + const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip); function createComponent(props = {}) { wrapper = shallowMount(ToggleRepliesWidget, { diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js index a04e2ebda5b..e1a66cea329 100644 --- a/spec/frontend/design_management/components/design_scaler_spec.js +++ b/spec/frontend/design_management/components/design_scaler_spec.js @@ -6,7 +6,7 @@ import DesignScaler from '~/design_management/components/design_scaler.vue'; describe('Design management design scaler component', () => { let wrapper; - const getButtons = () => wrapper.findAll(GlButton); + const getButtons = () => wrapper.findAllComponents(GlButton); const getDecreaseScaleButton = () => getButtons().at(0); const getResetScaleButton = () => getButtons().at(1); const getIncreaseScaleButton = () => getButtons().at(2); diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js index f13796138bd..af995f75ddc 100644 --- a/spec/frontend/design_management/components/design_sidebar_spec.js +++ b/spec/frontend/design_management/components/design_sidebar_spec.js @@ -32,12 +32,12 @@ describe('Design management design sidebar component', () => { const originalGon = window.gon; let wrapper; - const findDiscussions = () => wrapper.findAll(DesignDiscussion); + const findDiscussions = () => wrapper.findAllComponents(DesignDiscussion); const findFirstDiscussion = () => findDiscussions().at(0); const findUnresolvedDiscussions = () => wrapper.findAll('[data-testid="unresolved-discussion"]'); const findResolvedDiscussions = () => wrapper.findAll('[data-testid="resolved-discussion"]'); - const findParticipants = () => wrapper.find(Participants); - const findResolvedCommentsToggle = () => wrapper.find(GlAccordionItem); + const findParticipants = () => wrapper.findComponent(Participants); + const findResolvedCommentsToggle = () => wrapper.findComponent(GlAccordionItem); const findNewDiscussionDisclaimer = () => wrapper.find('[data-testid="new-discussion-disclaimer"]'); @@ -87,7 +87,7 @@ describe('Design management design sidebar component', () => { it('renders To-Do button', () => { createComponent(); - expect(wrapper.find(DesignTodoButton).exists()).toBe(true); + expect(wrapper.findComponent(DesignTodoButton).exists()).toBe(true); }); describe('when has no discussions', () => { diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js index 73661c9fcb0..b3afcefe1ed 100644 --- a/spec/frontend/design_management/components/design_todo_button_spec.js +++ b/spec/frontend/design_management/components/design_todo_button_spec.js @@ -57,7 +57,7 @@ describe('Design management design todo button', () => { }); it('renders TodoButton component', () => { - expect(wrapper.find(TodoButton).exists()).toBe(true); + expect(wrapper.findComponent(TodoButton).exists()).toBe(true); }); describe('when design has a pending todo', () => { diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js index 65ee0ae6238..8163cb0d87a 100644 --- a/spec/frontend/design_management/components/image_spec.js +++ b/spec/frontend/design_management/components/image_spec.js @@ -71,7 +71,7 @@ describe('Design management large image component', () => { image.trigger('error'); await nextTick(); expect(image.isVisible()).toBe(false); - expect(wrapper.find(GlIcon).element).toMatchSnapshot(); + expect(wrapper.findComponent(GlIcon).element).toMatchSnapshot(); }); describe('zoom', () => { diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js index e00dda2015e..66d3f883960 100644 --- a/spec/frontend/design_management/components/list/item_spec.js +++ b/spec/frontend/design_management/components/list/item_spec.js @@ -23,8 +23,8 @@ describe('Design management list item component', () => { const findDesignEvent = () => wrapper.findByTestId('design-event'); const findImgFilename = (id = imgId) => wrapper.findByTestId(`design-img-filename-${id}`); - const findEventIcon = () => findDesignEvent().find(GlIcon); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findEventIcon = () => findDesignEvent().findComponent(GlIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); function createComponent({ notesCount = 0, @@ -74,7 +74,7 @@ describe('Design management list item component', () => { beforeEach(async () => { createComponent(); image = wrapper.find('img'); - glIntersectionObserver = wrapper.find(GlIntersectionObserver); + glIntersectionObserver = wrapper.findComponent(GlIntersectionObserver); glIntersectionObserver.vm.$emit('appear'); await nextTick(); @@ -86,7 +86,7 @@ describe('Design management list item component', () => { describe('before image is loaded', () => { it('renders loading spinner', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); @@ -105,7 +105,7 @@ describe('Design management list item component', () => { image.trigger('error'); await nextTick(); expect(image.isVisible()).toBe(false); - expect(wrapper.find(GlIcon).element).toMatchSnapshot(); + expect(wrapper.findComponent(GlIcon).element).toMatchSnapshot(); }); describe('when imageV432x230 and image provided', () => { diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js index 412f3de911e..b6137ba2eee 100644 --- a/spec/frontend/design_management/components/toolbar/index_spec.js +++ b/spec/frontend/design_management/components/toolbar/index_spec.js @@ -85,35 +85,35 @@ describe('Design management toolbar component', () => { createComponent(); await nextTick(); - expect(wrapper.find(DeleteButton).exists()).toBe(true); + expect(wrapper.findComponent(DeleteButton).exists()).toBe(true); }); it('does not render delete button on non-latest version', async () => { createComponent(false, true, { isLatestVersion: false }); await nextTick(); - expect(wrapper.find(DeleteButton).exists()).toBe(false); + expect(wrapper.findComponent(DeleteButton).exists()).toBe(false); }); it('does not render delete button when user is not logged in', async () => { createComponent(false, false); await nextTick(); - expect(wrapper.find(DeleteButton).exists()).toBe(false); + expect(wrapper.findComponent(DeleteButton).exists()).toBe(false); }); it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => { createComponent(); await nextTick(); - wrapper.find(DeleteButton).vm.$emit('delete-selected-designs'); + wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs'); expect(wrapper.emitted().delete).toBeTruthy(); }); it('renders download button with correct link', () => { createComponent(); - expect(wrapper.find(GlButton).attributes('href')).toBe( + expect(wrapper.findComponent(GlButton).attributes('href')).toBe( '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d', ); }); diff --git a/spec/frontend/design_management/components/upload/button_spec.js b/spec/frontend/design_management/components/upload/button_spec.js index d123db43ce6..59821218ab8 100644 --- a/spec/frontend/design_management/components/upload/button_spec.js +++ b/spec/frontend/design_management/components/upload/button_spec.js @@ -34,7 +34,7 @@ describe('Design management upload button component', () => { it('Button `loading` prop is `true`', () => { createComponent({ isSaving: true }); - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); expect(button.exists()).toBe(true); expect(button.props('loading')).toBe(true); }); diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js index ec5db04bb80..7c26ab9739b 100644 --- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js +++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js @@ -46,7 +46,7 @@ describe('Design management design version dropdown component', () => { wrapper.destroy(); }); - const findVersionLink = (index) => wrapper.findAll(GlDropdownItem).at(index); + const findVersionLink = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); it('renders design version dropdown button', async () => { createComponent(); @@ -76,35 +76,35 @@ describe('Design management design version dropdown component', () => { createComponent(); await nextTick(); - expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); + expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version'); }); it('displays latest version text when only 1 version is present', async () => { createComponent({ maxVersions: 1 }); await nextTick(); - expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); + expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version'); }); it('displays version text when the current version is not the latest', async () => { createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) }); await nextTick(); - expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`); + expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(`Showing version #1`); }); it('displays latest version text when the current version is the latest', async () => { createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) }); await nextTick(); - expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); + expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version'); }); it('should have the same length as apollo query', async () => { createComponent(); await nextTick(); - expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length); + expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length); }); it('should render TimeAgo', async () => { diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js index 17a299c5de1..774e37a8b21 100644 --- a/spec/frontend/design_management/pages/design/index_spec.js +++ b/spec/frontend/design_management/pages/design/index_spec.js @@ -85,9 +85,9 @@ describe('Design management design index page', () => { let wrapper; let router; - const findDiscussionForm = () => wrapper.find(DesignReplyForm); - const findSidebar = () => wrapper.find(DesignSidebar); - const findDesignPresentation = () => wrapper.find(DesignPresentation); + const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm); + const findSidebar = () => wrapper.findComponent(DesignSidebar); + const findDesignPresentation = () => wrapper.findComponent(DesignPresentation); function createComponent( { loading = false } = {}, @@ -181,15 +181,15 @@ describe('Design management design index page', () => { it('sets loading state', () => { createComponent({ loading: true }); - expect(wrapper.find(DesignPresentation).props('isLoading')).toBe(true); - expect(wrapper.find(DesignSidebar).props('isLoading')).toBe(true); + expect(wrapper.findComponent(DesignPresentation).props('isLoading')).toBe(true); + expect(wrapper.findComponent(DesignSidebar).props('isLoading')).toBe(true); }); it('renders design index', () => { createComponent({ loading: false }, { data: { design } }); expect(wrapper.element).toMatchSnapshot(); - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); it('passes correct props to sidebar component', () => { diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index 21be7bd148b..f90feaadfb0 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -111,8 +111,8 @@ describe('Design management index page', () => { const findDropzoneWrapper = () => wrapper.findByTestId('design-dropzone-wrapper'); const findFirstDropzoneWithDesign = () => wrapper.findAllComponents(DesignDropzone).at(1); const findDesignsWrapper = () => wrapper.findByTestId('designs-root'); - const findDesigns = () => wrapper.findAll(Design); - const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs; + const findDesigns = () => wrapper.findAllComponents(Design); + const draggableAttributes = () => wrapper.findComponent(VueDraggable).vm.$attrs; const findDesignUploadButton = () => wrapper.findByTestId('design-upload-button'); const findDesignToolbarWrapper = () => wrapper.findByTestId('design-toolbar-wrapper'); const findDesignUpdateAlert = () => wrapper.findByTestId('design-update-alert'); @@ -120,8 +120,8 @@ describe('Design management index page', () => { async function moveDesigns(localWrapper) { await waitForPromises(); - localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns); - localWrapper.find(VueDraggable).vm.$emit('change', { + localWrapper.findComponent(VueDraggable).vm.$emit('input', reorderedDesigns); + localWrapper.findComponent(VueDraggable).vm.$emit('change', { moved: { newIndex: 0, element: designToMove, @@ -369,7 +369,7 @@ describe('Design management index page', () => { findDropzone().vm.$emit('change', [{ name: 'test' }]); expect(mutate).toHaveBeenCalledWith(mutationVariables); expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]); - expect(wrapper.vm.isSaving).toBeTruthy(); + expect(wrapper.vm.isSaving).toBe(true); expect(dropzoneClasses()).toContain('design-list-item'); expect(dropzoneClasses()).toContain('design-list-item-new'); }); @@ -399,7 +399,7 @@ describe('Design management index page', () => { await nextTick(); expect(wrapper.vm.filesToBeSaved).toEqual([]); - expect(wrapper.vm.isSaving).toBeFalsy(); + expect(wrapper.vm.isSaving).toBe(false); expect(wrapper.vm.isLatestVersion).toBe(true); }); @@ -412,7 +412,7 @@ describe('Design management index page', () => { wrapper.vm.onUploadDesignError(); await nextTick(); expect(wrapper.vm.filesToBeSaved).toEqual([]); - expect(wrapper.vm.isSaving).toBeFalsy(); + expect(wrapper.vm.isSaving).toBe(false); expect(findDesignUpdateAlert().exists()).toBe(true); expect(findDesignUpdateAlert().text()).toBe(UPLOAD_DESIGN_ERROR); }); diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js index b9c62334223..b9edde559c8 100644 --- a/spec/frontend/design_management/router_spec.js +++ b/spec/frontend/design_management/router_spec.js @@ -44,7 +44,7 @@ describe('Design management router', () => { it('pushes home component', () => { const wrapper = factory(routeArg); - expect(wrapper.find(Designs).exists()).toBe(true); + expect(wrapper.findComponent(Designs).exists()).toBe(true); }); }); @@ -55,7 +55,7 @@ describe('Design management router', () => { const wrapper = factory(routeArg); return nextTick().then(() => { - const detail = wrapper.find(DesignDetail); + const detail = wrapper.findComponent(DesignDetail); expect(detail.exists()).toBe(true); expect(detail.props('id')).toEqual('1'); }); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index d90afeb6b82..92b8b2d4aa3 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -263,7 +263,7 @@ describe('DiffFileHeader component', () => { }, }, }); - expect(findModeChangedLine().exists()).toBeFalsy(); + expect(findModeChangedLine().exists()).toBe(false); }, ); diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js index be81508213b..a74013dc2d4 100644 --- a/spec/frontend/diffs/components/diff_row_spec.js +++ b/spec/frontend/diffs/components/diff_row_spec.js @@ -239,7 +239,7 @@ describe('DiffRow', () => { const coverage = wrapper.find('.line-coverage.right-side'); expect(coverage.attributes('title')).toContain('Test coverage: 5 hits'); - expect(coverage.classes('coverage')).toBeTruthy(); + expect(coverage.classes('coverage')).toBe(true); }); it('for lines without coverage', () => { @@ -248,7 +248,7 @@ describe('DiffRow', () => { const coverage = wrapper.find('.line-coverage.right-side'); expect(coverage.attributes('title')).toContain('No test coverage'); - expect(coverage.classes('no-coverage')).toBeTruthy(); + expect(coverage.classes('no-coverage')).toBe(true); }); it('for unknown lines', () => { @@ -256,9 +256,9 @@ describe('DiffRow', () => { wrapper = createWrapper({ props, state: { coverageFiles } }); const coverage = wrapper.find('.line-coverage.right-side'); - expect(coverage.attributes('title')).toBeFalsy(); - expect(coverage.classes('coverage')).toBeFalsy(); - expect(coverage.classes('no-coverage')).toBeFalsy(); + expect(coverage.attributes('title')).toBeUndefined(); + expect(coverage.classes('coverage')).toBe(false); + expect(coverage.classes('no-coverage')).toBe(false); }); }); diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index 8852c6c62c5..3f870a98396 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -424,8 +424,8 @@ describe('DiffsStoreUtils', () => { expect(firstChar).not.toBe('+'); expect(firstChar).not.toBe('-'); - expect(preparedDiff.diff_files[0].renderIt).toBeTruthy(); - expect(preparedDiff.diff_files[0].collapsed).toBeFalsy(); + expect(preparedDiff.diff_files[0].renderIt).toBe(true); + expect(preparedDiff.diff_files[0].collapsed).toBe(false); }); it('guarantees an empty array for both diff styles', () => { @@ -506,8 +506,8 @@ describe('DiffsStoreUtils', () => { }); it('sets the renderIt and collapsed attribute on files', () => { - expect(preparedDiffFiles[0].renderIt).toBeTruthy(); - expect(preparedDiffFiles[0].collapsed).toBeFalsy(); + expect(preparedDiffFiles[0].renderIt).toBe(true); + expect(preparedDiffFiles[0].collapsed).toBeUndefined(); }); it('guarantees an empty array of lines for both diff styles', () => { diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js index a633de9ef56..0fe70bac6b7 100644 --- a/spec/frontend/dropzone_input_spec.js +++ b/spec/frontend/dropzone_input_spec.js @@ -29,7 +29,9 @@ describe('dropzone_input', () => { it('returns valid dropzone when successfully initialize', () => { const dropzone = dropzoneInput($(TEMPLATE)); - expect(dropzone.version).toBeTruthy(); + expect(dropzone).toMatchObject({ + version: expect.any(String), + }); }); describe('handlePaste', () => { diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index c59806a5d60..c9010fbec0c 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -2,7 +2,7 @@ import Ajv from 'ajv'; import AjvFormats from 'ajv-formats'; import CiSchema from '~/editor/schema/ci.json'; -// JSON POSITIVE TESTS +// JSON POSITIVE TESTS (LEGACY) import AllowFailureJson from './json_tests/positive_tests/allow_failure.json'; import EnvironmentJson from './json_tests/positive_tests/environment.json'; import GitlabCiDependenciesJson from './json_tests/positive_tests/gitlab-ci-dependencies.json'; @@ -14,7 +14,7 @@ import TerraformReportJson from './json_tests/positive_tests/terraform_report.js import VariablesMixStringAndUserInputJson from './json_tests/positive_tests/variables_mix_string_and_user_input.json'; import VariablesJson from './json_tests/positive_tests/variables.json'; -// JSON NEGATIVE TESTS +// JSON NEGATIVE TESTS (LEGACY) import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json'; import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json'; import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json'; @@ -24,14 +24,17 @@ import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_a import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json'; // YAML POSITIVE TEST +import ArtifactsYaml from './yaml_tests/positive_tests/artifacts.yml'; import CacheYaml from './yaml_tests/positive_tests/cache.yml'; import FilterYaml from './yaml_tests/positive_tests/filter.yml'; import IncludeYaml from './yaml_tests/positive_tests/include.yml'; import RulesYaml from './yaml_tests/positive_tests/rules.yml'; // YAML NEGATIVE TEST +import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml'; import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml'; import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml'; +import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml'; const ajv = new Ajv({ strictTypes: false, @@ -59,6 +62,7 @@ describe('positive tests', () => { VariablesJson, // YAML + ArtifactsYaml, CacheYaml, FilterYaml, IncludeYaml, @@ -82,8 +86,10 @@ describe('negative tests', () => { RetryUnknownWhenJson, // YAML + ArtifactsNegativeYaml, CacheNegativeYaml, IncludeNegativeYaml, + RulesNegativeYaml, }), )('schema validates %s', (_, input) => { expect(input).not.toValidateJsonSchema(schema); diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml new file mode 100644 index 00000000000..f5670376efc --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml @@ -0,0 +1,18 @@ +# invalid artifact:reports:cyclonedx + +cyclonedx no paths: + artifacts: + reports: + cyclonedx: + +cyclonedx not a report: + artifacts: + cyclonedx: foo + +cyclonedx not an array or string: + artifacts: + reports: + cyclonedx: + paths: + - foo + - bar diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml index ee533f54d3b..04020c06753 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml @@ -1,15 +1,13 @@ -# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779 stages: - prepare -# invalid cache:when value -job1: +# invalid cache:when values +when no integer: stage: prepare cache: when: 0 -# invalid cache:when value -job2: +when must be a reserved word: stage: prepare cache: when: 'never' diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml index 287150a765f..1e16bb55405 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml @@ -1,16 +1,14 @@ -# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779 stages: - prepare -# missing file property -childPipeline: +# invalid trigger:include +trigger missing file property: stage: prepare trigger: include: - project: 'my-group/my-pipeline-library' -# missing project property -childPipeline2: +trigger missing project property: stage: prepare trigger: include: diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml new file mode 100644 index 00000000000..d74a681b23b --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml @@ -0,0 +1,14 @@ +# invalid rules:changes +unnecessary ref declaration: + script: exit 0 + rules: + - changes: + paths: + - README.md + compare_to: { ref: 'main' } + +wrong path declaration: + script: exit 0 + rules: + - changes: + paths: { file: 'DOCKER' } diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml new file mode 100644 index 00000000000..20c1fc2c50f --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml @@ -0,0 +1,25 @@ +# valid artifact:reports:cyclonedx + +cyclonedx string path: + artifacts: + reports: + cyclonedx: foo + +cyclonedx glob path: + artifacts: + reports: + cyclonedx: "*.foo" + +cylonedx list of string paths: + artifacts: + reports: + cyclonedx: + - foo + - ./bar/baz + +cylonedx mixed list of string paths and globs: + artifacts: + reports: + cyclonedx: + - ./foo + - "bar/*.baz" diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml index 436c7d72699..d83e14fdc6a 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml @@ -1,8 +1,7 @@ -# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779 stages: - prepare -# test for cache:when values +# valid cache:when values job1: stage: prepare script: diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml index 2b29c24fa3c..f82ea71dcf3 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml @@ -1,5 +1,5 @@ -# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79335 -deploy-template: +# valid only/except values +only and except as array of strings: script: - echo "hello world" only: @@ -7,12 +7,10 @@ deploy-template: except: - bar -# null value allowed -deploy-without-only: +only as null value: extends: deploy-template only: -# null value allowed -deploy-without-except: +except as null value: extends: deploy-template except: diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml index 3497be28058..c00ab0d464a 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml @@ -1,17 +1,15 @@ -# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779 +stages: + - prepare -# test for include:rules +# valid include:rules include: - local: builds.yml rules: - if: '$INCLUDE_BUILDS == "true"' when: always -stages: - - prepare - -# test for trigger:include -childPipeline: +# valid trigger:include +trigger:include accepts project and file properties: stage: prepare script: - echo 'creating pipeline...' @@ -20,8 +18,7 @@ childPipeline: - project: 'my-group/my-pipeline-library' file: '.gitlab-ci.yml' -# accepts optional ref property -childPipeline2: +trigger:include accepts optional ref property: stage: prepare script: - echo 'creating pipeline...' diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml index 27a199cff13..37cae6b4264 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml @@ -1,13 +1,28 @@ -# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74164 +# valid workflow:rules:changes +rules:changes with paths and compare_to properties: + script: exit 0 + rules: + - changes: + paths: + - README.md + compare_to: main + +rules:changes as array of strings: + script: exit 0 + rules: + - changes: + - README.md -# test for workflow:rules:changes and workflow:rules:exists +# valid workflow:rules:exists +# valid rules:changes:path workflow: rules: + - changes: + paths: + - README.md - if: '$CI_PIPELINE_SOURCE == "schedule"' exists: - Dockerfile - changes: - - Dockerfile variables: IS_A_FEATURE: 'true' when: always diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js index 99c4ff4f3fa..1223fee320e 100644 --- a/spec/frontend/editor/source_editor_instance_spec.js +++ b/spec/frontend/editor/source_editor_instance_spec.js @@ -423,7 +423,7 @@ describe('Source Editor Instance', () => { 'changes language of an attached model to "$expectedLanguage" when filepath is "$path"', ({ path, expectedLanguage }) => { seInstance.updateModelLanguage(path); - expect(instanceModel.getLanguageIdentifier().language).toBe(expectedLanguage); + expect(instanceModel.getLanguageId()).toBe(expectedLanguage); }, ); }); diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js index 74aae7b899b..6a8e7b296aa 100644 --- a/spec/frontend/editor/source_editor_spec.js +++ b/spec/frontend/editor/source_editor_spec.js @@ -267,7 +267,6 @@ describe('Base editor', () => { let editorEl2; let inst1; let inst2; - const readOnlyIndex = '78'; // readOnly option has the internal index of 78 in the editor's options beforeEach(() => { setHTMLFixture('<div id="editor1"></div><div id="editor2"></div>'); @@ -331,10 +330,10 @@ describe('Base editor', () => { }); inst1 = editor.createInstance(inst1Args); - expect(inst1.getOption(readOnlyIndex)).toBe(true); + expect(inst1.getRawOptions().readOnly).toBe(true); inst2 = editor.createInstance(inst2Args); - expect(inst2.getOption(readOnlyIndex)).toBe(true); + expect(inst2.getRawOptions().readOnly).toBe(true); }); it('allows overriding editor options on the instance level', () => { @@ -346,7 +345,7 @@ describe('Base editor', () => { readOnly: false, }); - expect(inst1.getOption(readOnlyIndex)).toBe(false); + expect(inst1.getRawOptions().readOnly).toBe(false); }); it('disposes instances and relevant models independently from each other', () => { diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index dc1c1dfbe4a..1c84350bd8e 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -70,7 +70,6 @@ class CustomEnvironment extends JSDOMEnvironment { // // Monaco-related environment variables // - this.global.MonacoEnvironment = { globalAPI: true }; Object.defineProperty(this.global, 'matchMedia', { writable: true, value: (query) => ({ diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js index d58f9f9b8a2..340740e6499 100644 --- a/spec/frontend/environments/canary_ingress_spec.js +++ b/spec/frontend/environments/canary_ingress_spec.js @@ -10,7 +10,7 @@ describe('/environments/components/canary_ingress.vue', () => { const setWeightTo = (weightWrapper, x) => weightWrapper - .findAll(GlDropdownItem) + .findAllComponents(GlDropdownItem) .at(x / 5) .vm.$emit('click'); @@ -59,14 +59,14 @@ describe('/environments/components/canary_ingress.vue', () => { }); it('lists options from 0 to 100 in increments of 5', () => { - const options = stableWeightDropdown.findAll(GlDropdownItem); + const options = stableWeightDropdown.findAllComponents(GlDropdownItem); expect(options).toHaveLength(21); options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString())); }); it('is set to open the change modal', () => { stableWeightDropdown - .findAll(GlDropdownItem) + .findAllComponents(GlDropdownItem) .wrappers.forEach((w) => expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }), ); @@ -92,13 +92,13 @@ describe('/environments/components/canary_ingress.vue', () => { it('lists options from 0 to 100 in increments of 5', () => { canaryWeightDropdown - .findAll(GlDropdownItem) + .findAllComponents(GlDropdownItem) .wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString())); }); it('is set to open the change modal', () => { canaryWeightDropdown - .findAll(GlDropdownItem) + .findAllComponents(GlDropdownItem) .wrappers.forEach((w) => expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }), ); diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js index 16792dcda1e..31b1770da59 100644 --- a/spec/frontend/environments/canary_update_modal_spec.js +++ b/spec/frontend/environments/canary_update_modal_spec.js @@ -10,7 +10,7 @@ describe('/environments/components/canary_update_modal.vue', () => { let modal; let mutate; - const findAlert = () => wrapper.find(GlAlert); + const findAlert = () => wrapper.findComponent(GlAlert); const createComponent = () => { mutate = jest.fn().mockResolvedValue(); @@ -27,7 +27,7 @@ describe('/environments/components/canary_update_modal.vue', () => { $apollo: { mutate }, }, }); - modal = wrapper.find(GlModal); + modal = wrapper.findComponent(GlModal); }; afterEach(() => { diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js index c4763933468..2163814528a 100644 --- a/spec/frontend/environments/confirm_rollback_modal_spec.js +++ b/spec/frontend/environments/confirm_rollback_modal_spec.js @@ -73,7 +73,7 @@ describe('Confirm Rollback Modal Component', () => { hasMultipleCommits, retryUrl, }); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); expect(modal.attributes('title')).toContain('Rollback'); expect(modal.attributes('title')).toContain('test'); @@ -92,7 +92,7 @@ describe('Confirm Rollback Modal Component', () => { hasMultipleCommits, }); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); expect(modal.attributes('title')).toContain('Re-deploy'); expect(modal.attributes('title')).toContain('test'); @@ -110,7 +110,7 @@ describe('Confirm Rollback Modal Component', () => { }); const eventHubSpy = jest.spyOn(eventHub, '$emit'); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); modal.vm.$emit('ok'); expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', env); @@ -155,7 +155,7 @@ describe('Confirm Rollback Modal Component', () => { }, { apolloProvider }, ); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); expect(trimText(modal.text())).toContain('commit abc0123'); expect(modal.text()).toContain('Are you sure you want to continue?'); @@ -177,7 +177,7 @@ describe('Confirm Rollback Modal Component', () => { }, { apolloProvider }, ); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); expect(modal.attributes('title')).toContain('Rollback'); expect(modal.attributes('title')).toContain('test'); @@ -201,7 +201,7 @@ describe('Confirm Rollback Modal Component', () => { { apolloProvider }, ); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); expect(modal.attributes('title')).toContain('Re-deploy'); expect(modal.attributes('title')).toContain('test'); @@ -220,7 +220,7 @@ describe('Confirm Rollback Modal Component', () => { { apolloProvider }, ); - const modal = component.find(GlModal); + const modal = component.findComponent(GlModal); modal.vm.$emit('ok'); await nextTick(); diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js index 4d63648dd48..c005ca22070 100644 --- a/spec/frontend/environments/deploy_board_component_spec.js +++ b/spec/frontend/environments/deploy_board_component_spec.js @@ -26,7 +26,9 @@ describe('Deploy Board', () => { }); it('should render percentage with completion value provided', () => { - expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${deployBoardMockData.completion}%`); + expect(wrapper.findComponent({ ref: 'percentage' }).text()).toBe( + `${deployBoardMockData.completion}%`, + ); }); it('should render total instance count', () => { @@ -79,7 +81,9 @@ describe('Deploy Board', () => { }); it('should render percentage with completion value provided', () => { - expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${rolloutStatus.completion}%`); + expect(wrapper.findComponent({ ref: 'percentage' }).text()).toBe( + `${rolloutStatus.completion}%`, + ); }); it('should render total instance count', () => { diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js index 2c8c054ccbd..0f2d6e95bf0 100644 --- a/spec/frontend/environments/edit_environment_spec.js +++ b/spec/frontend/environments/edit_environment_spec.js @@ -42,7 +42,7 @@ describe('~/environments/components/edit.vue', () => { const findExternalUrlInput = () => wrapper.findByLabelText('External URL'); const findForm = () => wrapper.findByRole('form', { name: 'Edit environment' }); - const showsLoading = () => wrapper.find(GlLoadingIcon).exists(); + const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists(); const submitForm = async (expected, response) => { mock diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js index ada79e2d415..68895b194a1 100644 --- a/spec/frontend/environments/environment_actions_spec.js +++ b/spec/frontend/environments/environment_actions_spec.js @@ -51,7 +51,7 @@ describe('EnvironmentActions Component', () => { } const findDropdownItem = (action) => { - const buttons = wrapper.findAll(GlDropdownItem); + const buttons = wrapper.findAllComponents(GlDropdownItem); return buttons.filter((button) => button.text().startsWith(action.name)).at(0); }; @@ -62,12 +62,12 @@ describe('EnvironmentActions Component', () => { it('should render a dropdown button with 2 icons', () => { createComponent({}, { mountFn: mount }); - expect(wrapper.find(GlDropdown).findAll(GlIcon).length).toBe(2); + expect(wrapper.findComponent(GlDropdown).findAllComponents(GlIcon).length).toBe(2); }); it('should render a dropdown button with aria-label description', () => { createComponent(); - expect(wrapper.find(GlDropdown).attributes('aria-label')).toBe('Deploy to...'); + expect(wrapper.findComponent(GlDropdown).attributes('aria-label')).toBe('Deploy to...'); }); it('should render a tooltip', () => { @@ -98,11 +98,11 @@ describe('EnvironmentActions Component', () => { }); it('should render a dropdown with the provided list of actions', () => { - expect(wrapper.findAll(GlDropdownItem)).toHaveLength(actions.length); + expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(actions.length); }); it("should render a disabled action when it's not playable", () => { - const dropdownItems = wrapper.findAll(GlDropdownItem); + const dropdownItems = wrapper.findAllComponents(GlDropdownItem); const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1); expect(lastDropdownItem.attributes('disabled')).toBe('true'); }); @@ -136,7 +136,7 @@ describe('EnvironmentActions Component', () => { }); it('should render a dropdown button with a loading icon', () => { - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true); }); }); diff --git a/spec/frontend/environments/environment_delete_spec.js b/spec/frontend/environments/environment_delete_spec.js index 057cb9858c4..530f9f55088 100644 --- a/spec/frontend/environments/environment_delete_spec.js +++ b/spec/frontend/environments/environment_delete_spec.js @@ -21,7 +21,7 @@ describe('External URL Component', () => { }); }; - const findDropdownItem = () => wrapper.find(GlDropdownItem); + const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); describe('event hub', () => { beforeEach(() => { diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js index 1c86a66d9b8..dd909cf4473 100644 --- a/spec/frontend/environments/environment_item_spec.js +++ b/spec/frontend/environments/environment_item_spec.js @@ -88,11 +88,11 @@ describe('Environment item', () => { it('should render user avatar with link to profile', () => { const avatarLink = findLastDeploymentAvatarLink(); const avatar = findLastDeploymentAvatar(); - const { username, avatar_url, web_url } = environment.last_deployment.user; + const { username, avatar_url: src, web_url } = environment.last_deployment.user; expect(avatarLink.attributes('href')).toBe(web_url); expect(avatar.props()).toMatchObject({ - src: avatar_url, + src, entityName: username, }); expect(avatar.attributes()).toMatchObject({ @@ -127,12 +127,12 @@ describe('Environment item', () => { it('should render the build ID and user', () => { const avatarLink = findUpcomingDeploymentAvatarLink(); const avatar = findUpcomingDeploymentAvatar(); - const { username, avatar_url, web_url } = environment.upcoming_deployment.user; + const { username, avatar_url: src, web_url } = environment.upcoming_deployment.user; expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by'); expect(avatarLink.attributes('href')).toBe(web_url); expect(avatar.props()).toMatchObject({ - src: avatar_url, + src, entityName: username, }); }); @@ -166,12 +166,12 @@ describe('Environment item', () => { it('should still render the build ID and user avatar', () => { const avatarLink = findUpcomingDeploymentAvatarLink(); const avatar = findUpcomingDeploymentAvatar(); - const { username, avatar_url, web_url } = environment.upcoming_deployment.user; + const { username, avatar_url: src, web_url } = environment.upcoming_deployment.user; expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by'); expect(avatarLink.attributes('href')).toBe(web_url); expect(avatar.props()).toMatchObject({ - src: avatar_url, + src, entityName: username, }); }); diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js index 669c974ea4f..170036b5b00 100644 --- a/spec/frontend/environments/environment_pin_spec.js +++ b/spec/frontend/environments/environment_pin_spec.js @@ -41,7 +41,7 @@ describe('Pin Component', () => { it('should emit onPinClick when clicked', () => { const eventHubSpy = jest.spyOn(eventHub, '$emit'); - const item = wrapper.find(GlDropdownItem); + const item = wrapper.findComponent(GlDropdownItem); item.vm.$emit('click'); @@ -74,7 +74,7 @@ describe('Pin Component', () => { it('should emit onPinClick when clicked', () => { jest.spyOn(mockApollo.defaultClient, 'mutate'); - const item = wrapper.find(GlDropdownItem); + const item = wrapper.findComponent(GlDropdownItem); item.vm.$emit('click'); diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js index 7eff46baaf7..be61c6fcc90 100644 --- a/spec/frontend/environments/environment_rollback_spec.js +++ b/spec/frontend/environments/environment_rollback_spec.js @@ -44,7 +44,7 @@ describe('Rollback Component', () => { }, }, }); - const button = wrapper.find(GlDropdownItem); + const button = wrapper.findComponent(GlDropdownItem); button.vm.$emit('click'); @@ -71,7 +71,7 @@ describe('Rollback Component', () => { }, apolloProvider, }); - const button = wrapper.find(GlDropdownItem); + const button = wrapper.findComponent(GlDropdownItem); button.vm.$emit('click'); expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js index 358abca2f77..851e24c22cc 100644 --- a/spec/frontend/environments/environment_stop_spec.js +++ b/spec/frontend/environments/environment_stop_spec.js @@ -22,7 +22,7 @@ describe('Stop Component', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); describe('eventHub', () => { beforeEach(() => { diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js index aff6b1327f0..49a643aaac8 100644 --- a/spec/frontend/environments/environment_table_spec.js +++ b/spec/frontend/environments/environment_table_spec.js @@ -177,10 +177,10 @@ describe('Environment table', () => { }, }); - wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40); + wrapper.findComponent(DeployBoard).vm.$emit('changeCanaryWeight', 40); await nextTick(); - expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({ + expect(wrapper.findComponent(CanaryUpdateModal).props()).toMatchObject({ weight: 40, environment: mockItem, }); diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js index 305e7385b43..4687119127d 100644 --- a/spec/frontend/environments/environments_detail_header_spec.js +++ b/spec/frontend/environments/environments_detail_header_spec.js @@ -1,5 +1,6 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; 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'; @@ -43,6 +44,9 @@ describe('Environments detail header component', () => { GlSprintf, TimeAgo, }, + directives: { + GlTooltip: createMockDirective(), + }, propsData: { canAdminEnvironment: false, canUpdateEnvironment: false, @@ -185,6 +189,14 @@ describe('Environments detail header component', () => { it('displays the metrics button with correct path', () => { expect(findMetricsButton().attributes('href')).toBe(metricsPath); }); + + it('uses a gl tooltip for the title', () => { + const button = findMetricsButton(); + const tooltip = getBinding(button.element, 'gl-tooltip'); + + expect(tooltip).toBeDefined(); + expect(button.attributes('title')).toBe('See metrics'); + }); }); describe('when has all admin rights', () => { diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js index 9eb57b2682f..f8b8465cf6f 100644 --- a/spec/frontend/environments/folder/environments_folder_view_spec.js +++ b/spec/frontend/environments/folder/environments_folder_view_spec.js @@ -65,7 +65,7 @@ describe('Environments Folder View', () => { }); it('should render a table with environments', () => { - const table = wrapper.find(EnvironmentTable); + const table = wrapper.findComponent(EnvironmentTable); expect(table.exists()).toBe(true); expect(table.find('.environment-name').text()).toEqual(environmentsList[0].name); @@ -93,7 +93,7 @@ describe('Environments Folder View', () => { describe('pagination', () => { it('should render pagination', () => { - expect(wrapper.find(GlPagination).exists()).toBe(true); + expect(wrapper.findComponent(GlPagination).exists()).toBe(true); }); it('should make an API request when changing page', () => { @@ -126,7 +126,7 @@ describe('Environments Folder View', () => { }); it('should not render a table', () => { - expect(wrapper.find(EnvironmentTable).exists()).toBe(false); + expect(wrapper.findComponent(EnvironmentTable).exists()).toBe(false); }); it('should render available tab with count 0', () => { diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js index f6d970e02d8..5a1c1c7714c 100644 --- a/spec/frontend/environments/new_environment_spec.js +++ b/spec/frontend/environments/new_environment_spec.js @@ -40,7 +40,7 @@ describe('~/environments/components/new.vue', () => { wrapper.destroy(); }); - const showsLoading = () => wrapper.find(GlLoadingIcon).exists(); + const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists(); const submitForm = async (expected, response) => { mock diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index 4273da6c735..732eff65495 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -35,7 +35,9 @@ describe('ErrorDetails', () => { const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1'; const findInput = (name) => { - const inputs = wrapper.findAll(GlFormInput).filter((c) => c.attributes('name') === name); + const inputs = wrapper + .findAllComponents(GlFormInput) + .filter((c) => c.attributes('name') === name); return inputs.length ? inputs.at(0) : inputs; }; @@ -44,7 +46,7 @@ describe('ErrorDetails', () => { const findUpdateResolveStatusButton = () => wrapper.find('[data-testid="update-resolve-status-btn"]'); const findExternalUrl = () => wrapper.find('[data-testid="external-url-link"]'); - const findAlert = () => wrapper.find(GlAlert); + const findAlert = () => wrapper.findComponent(GlAlert); function mountComponent() { wrapper = shallowMount(ErrorDetails, { @@ -119,9 +121,9 @@ describe('ErrorDetails', () => { }); it('should show spinner while loading', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find(GlLink).exists()).toBe(false); - expect(wrapper.find(Stacktrace).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLink).exists()).toBe(false); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(false); }); }); @@ -141,7 +143,7 @@ describe('ErrorDetails', () => { wrapper.vm.onNoApolloResult(); await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); expect(createFlash).not.toHaveBeenCalled(); expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled(); }); @@ -152,8 +154,8 @@ describe('ErrorDetails', () => { wrapper.vm.onNoApolloResult(); await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(GlLink).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLink).exists()).toBe(false); expect(createFlash).toHaveBeenCalledWith({ message: 'Could not connect to Sentry. Refresh the page to try again.', type: 'warning', @@ -186,11 +188,11 @@ describe('ErrorDetails', () => { }); it('should show Sentry error details without stacktrace', () => { - expect(wrapper.find(GlLink).exists()).toBe(true); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find(Stacktrace).exists()).toBe(false); - expect(wrapper.find(GlBadge).exists()).toBe(false); - expect(wrapper.findAll(GlButton)).toHaveLength(3); + expect(wrapper.findComponent(GlLink).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(false); + expect(wrapper.findComponent(GlBadge).exists()).toBe(false); + expect(wrapper.findAllComponents(GlButton)).toHaveLength(3); }); describe('unsafe chars for culprit field', () => { @@ -227,7 +229,7 @@ describe('ErrorDetails', () => { }, }); await nextTick(); - expect(wrapper.findAll(GlBadge).length).toBe(2); + expect(wrapper.findAllComponents(GlBadge).length).toBe(2); }); it('should NOT show the badge if the tag is not present', async () => { @@ -239,7 +241,7 @@ describe('ErrorDetails', () => { }, }); await nextTick(); - expect(wrapper.findAll(GlBadge).length).toBe(1); + expect(wrapper.findAllComponents(GlBadge).length).toBe(1); }); it.each(Object.keys(severityLevel))( @@ -253,7 +255,7 @@ describe('ErrorDetails', () => { }, }); await nextTick(); - expect(wrapper.find(GlBadge).props('variant')).toEqual( + expect(wrapper.findComponent(GlBadge).props('variant')).toEqual( severityLevelVariant[severityLevel[level]], ); }, @@ -268,7 +270,7 @@ describe('ErrorDetails', () => { }, }); await nextTick(); - expect(wrapper.find(GlBadge).props('variant')).toEqual( + expect(wrapper.findComponent(GlBadge).props('variant')).toEqual( severityLevelVariant[severityLevel.ERROR], ); }); @@ -278,8 +280,8 @@ describe('ErrorDetails', () => { it('should show stacktrace', async () => { store.state.details.loadingStacktrace = false; await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(Stacktrace).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(true); expect(findAlert().exists()).toBe(false); }); @@ -287,8 +289,8 @@ describe('ErrorDetails', () => { store.state.details.loadingStacktrace = false; store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] }; await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(Stacktrace).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(false); expect(findAlert().text()).toBe('No stack trace for this error'); }); }); diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js index 7ed4e5f6b05..5f6c9ddb4d7 100644 --- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js @@ -35,7 +35,7 @@ describe('Error Tracking Actions', () => { } }); - const findButtons = () => wrapper.findAll(GlButton); + const findButtons = () => wrapper.findAllComponents(GlButton); describe('when error status is unresolved', () => { it('renders the correct actions buttons to allow ignore and resolve', async () => { diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 23d448f3964..b7dffbbec04 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -19,13 +19,13 @@ describe('ErrorTrackingList', () => { const findErrorListTable = () => wrapper.find('table'); const findErrorListRows = () => wrapper.findAll('tbody tr'); - const dropdownsArray = () => wrapper.findAll(GlDropdown); - const findRecentSearchesDropdown = () => dropdownsArray().at(0).find(GlDropdown); - const findStatusFilterDropdown = () => dropdownsArray().at(1).find(GlDropdown); - const findSortDropdown = () => dropdownsArray().at(2).find(GlDropdown); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findPagination = () => wrapper.find(GlPagination); - const findErrorActions = () => wrapper.find(ErrorTrackingActions); + const dropdownsArray = () => wrapper.findAllComponents(GlDropdown); + const findRecentSearchesDropdown = () => dropdownsArray().at(0).findComponent(GlDropdown); + const findStatusFilterDropdown = () => dropdownsArray().at(1).findComponent(GlDropdown); + const findSortDropdown = () => dropdownsArray().at(2).findComponent(GlDropdown); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findPagination = () => wrapper.findComponent(GlPagination); + const findErrorActions = () => wrapper.findComponent(ErrorTrackingActions); const findIntegratedDisabledAlert = () => wrapper.findByTestId('integrated-disabled-alert'); function mountComponent({ @@ -152,12 +152,12 @@ describe('ErrorTrackingList', () => { it('each error in the list should have an action button set', () => { findErrorListRows().wrappers.forEach((row) => { - expect(row.find(ErrorTrackingActions).exists()).toBe(true); + expect(row.findComponent(ErrorTrackingActions).exists()).toBe(true); }); }); describe('filtering', () => { - const findSearchBox = () => wrapper.find(GlFormInput); + const findSearchBox = () => wrapper.findComponent(GlFormInput); it('shows search box & sort dropdown', () => { expect(findSearchBox().exists()).toBe(true); @@ -222,7 +222,7 @@ describe('ErrorTrackingList', () => { }); it('shows empty state', () => { - expect(wrapper.find(GlEmptyState).exists()).toBe(true); + expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(false); expect(findErrorListTable().exists()).toBe(false); expect(dropdownsArray().length).toBe(0); @@ -327,7 +327,7 @@ describe('ErrorTrackingList', () => { }); it('shows empty state', () => { - expect(wrapper.find(GlEmptyState).isVisible()).toBe(true); + expect(wrapper.findComponent(GlEmptyState).isVisible()).toBe(true); }); }); @@ -358,7 +358,7 @@ describe('ErrorTrackingList', () => { }); describe('clear', () => { - const clearRecentButton = () => wrapper.find({ ref: 'clearRecentSearches' }); + const clearRecentButton = () => wrapper.findComponent({ ref: 'clearRecentSearches' }); it('is hidden when list empty', () => { store.state.list.recentSearches = []; diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js index 0b43167c19b..693fcff50ca 100644 --- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js +++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js @@ -36,10 +36,10 @@ describe('Stacktrace Entry', () => { it('should render stacktrace entry collapsed', () => { mountComponent({ lines }); - expect(wrapper.find(StackTraceEntry).exists()).toBe(true); - expect(wrapper.find(ClipboardButton).exists()).toBe(true); - expect(wrapper.find(GlIcon).exists()).toBe(true); - expect(wrapper.find(FileIcon).exists()).toBe(true); + expect(wrapper.findComponent(StackTraceEntry).exists()).toBe(true); + expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(FileIcon).exists()).toBe(true); expect(wrapper.find('table').exists()).toBe(false); }); @@ -56,7 +56,7 @@ describe('Stacktrace Entry', () => { it('should hide collapse icon and render error fn name and error line when there is no code block', () => { const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 }; mountComponent({ expanded: false, lines: [], ...extraInfo }); - expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); expect(trimText(findFileHeaderContent())).toContain( `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`, ); diff --git a/spec/frontend/error_tracking/components/stacktrace_spec.js b/spec/frontend/error_tracking/components/stacktrace_spec.js index 4f4a60acba4..cd5a57f5683 100644 --- a/spec/frontend/error_tracking/components/stacktrace_spec.js +++ b/spec/frontend/error_tracking/components/stacktrace_spec.js @@ -33,13 +33,13 @@ describe('ErrorDetails', () => { it('should render single Stacktrace entry', () => { mountComponent([stackTraceEntry]); - expect(wrapper.findAll(StackTraceEntry).length).toBe(1); + expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(1); }); it('should render multiple Stacktrace entry', () => { const entriesNum = 3; mountComponent(new Array(entriesNum).fill(stackTraceEntry)); - expect(wrapper.findAll(StackTraceEntry).length).toBe(entriesNum); + expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(entriesNum); }); }); }); diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js index 1ba5a505f57..b44af547658 100644 --- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js +++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js @@ -41,23 +41,23 @@ describe('error tracking settings project dropdown', () => { describe('empty project list', () => { it('renders the dropdown', () => { - expect(wrapper.find('#project-dropdown').exists()).toBeTruthy(); - expect(wrapper.find(GlDropdown).exists()).toBeTruthy(); + expect(wrapper.find('#project-dropdown').exists()).toBe(true); + expect(wrapper.find(GlDropdown).exists()).toBe(true); }); it('shows helper text', () => { - expect(wrapper.find('.js-project-dropdown-label').exists()).toBeTruthy(); + expect(wrapper.find('.js-project-dropdown-label').exists()).toBe(true); expect(wrapper.find('.js-project-dropdown-label').text()).toContain( 'To enable project selection', ); }); it('does not show an error', () => { - expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy(); + expect(wrapper.find('.js-project-dropdown-error').exists()).toBe(false); }); it('does not contain any dropdown items', () => { - expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy(); + expect(wrapper.find(GlDropdownItem).exists()).toBe(false); expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available'); }); }); @@ -70,12 +70,12 @@ describe('error tracking settings project dropdown', () => { }); it('renders the dropdown', () => { - expect(wrapper.find('#project-dropdown').exists()).toBeTruthy(); - expect(wrapper.find(GlDropdown).exists()).toBeTruthy(); + expect(wrapper.find('#project-dropdown').exists()).toBe(true); + expect(wrapper.find(GlDropdown).exists()).toBe(true); }); it('contains a number of dropdown items', () => { - expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy(); + expect(wrapper.find(GlDropdownItem).exists()).toBe(true); expect(wrapper.findAll(GlDropdownItem).length).toBe(2); }); }); @@ -89,8 +89,8 @@ describe('error tracking settings project dropdown', () => { }); it('does not show helper text', () => { - expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy(); - expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy(); + expect(wrapper.find('.js-project-dropdown-label').exists()).toBe(false); + expect(wrapper.find('.js-project-dropdown-error').exists()).toBe(false); }); }); @@ -105,8 +105,8 @@ describe('error tracking settings project dropdown', () => { }); it('displays a error', () => { - expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy(); - expect(wrapper.find('.js-project-dropdown-error').exists()).toBeTruthy(); + expect(wrapper.find('.js-project-dropdown-label').exists()).toBe(false); + expect(wrapper.find('.js-project-dropdown-error').exists()).toBe(true); }); }); }); diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js index 4a0242b4a46..c1051a14a08 100644 --- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js +++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js @@ -39,7 +39,7 @@ describe('Configure Feature Flags Modal', () => { const findSecondaryAction = () => findGlModal().props('actionSecondary'); const findProjectNameInput = () => wrapper.find('#project_name_verification'); const findDangerGlAlert = () => - wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'danger'); + wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'danger'); describe('idle', () => { afterEach(() => wrapper.destroy()); @@ -157,7 +157,7 @@ describe('Configure Feature Flags Modal', () => { beforeEach(factory.bind(null, { isRotating: true })); it('should disable the project name input', async () => { - expect(findProjectNameInput().attributes('disabled')).toBeTruthy(); + expect(findProjectNameInput().attributes('disabled')).toBe('true'); }); }); }); diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js index 4ac82ae44a6..e3cc6f703c4 100644 --- a/spec/frontend/feature_flags/components/empty_state_spec.js +++ b/spec/frontend/feature_flags/components/empty_state_spec.js @@ -57,7 +57,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { beforeEach(() => { wrapper = factory(); - alerts = wrapper.findAll(GlAlert); + alerts = wrapper.findAllComponents(GlAlert); }); it('should show any alerts', () => { @@ -68,7 +68,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { it('should emit a dismiss event for a dismissed alert', () => { alerts.at(0).vm.$emit('dismiss'); - expect(wrapper.find(EmptyState).emitted('dismissAlert')).toEqual([[0]]); + expect(wrapper.findComponent(EmptyState).emitted('dismissAlert')).toEqual([[0]]); }); }); @@ -78,8 +78,8 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { }); it('should show a loading icon and nothing else', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.findAll(GlEmptyState)).toHaveLength(0); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findAllComponents(GlEmptyState)).toHaveLength(0); }); }); @@ -88,7 +88,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { beforeEach(() => { wrapper = factory({ errorState: true }); - emptyState = wrapper.find(GlEmptyState); + emptyState = wrapper.findComponent(GlEmptyState); }); it('should show an error state if there has been an error', () => { @@ -106,8 +106,8 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { beforeEach(() => { wrapper = factory({ emptyState: true }); - emptyState = wrapper.find(GlEmptyState); - emptyStateLink = emptyState.find(GlLink); + emptyState = wrapper.findComponent(GlEmptyState); + emptyStateLink = emptyState.findComponent(GlLink); }); it('should show an empty state if it is empty', () => { diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js index cca472012e9..e8103df78bc 100644 --- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js @@ -23,7 +23,7 @@ describe('Feature flags > Environments dropdown ', () => { }); }; - const findEnvironmentSearchInput = () => wrapper.find(GlSearchBoxByType); + const findEnvironmentSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findDropdownMenu = () => wrapper.find('.dropdown-menu'); afterEach(() => { @@ -91,7 +91,7 @@ describe('Feature flags > Environments dropdown ', () => { describe('with received data', () => { it('sets is loading to false', () => { expect(wrapper.vm.isLoading).toBe(false); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); it('shows the suggestions', () => { @@ -100,7 +100,7 @@ describe('Feature flags > Environments dropdown ', () => { it('emits event when a suggestion is clicked', async () => { const button = wrapper - .findAll(GlButton) + .findAllComponents(GlButton) .filter((b) => b.text() === 'production') .at(0); button.vm.$emit('click'); @@ -111,7 +111,7 @@ describe('Feature flags > Environments dropdown ', () => { describe('on click clear button', () => { beforeEach(async () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); await nextTick(); }); @@ -137,7 +137,7 @@ describe('Feature flags > Environments dropdown ', () => { }); it('emits create event', async () => { - wrapper.findAll(GlButton).at(0).vm.$emit('click'); + wrapper.findAllComponents(GlButton).at(0).vm.$emit('click'); await nextTick(); expect(wrapper.emitted('createClicked')).toEqual([['production']]); }); diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js index 99864a95f59..47f12f70056 100644 --- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js +++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js @@ -119,7 +119,7 @@ describe('Feature flag table', () => { it('should render an environments specs badge with active class', () => { const envColumn = wrapper.find('.js-feature-flag-environments'); - expect(trimText(envColumn.find(GlBadge).text())).toBe('All Users: All Environments'); + expect(trimText(envColumn.findComponent(GlBadge).text())).toBe('All Users: All Environments'); }); it('should render an actions column', () => { @@ -137,7 +137,7 @@ describe('Feature flag table', () => { beforeEach(() => { props.featureFlags[0].update_path = props.featureFlags[0].destroy_path; createWrapper(props); - toggle = wrapper.find(GlToggle); + toggle = wrapper.findComponent(GlToggle); spy = mockTracking('_category_', toggle.element, jest.spyOn); }); diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js index 3ad1225906b..7dd7c709c94 100644 --- a/spec/frontend/feature_flags/components/form_spec.js +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -61,7 +61,7 @@ describe('feature flag form', () => { it('does not render the related issues widget without the featureFlagIssuesEndpoint', () => { factory(requiredProps); - expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(false); + expect(wrapper.findComponent(RelatedIssuesRoot).exists()).toBe(false); }); it('renders the related issues widget when the featureFlagIssuesEndpoint is provided', () => { @@ -73,7 +73,7 @@ describe('feature flag form', () => { }, ); - expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(true); + expect(wrapper.findComponent(RelatedIssuesRoot).exists()).toBe(true); }); describe('without provided data', () => { @@ -114,7 +114,7 @@ describe('feature flag form', () => { }); it('should show the strategy component', () => { - const strategy = wrapper.find(Strategy); + const strategy = wrapper.findComponent(Strategy); expect(strategy.exists()).toBe(true); expect(strategy.props('strategy')).toEqual({ type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, @@ -124,14 +124,14 @@ describe('feature flag form', () => { }); it('should show one strategy component per strategy', () => { - expect(wrapper.findAll(Strategy)).toHaveLength(2); + expect(wrapper.findAllComponents(Strategy)).toHaveLength(2); }); it('adds an all users strategy when clicking the Add button', async () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); await nextTick(); - const strategies = wrapper.findAll(Strategy); + const strategies = wrapper.findAllComponents(Strategy); expect(strategies).toHaveLength(3); expect(strategies.at(2).props('strategy')).toEqual(allUsersStrategy); @@ -143,10 +143,10 @@ describe('feature flag form', () => { parameters: { percentage: '30' }, scopes: [], }; - wrapper.find(Strategy).vm.$emit('delete'); + wrapper.findComponent(Strategy).vm.$emit('delete'); await nextTick(); - expect(wrapper.findAll(Strategy)).toHaveLength(1); - expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy); + expect(wrapper.findAllComponents(Strategy)).toHaveLength(1); + expect(wrapper.findComponent(Strategy).props('strategy')).not.toEqual(strategy); }); }); }); diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js index 63fa5d19982..1c0c444c296 100644 --- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js @@ -31,17 +31,17 @@ describe('New Environments Dropdown', () => { describe('before results', () => { it('should show a loading icon', () => { axiosMock.onGet(TEST_HOST).reply(() => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); return axios.waitForAll(); }); it('should not show any dropdown items', () => { axiosMock.onGet(TEST_HOST).reply(() => { - expect(wrapper.findAll(GlDropdownItem)).toHaveLength(0); + expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(0); }); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); return axios.waitForAll(); }); }); @@ -50,11 +50,11 @@ describe('New Environments Dropdown', () => { let item; beforeEach(async () => { axiosMock.onGet(TEST_HOST).reply(200, []); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); - wrapper.find(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH); await axios.waitForAll(); await nextTick(); - item = wrapper.find(GlDropdownItem); + item = wrapper.findComponent(GlDropdownItem); }); it('should display a Create item label', () => { @@ -62,7 +62,7 @@ describe('New Environments Dropdown', () => { }); it('should display that no matching items are found', () => { - expect(wrapper.find({ ref: 'noResults' }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(true); }); it('should emit a new scope when selected', () => { @@ -75,10 +75,10 @@ describe('New Environments Dropdown', () => { let items; beforeEach(() => { axiosMock.onGet(TEST_HOST).reply(httpStatusCodes.OK, ['prod', 'production']); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); - wrapper.find(GlSearchBoxByType).vm.$emit('input', 'prod'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod'); return axios.waitForAll().then(() => { - items = wrapper.findAll(GlDropdownItem); + items = wrapper.findAllComponents(GlDropdownItem); }); }); @@ -97,7 +97,7 @@ describe('New Environments Dropdown', () => { }); it('should not display a message about no results', () => { - expect(wrapper.find({ ref: 'noResults' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js index 688ba54f919..300d0e47082 100644 --- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js +++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js @@ -40,7 +40,7 @@ describe('New feature flag form', () => { }; const findWarningGlAlert = () => - wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning'); + wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'warning'); beforeEach(() => { factory(); @@ -65,11 +65,11 @@ describe('New feature flag form', () => { }); it('should render feature flag form', () => { - expect(wrapper.find(Form).exists()).toEqual(true); + expect(wrapper.findComponent(Form).exists()).toEqual(true); }); it('has an all users strategy by default', () => { - const strategies = wrapper.find(Form).props('strategies'); + const strategies = wrapper.findComponent(Form).props('strategies'); expect(strategies).toEqual([allUsersStrategy]); }); diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js index 56b14d80ab3..70a9156b5a9 100644 --- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js @@ -34,12 +34,12 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { percentageFormGroup = wrapper .find('[data-testid="strategy-flexible-rollout-percentage"]') - .find(ParameterFormGroup); - percentageInput = percentageFormGroup.find(GlFormInput); + .findComponent(ParameterFormGroup); + percentageInput = percentageFormGroup.findComponent(GlFormInput); stickinessFormGroup = wrapper .find('[data-testid="strategy-flexible-rollout-stickiness"]') - .find(ParameterFormGroup); - stickinessSelect = stickinessFormGroup.find(GlFormSelect); + .findComponent(ParameterFormGroup); + stickinessSelect = stickinessFormGroup.findComponent(GlFormSelect); }); it('displays the current percentage value', () => { @@ -94,7 +94,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { it('shows errors', () => { const formGroup = wrapper .find('[data-testid="strategy-flexible-rollout-percentage"]') - .find(ParameterFormGroup); + .findComponent(ParameterFormGroup); expect(formGroup.attributes('state')).toBeUndefined(); }); @@ -108,7 +108,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { it('shows errors', () => { const formGroup = wrapper .find('[data-testid="strategy-flexible-rollout-percentage"]') - .find(ParameterFormGroup); + .findComponent(ParameterFormGroup); expect(formGroup.attributes('state')).toBeUndefined(); }); diff --git a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js index 3b69194494f..96b9434f3ec 100644 --- a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js +++ b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js @@ -24,10 +24,10 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { propsData: { ...DEFAULT_PROPS, ...props }, }); - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); describe('with user lists', () => { - const findDropdownItem = () => wrapper.find(GlDropdownItem); + const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); beforeEach(() => { Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] }); @@ -69,10 +69,10 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { r = resolve; }), ); - const searchWrapper = wrapper.find(GlSearchBoxByType); + const searchWrapper = wrapper.findComponent(GlSearchBoxByType); searchWrapper.vm.$emit('input', 'new'); await nextTick(); - const loadingIcon = wrapper.find(GlLoadingIcon); + const loadingIcon = wrapper.findComponent(GlLoadingIcon); expect(loadingIcon.exists()).toBe(true); expect(Api.searchFeatureFlagUserLists).toHaveBeenCalledWith('1', 'new'); diff --git a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js index 33696064d55..23ad0d3a08d 100644 --- a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js +++ b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js @@ -20,7 +20,7 @@ describe('~/feature_flags/strategies/parameter_form_group.vue', () => { }, }); - formGroup = wrapper.find(GlFormGroup); + formGroup = wrapper.findComponent(GlFormGroup); slot = wrapper.find('[data-testid="slot"]'); }); diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js index 180697e93e4..cb422a018f9 100644 --- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js @@ -30,8 +30,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { beforeEach(() => { wrapper = factory(); - input = wrapper.find(GlFormInput); - formGroup = wrapper.find(ParameterFormGroup); + input = wrapper.findComponent(GlFormInput); + formGroup = wrapper.findComponent(ParameterFormGroup); }); it('displays the current value', () => { @@ -55,8 +55,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { beforeEach(() => { wrapper = factory({ strategy: { parameters: { percentage: '101' } } }); - input = wrapper.find(GlFormInput); - formGroup = wrapper.find(ParameterFormGroup); + input = wrapper.findComponent(GlFormInput); + formGroup = wrapper.findComponent(ParameterFormGroup); }); it('shows errors', () => { @@ -68,8 +68,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { beforeEach(() => { wrapper = factory({ strategy: { parameters: { percentage: '3.14' } } }); - input = wrapper.find(GlFormInput); - formGroup = wrapper.find(ParameterFormGroup); + input = wrapper.findComponent(GlFormInput); + formGroup = wrapper.findComponent(ParameterFormGroup); }); it('shows errors', () => { diff --git a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js index 745fbca00fe..0a72714c22a 100644 --- a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js +++ b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js @@ -15,7 +15,7 @@ describe('~/feature_flags/components/users_with_id.vue', () => { beforeEach(() => { wrapper = factory(); - textarea = wrapper.find(GlFormTextarea); + textarea = wrapper.findComponent(GlFormTextarea); }); afterEach(() => { diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js index 979ca255b08..d0f1f7d0e2a 100644 --- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js +++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js @@ -51,11 +51,11 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => { }); it('should show the correct component', () => { - expect(wrapper.find(component).exists()).toBe(true); + expect(wrapper.findComponent(component).exists()).toBe(true); }); it('should emit changes from the lower component', () => { - const strategyParameterWrapper = wrapper.find(component); + const strategyParameterWrapper = wrapper.findComponent(component); strategyParameterWrapper.vm.$emit('change', { parameters: { foo: 'bar' } }); @@ -77,7 +77,7 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => { strategy, }); - expect(wrapper.find(UsersWithId).props('strategy')).toEqual(strategy); + expect(wrapper.findComponent(UsersWithId).props('strategy')).toEqual(strategy); }); }); }); diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js index aee3873721c..84d4180fe63 100644 --- a/spec/frontend/feature_flags/components/strategy_spec.js +++ b/spec/frontend/feature_flags/components/strategy_spec.js @@ -32,8 +32,8 @@ Vue.use(Vuex); describe('Feature flags strategy', () => { let wrapper; - const findStrategyParameters = () => wrapper.find(StrategyParameters); - const findDocsLinks = () => wrapper.findAll(GlLink); + const findStrategyParameters = () => wrapper.findComponent(StrategyParameters); + const findDocsLinks = () => wrapper.findAllComponents(GlLink); const factory = ( opts = { @@ -93,7 +93,7 @@ describe('Feature flags strategy', () => { }); it('should set the select to match the strategy name', () => { - expect(wrapper.find(GlFormSelect).element.value).toBe(name); + expect(wrapper.findComponent(GlFormSelect).element.value).toBe(name); }); it('should emit a change if the parameters component does', () => { @@ -118,7 +118,7 @@ describe('Feature flags strategy', () => { }); it('shows an alert asking users to consider using flexibleRollout instead', () => { - expect(wrapper.find(GlAlert).text()).toContain( + expect(wrapper.findComponent(GlAlert).text()).toContain( 'Consider using the more flexible "Percent rollout" strategy instead.', ); }); @@ -139,10 +139,10 @@ describe('Feature flags strategy', () => { }); it('should revert to all-environments scope when last scope is removed', async () => { - const token = wrapper.find(GlToken); + const token = wrapper.findComponent(GlToken); token.vm.$emit('close'); await nextTick(); - expect(wrapper.findAll(GlToken)).toHaveLength(0); + expect(wrapper.findAllComponents(GlToken)).toHaveLength(0); expect(last(wrapper.emitted('change'))).toEqual([ { name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, @@ -167,7 +167,7 @@ describe('Feature flags strategy', () => { }); it('should change the parameters if a different strategy is chosen', async () => { - const select = wrapper.find(GlFormSelect); + const select = wrapper.findComponent(GlFormSelect); select.setValue(ROLLOUT_STRATEGY_ALL_USERS); await nextTick(); expect(last(wrapper.emitted('change'))).toEqual([ @@ -180,26 +180,26 @@ describe('Feature flags strategy', () => { }); it('should display selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); - expect(wrapper.findAll(GlToken)).toHaveLength(1); - expect(wrapper.find(GlToken).text()).toBe('production'); + expect(wrapper.findAllComponents(GlToken)).toHaveLength(1); + expect(wrapper.findComponent(GlToken).text()).toBe('production'); }); it('should display all selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); dropdown.vm.$emit('add', 'staging'); await nextTick(); - const tokens = wrapper.findAll(GlToken); + const tokens = wrapper.findAllComponents(GlToken); expect(tokens).toHaveLength(2); expect(tokens.at(0).text()).toBe('production'); expect(tokens.at(1).text()).toBe('staging'); }); it('should emit selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); expect(last(wrapper.emitted('change'))).toEqual([ @@ -215,7 +215,7 @@ describe('Feature flags strategy', () => { }); it('should emit a delete if the delete button is clicked', () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); expect(wrapper.emitted('delete')).toEqual([[]]); }); }); @@ -232,26 +232,26 @@ describe('Feature flags strategy', () => { }); it('should display selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); - expect(wrapper.findAll(GlToken)).toHaveLength(1); - expect(wrapper.find(GlToken).text()).toBe('production'); + expect(wrapper.findAllComponents(GlToken)).toHaveLength(1); + expect(wrapper.findComponent(GlToken).text()).toBe('production'); }); it('should display all selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); dropdown.vm.$emit('add', 'staging'); await nextTick(); - const tokens = wrapper.findAll(GlToken); + const tokens = wrapper.findAllComponents(GlToken); expect(tokens).toHaveLength(2); expect(tokens.at(0).text()).toBe('production'); expect(tokens.at(1).text()).toBe('staging'); }); it('should emit selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); expect(last(wrapper.emitted('change'))).toEqual([ diff --git a/spec/frontend/fixtures/integrations.rb b/spec/frontend/fixtures/integrations.rb index 1bafb0bfe78..45d1c400f5d 100644 --- a/spec/frontend/fixtures/integrations.rb +++ b/spec/frontend/fixtures/integrations.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') } let!(:service) { create(:custom_issue_tracker_integration, project: project) } let(:user) { project.first_owner } diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb index 8bedb802242..cde796497d4 100644 --- a/spec/frontend/fixtures/issues.rb +++ b/spec/frontend/fixtures/issues.rb @@ -106,3 +106,43 @@ RSpec.describe API::Issues, '(JavaScript fixtures)', type: :request do expect(response).to be_successful end end + +RSpec.describe GraphQL::Query, type: :request do + include ApiHelpers + include GraphqlHelpers + include JavaScriptFixturesHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:issue_type) { 'issue' } + + before_all do + project.add_reporter(user) + end + + issue_popover_query_path = 'issuable/popover/queries/issue.query.graphql' + + it "graphql/#{issue_popover_query_path}.json" do + query = get_graphql_query_as_string(issue_popover_query_path, ee: Gitlab.ee?) + + issue = create( + :issue, + project: project, + confidential: true, + created_at: Time.parse('2020-07-01T04:08:01Z'), + due_date: Date.new(2020, 7, 5), + milestone: create( + :milestone, + project: project, + title: '15.2', + start_date: Date.new(2020, 7, 1), + due_date: Date.new(2020, 7, 30) + ), + issue_type: issue_type + ) + + post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s }) + + expect_graphql_errors_to_be_empty + end +end diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb new file mode 100644 index 00000000000..b11f661fe09 --- /dev/null +++ b/spec/frontend/fixtures/namespaces.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Jobs (JavaScript fixtures)' do + include ApiHelpers + include JavaScriptFixturesHelpers + include GraphqlHelpers + + describe GraphQL::Query, type: :request do + let_it_be(:user) { create(:user) } + let_it_be(:groups) { create_list(:group, 4) } + + before_all do + groups.each { |group| group.add_owner(user) } + end + + query_name = 'search_namespaces_where_user_can_transfer_projects' + query_extension = '.query.graphql' + + full_input_path = "projects/settings/graphql/queries/#{query_name}#{query_extension}" + base_output_path = "graphql/projects/settings/#{query_name}" + + it "#{base_output_path}_page_1#{query_extension}.json" do + query = get_graphql_query_as_string(full_input_path) + + post_graphql(query, current_user: user, variables: { first: 2 }) + + expect_graphql_errors_to_be_empty + end + + it "#{base_output_path}_page_2#{query_extension}.json" do + query = get_graphql_query_as_string(full_input_path) + + post_graphql(query, current_user: user, variables: { first: 2 }) + + post_graphql( + query, + current_user: user, + variables: { first: 2, after: graphql_data_at('currentUser', 'groups', 'pageInfo', 'endCursor') } + ) + + expect_graphql_errors_to_be_empty + end + end +end diff --git a/spec/frontend/fixtures/prometheus_integration.rb b/spec/frontend/fixtures/prometheus_integration.rb index 883dbb929a2..250c50bc8bb 100644 --- a/spec/frontend/fixtures/prometheus_integration.rb +++ b/spec/frontend/fixtures/prometheus_integration.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') } let!(:integration) { create(:prometheus_integration, project: project) } let(:user) { project.first_owner } diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb index 36281af0219..b523650dda5 100644 --- a/spec/frontend/fixtures/runner.rb +++ b/spec/frontend/fixtures/runner.rb @@ -13,11 +13,12 @@ RSpec.describe 'Runner (JavaScript fixtures)' do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:project_2) { create(:project, :repository, :public) } - let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') } - let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') } - let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') } - let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') } - let_it_be(:build) { create(:ci_build, runner: instance_runner) } + let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', version: '1.0.0') } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], version: '2.0.0') } + let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], version: '2.0.0') } + let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], version: '2.0.0') } + + let_it_be(:build) { create(:ci_build, runner: runner) } query_path = 'runner/graphql/' fixtures_path = 'graphql/runner/' @@ -27,18 +28,19 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end before do - allow(Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ not_available: nil }) + allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance| + allow(instance).to receive(:check_runner_upgrade_suggestion) + .and_return([nil, :not_available]) + end end - describe do + describe 'as admin', GraphQL::Query do before do sign_in(admin) enable_admin_mode!(admin) end - describe GraphQL::Query, type: :request do + describe 'all_runners.query.graphql', type: :request do all_runners_query = 'list/all_runners.query.graphql' let_it_be(:query) do @@ -58,7 +60,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end end - describe GraphQL::Query, type: :request do + describe 'all_runners_count.query.graphql', type: :request do all_runners_count_query = 'list/all_runners_count.query.graphql' let_it_be(:query) do @@ -72,7 +74,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end end - describe GraphQL::Query, type: :request do + describe 'runner.query.graphql', type: :request do runner_query = 'show/runner.query.graphql' let_it_be(:query) do @@ -81,7 +83,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do it "#{fixtures_path}#{runner_query}.json" do post_graphql(query, current_user: admin, variables: { - id: instance_runner.to_global_id.to_s + id: runner.to_global_id.to_s }) expect_graphql_errors_to_be_empty @@ -96,7 +98,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end end - describe GraphQL::Query, type: :request do + describe 'runner_projects.query.graphql', type: :request do runner_projects_query = 'show/runner_projects.query.graphql' let_it_be(:query) do @@ -112,7 +114,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end end - describe GraphQL::Query, type: :request do + describe 'runner_jobs.query.graphql', type: :request do runner_jobs_query = 'show/runner_jobs.query.graphql' let_it_be(:query) do @@ -121,14 +123,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do it "#{fixtures_path}#{runner_jobs_query}.json" do post_graphql(query, current_user: admin, variables: { - id: instance_runner.to_global_id.to_s + id: runner.to_global_id.to_s }) expect_graphql_errors_to_be_empty end end - describe GraphQL::Query, type: :request do + describe 'runner_form.query.graphql', type: :request do runner_jobs_query = 'edit/runner_form.query.graphql' let_it_be(:query) do @@ -137,7 +139,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do it "#{fixtures_path}#{runner_jobs_query}.json" do post_graphql(query, current_user: admin, variables: { - id: instance_runner.to_global_id.to_s + id: runner.to_global_id.to_s }) expect_graphql_errors_to_be_empty @@ -145,14 +147,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end end - describe do + describe 'as group owner', GraphQL::Query do let_it_be(:group_owner) { create(:user) } before do group.add_owner(group_owner) end - describe GraphQL::Query, type: :request do + describe 'group_runners.query.graphql', type: :request do group_runners_query = 'list/group_runners.query.graphql' let_it_be(:query) do @@ -177,7 +179,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do end end - describe GraphQL::Query, type: :request do + describe 'group_runners_count.query.graphql', type: :request do group_runners_count_query = 'list/group_runners_count.query.graphql' let_it_be(:query) do diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js index 32c66c0d288..c201bbf4af2 100644 --- a/spec/frontend/frequent_items/components/app_spec.js +++ b/spec/frontend/frequent_items/components/app_spec.js @@ -145,11 +145,16 @@ describe('Frequent Items App Component', () => { expect(findFrequentItemsList().props()).toEqual( expect.objectContaining({ items: mockSearchedProjects.data.map( - ({ avatar_url, web_url, name_with_namespace, ...item }) => ({ + ({ + avatar_url: avatarUrl, + web_url: webUrl, + name_with_namespace: namespace, + ...item + }) => ({ ...item, - avatarUrl: avatar_url, - webUrl: web_url, - namespace: name_with_namespace, + avatarUrl, + webUrl, + namespace, }), ), namespace: TEST_NAMESPACE, diff --git a/spec/frontend/gfm_auto_complete/mock_data.js b/spec/frontend/gfm_auto_complete/mock_data.js new file mode 100644 index 00000000000..86795ffd0a5 --- /dev/null +++ b/spec/frontend/gfm_auto_complete/mock_data.js @@ -0,0 +1,34 @@ +export const eventlistenersMockDefaultMap = [ + { + key: 'shown', + namespace: 'atwho', + }, + { + key: 'shown-users', + namespace: 'atwho', + }, + { + key: 'shown-issues', + namespace: 'atwho', + }, + { + key: 'shown-milestones', + namespace: 'atwho', + }, + { + key: 'shown-mergerequests', + namespace: 'atwho', + }, + { + key: 'shown-labels', + namespace: 'atwho', + }, + { + key: 'shown-snippets', + namespace: 'atwho', + }, + { + key: 'shown-contacts', + namespace: 'atwho', + }, +]; diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 072cf34d0ef..c3dfc4570f9 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -10,6 +10,7 @@ import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import AjaxCache from '~/lib/utils/ajax_cache'; import axios from '~/lib/utils/axios_utils'; +import { eventlistenersMockDefaultMap } from 'ee_else_ce_jest/gfm_auto_complete/mock_data'; describe('GfmAutoComplete', () => { const fetchDataMock = { fetchData: jest.fn() }; @@ -457,12 +458,12 @@ describe('GfmAutoComplete', () => { it('should be false with actual array data', () => { expect( - GfmAutoComplete.isLoading([{ title: 'Foo' }, { title: 'Bar' }, { title: 'Qux' }]), + GfmAutoComplete.isLoading([{ title: 'events' }, { title: 'Bar' }, { title: 'Qux' }]), ).toBe(false); }); it('should be false with actual data item', () => { - expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false); + expect(GfmAutoComplete.isLoading({ title: 'events' })).toBe(false); }); }); @@ -884,4 +885,47 @@ describe('GfmAutoComplete', () => { ).toBe(`<li><small>${escapedPayload} ${escapedPayload}</small> ${escapedPayload}</li>`); }); }); + + describe('autocomplete show eventlisteners', () => { + let $textarea; + + beforeEach(() => { + setHTMLFixture('<textarea></textarea>'); + $textarea = $('textarea'); + }); + + it('sets correct eventlisteners when autocomplete features are enabled', () => { + const autocomplete = new GfmAutoComplete({}); + autocomplete.setup($textarea); + autocomplete.setupAtWho($textarea); + /* eslint-disable-next-line no-underscore-dangle */ + const events = $._data($textarea[0], 'events'); + expect( + Object.keys(events) + .filter((x) => { + return x.startsWith('shown'); + }) + .map((e) => { + return { key: e, namespace: events[e][0].namespace }; + }), + ).toEqual(expect.arrayContaining(eventlistenersMockDefaultMap)); + }); + + it('sets no eventlisteners when features are disabled', () => { + const autocomplete = new GfmAutoComplete({}); + autocomplete.setup($textarea, {}); + autocomplete.setupAtWho($textarea); + /* eslint-disable-next-line no-underscore-dangle */ + const events = $._data($textarea[0], 'events'); + expect( + Object.keys(events) + .filter((x) => { + return x.startsWith('shown'); + }) + .map((e) => { + return { key: e, namespace: events[e][0].namespace }; + }), + ).toStrictEqual([]); + }); + }); }); diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js index 70a22c86e62..5282c0ed839 100644 --- a/spec/frontend/group_settings/components/shared_runners_form_spec.js +++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js @@ -1,24 +1,24 @@ import { GlAlert } from '@gitlab/ui'; -import MockAxiosAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue'; -import axios from '~/lib/utils/axios_utils'; +import { updateGroup } from '~/api/groups_api'; -const UPDATE_PATH = '/test/update'; +jest.mock('~/api/groups_api'); + +const GROUP_ID = '99'; const RUNNER_ENABLED_VALUE = 'enabled'; const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable'; const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_with_override'; describe('group_settings/components/shared_runners_form', () => { let wrapper; - let mock; const createComponent = (provide = {}) => { wrapper = shallowMountExtended(SharedRunnersForm, { provide: { - updatePath: UPDATE_PATH, + groupId: GROUP_ID, sharedRunnersSetting: RUNNER_ENABLED_VALUE, parentSharedRunnersSetting: null, runnerEnabledValue: RUNNER_ENABLED_VALUE, @@ -36,18 +36,19 @@ describe('group_settings/components/shared_runners_form', () => { .at(0); const findSharedRunnersToggle = () => wrapper.findByTestId('shared-runners-toggle'); const findOverrideToggle = () => wrapper.findByTestId('override-runners-toggle'); - const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting; + const getSharedRunnersSetting = () => { + return updateGroup.mock.calls[0][1].shared_runners_setting; + }; beforeEach(() => { - mock = new MockAxiosAdapter(axios); - mock.onPut(UPDATE_PATH).reply(200); + updateGroup.mockResolvedValue({}); }); afterEach(() => { wrapper.destroy(); wrapper = null; - mock.restore(); + updateGroup.mockReset(); }); describe('default state', () => { @@ -115,7 +116,7 @@ describe('group_settings/components/shared_runners_form', () => { findSharedRunnersToggle().vm.$emit('change', false); await waitForPromises(); - expect(mock.history.put.length).toBe(1); + expect(updateGroup).toHaveBeenCalledTimes(1); }); it('is not loading state after completed request', async () => { @@ -170,12 +171,14 @@ describe('group_settings/components/shared_runners_form', () => { }); describe.each` - errorObj | message + errorData | message ${{}} | ${'An error occurred while updating configuration. Refresh the page and try again.'} ${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'} - `(`with error $errorObj`, ({ errorObj, message }) => { + `(`with error $errorObj`, ({ errorData, message }) => { beforeEach(async () => { - mock.onPut(UPDATE_PATH).reply(500, errorObj); + updateGroup.mockRejectedValue({ + response: { data: errorData }, + }); createComponent(); findSharedRunnersToggle().vm.$emit('change', false); diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index 9e4666ffc70..a6bbea648d2 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -85,30 +85,6 @@ describe('AppComponent', () => { await nextTick(); }); - describe('computed', () => { - describe('groups', () => { - it('should return list of groups from store', () => { - jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {}); - - const { groups } = vm; - - expect(vm.store.getGroups).toHaveBeenCalled(); - expect(groups).not.toBeDefined(); - }); - }); - - describe('pageInfo', () => { - it('should return pagination info from store', () => { - jest.spyOn(vm.store, 'getPaginationInfo').mockImplementation(() => {}); - - const { pageInfo } = vm; - - expect(vm.store.getPaginationInfo).toHaveBeenCalled(); - expect(pageInfo).not.toBeDefined(); - }); - }); - }); - describe('methods', () => { describe('fetchGroups', () => { it('should call `getGroups` with all the params provided', () => { diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 0bc80df6535..9906f62878f 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -6,19 +6,20 @@ import ItemActions from '~/groups/components/item_actions.vue'; import eventHub from '~/groups/event_hub'; import { getGroupItemMicrodata } from '~/groups/store/utils'; import * as urlUtilities from '~/lib/utils/url_utility'; +import { ITEM_TYPE } from '~/groups/constants'; import { - ITEM_TYPE, - VISIBILITY_INTERNAL, - VISIBILITY_PRIVATE, - VISIBILITY_PUBLIC, -} from '~/groups/constants'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; + VISIBILITY_LEVEL_PRIVATE, + VISIBILITY_LEVEL_INTERNAL, + VISIBILITY_LEVEL_PUBLIC, +} from '~/visibility_level/constants'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import { mockParentGroupItem, mockChildren } from '../mock_data'; const createComponent = ( propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] }, provide = { - currentGroupVisibility: VISIBILITY_PRIVATE, + currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE, }, ) => { return mountExtended(GroupItem, { @@ -289,7 +290,7 @@ describe('GroupItemComponent', () => { }); describe('visibility warning popover', () => { - const findPopover = () => wrapper.findComponent(GlPopover); + const findPopover = () => extendedWrapper(wrapper.findComponent(GlPopover)); const itDoesNotRenderVisibilityWarningPopover = () => { it('does not render visibility warning popover', () => { @@ -319,13 +320,16 @@ describe('GroupItemComponent', () => { describe('when showing projects', () => { describe.each` - itemVisibility | currentGroupVisibility | isPopoverShown - ${VISIBILITY_PRIVATE} | ${VISIBILITY_PUBLIC} | ${false} - ${VISIBILITY_INTERNAL} | ${VISIBILITY_PUBLIC} | ${false} - ${VISIBILITY_PUBLIC} | ${VISIBILITY_PUBLIC} | ${false} - ${VISIBILITY_PRIVATE} | ${VISIBILITY_PRIVATE} | ${false} - ${VISIBILITY_INTERNAL} | ${VISIBILITY_PRIVATE} | ${true} - ${VISIBILITY_PUBLIC} | ${VISIBILITY_PRIVATE} | ${true} + itemVisibility | currentGroupVisibility | isPopoverShown + ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} + ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} + ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} + ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PRIVATE} | ${false} + ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PRIVATE} | ${true} + ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PRIVATE} | ${true} + ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_INTERNAL} | ${false} + ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_INTERNAL} | ${false} + ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_INTERNAL} | ${true} `( 'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility', ({ itemVisibility, currentGroupVisibility, isPopoverShown }) => { @@ -347,8 +351,17 @@ describe('GroupItemComponent', () => { }); if (isPopoverShown) { - it('renders visibility warning popover', () => { - expect(findPopover().exists()).toBe(true); + it('renders visibility warning popover with `Learn more` link', () => { + const popover = findPopover(); + + expect(popover.exists()).toBe(true); + expect( + popover.findByRole('link', { name: GroupItem.i18n.learnMore }).attributes('href'), + ).toBe( + helpPagePath('user/project/members/share_project_with_groups', { + anchor: 'sharing-projects-with-groups-of-a-higher-restrictive-visibility-level', + }), + ); }); } else { itDoesNotRenderVisibilityWarningPopover(); @@ -361,7 +374,7 @@ describe('GroupItemComponent', () => { wrapper = createComponent({ group: { ...mockParentGroupItem, - visibility: VISIBILITY_PUBLIC, + visibility: VISIBILITY_LEVEL_PUBLIC, type: ITEM_TYPE.PROJECT, }, parentGroup: mockChildren[0], diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js index 9c9bdead6fa..823d2ed286a 100644 --- a/spec/frontend/groups/components/group_name_and_path_spec.js +++ b/spec/frontend/groups/components/group_name_and_path_spec.js @@ -1,18 +1,23 @@ -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { merge } from 'lodash'; -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlDropdown, GlTruncate, GlDropdownItem } from '@gitlab/ui'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; import GroupNameAndPath from '~/groups/components/group_name_and_path.vue'; import { getGroupPathAvailability } from '~/rest_api'; import { createAlert } from '~/flash'; import { helpPagePath } from '~/helpers/help_page_helper'; +import searchGroupsWhereUserCanCreateSubgroups from '~/groups/queries/search_groups_where_user_can_create_subgroups.query.graphql'; jest.mock('~/flash'); jest.mock('~/rest_api', () => ({ getGroupPathAvailability: jest.fn(), })); +Vue.use(VueApollo); + describe('GroupNameAndPath', () => { let wrapper; @@ -20,6 +25,17 @@ describe('GroupNameAndPath', () => { const mockGroupUrl = 'my-awesome-group'; const mockGroupUrlSuggested = 'my-awesome-group1'; + const mockQueryResponse = jest.fn().mockResolvedValue({ + data: { + currentUser: { + id: '1', + groups: { + nodes: [{ id: '2', fullPath: '/path2' }], + }, + }, + }, + }); + const defaultProvide = { basePath: 'http://gitlab.com/', fields: { @@ -32,13 +48,20 @@ describe('GroupNameAndPath', () => { pattern: '[a-zA-Z0-9_\\.][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9_\\-]|[a-zA-Z0-9_]', }, parentId: { name: 'group[parent_id]', id: 'group_parent_id', value: '1' }, + parentFullPath: { name: 'group[parent_full_path]', id: 'group_full_path', value: '/path1' }, groupId: { name: 'group[id]', id: 'group_id', value: '' }, }, + newSubgroup: false, mattermostEnabled: false, }; const createComponent = ({ provide = {} } = {}) => { - wrapper = mountExtended(GroupNameAndPath, { provide: merge({}, defaultProvide, provide) }); + wrapper = mountExtended(GroupNameAndPath, { + provide: merge({}, defaultProvide, provide), + apolloProvider: createMockApollo([ + [searchGroupsWhereUserCanCreateSubgroups, mockQueryResponse], + ]), + }); }; const createComponentEditGroup = ({ path = mockGroupUrl } = {}) => { createComponent({ @@ -46,8 +69,11 @@ describe('GroupNameAndPath', () => { }); }; - const findGroupNameField = () => wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.name.label); - const findGroupUrlField = () => wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.path.label); + const findGroupNameField = () => wrapper.findByLabelText('Group name'); + const findGroupUrlField = () => wrapper.findByLabelText('Group URL'); + const findSubgroupNameField = () => wrapper.findByLabelText('Subgroup name'); + const findSubgroupSlugField = () => wrapper.findByLabelText('Subgroup slug'); + const findSelectedGroup = () => wrapper.findComponent(GlTruncate); const findAlert = () => extendedWrapper(wrapper.findComponent(GlAlert)); const apiMockAvailablePath = () => { @@ -79,6 +105,41 @@ describe('GroupNameAndPath', () => { }); }); + describe('when creating a new subgroup', () => { + beforeEach(() => { + createComponent({ provide: { newSubgroup: true } }); + }); + + it('updates `Subgroup slug` field as user types', async () => { + await findSubgroupNameField().setValue(mockGroupName); + + expect(findSubgroupSlugField().element.value).toBe(mockGroupUrl); + }); + + describe('when user selects parent group', () => { + it('updates `Subgroup URL` dropdown and calls API', async () => { + expect(findSelectedGroup().text()).toContain('/path1'); + + await findSubgroupNameField().setValue(mockGroupName); + + wrapper.findComponent(GlDropdown).vm.$emit('shown'); + await wrapper.vm.$apollo.queries.currentUserGroups.refetch(); + jest.runOnlyPendingTimers(); + await waitForPromises(); + + wrapper.findComponent(GlDropdownItem).vm.$emit('click'); + await nextTick(); + + expect(findSelectedGroup().text()).toContain('/path2'); + expect(getGroupPathAvailability).toHaveBeenCalled(); + + expect(wrapper.findByText(GroupNameAndPath.i18n.inputs.path.validFeedback).exists()).toBe( + true, + ); + }); + }); + }); + describe('when editing a group', () => { it('does not update `Group URL` field and does not call API', async () => { const groupUrl = 'foo-bar'; @@ -346,9 +407,7 @@ describe('GroupNameAndPath', () => { it('shows `Group ID` field', () => { createComponentEditGroup(); - expect( - wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.groupId.label).element.value, - ).toBe('1'); + expect(wrapper.findByLabelText('Group ID').element.value).toBe('1'); }); }); }); diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index 48a2319cf96..6c1eb373b7e 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -6,7 +6,7 @@ import GroupItemComponent from '~/groups/components/group_item.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import GroupsComponent from '~/groups/components/groups.vue'; import eventHub from '~/groups/event_hub'; -import { VISIBILITY_PRIVATE } from '~/groups/constants'; +import { VISIBILITY_LEVEL_PRIVATE } from '~/visibility_level/constants'; import { mockGroups, mockPageInfo } from '../mock_data'; describe('GroupsComponent', () => { @@ -26,7 +26,7 @@ describe('GroupsComponent', () => { ...propsData, }, provide: { - currentGroupVisibility: VISIBILITY_PRIVATE, + currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE, }, }); }; diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js index 6dc760f4f7c..8cfe8ce8e18 100644 --- a/spec/frontend/groups/components/transfer_group_form_spec.js +++ b/spec/frontend/groups/components/transfer_group_form_spec.js @@ -82,7 +82,6 @@ describe('Transfer group form', () => { it('sets the confirm danger properties', () => { expect(findConfirmDanger().props()).toMatchObject({ - buttonClass: 'qa-transfer-button', disabled: true, buttonText: confirmButtonText, phrase: confirmationPhrase, diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index d89218f5542..6a138f9a247 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -15,6 +15,10 @@ import { ICON_GROUP, ICON_SUBGROUP, SCOPE_TOKEN_MAX_LENGTH, + IS_SEARCHING, + IS_NOT_FOCUSED, + IS_FOCUSED, + SEARCH_SHORTCUTS_MIN_CHARACTERS, } from '~/header_search/constants'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { ENTER_KEY } from '~/lib/utils/keys'; @@ -170,6 +174,14 @@ describe('HeaderSearchApp', () => { it(`should render the Dropdown Navigation Component`, () => { expect(findDropdownKeyboardNavigation().exists()).toBe(true); }); + + it(`should close the dropdown when press escape key`, async () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 })); + await nextTick(); + expect(findHeaderSearchDropdown().exists()).toBe(false); + // only one event emmited from findHeaderSearchInput().vm.$emit('click'); + expect(wrapper.emitted().expandSearchBar.length).toBe(1); + }); }); }); @@ -245,6 +257,7 @@ describe('HeaderSearchApp', () => { searchOptions: () => searchOptions, }, ); + findHeaderSearchInput().vm.$emit('click'); }); it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${ @@ -263,47 +276,43 @@ describe('HeaderSearchApp', () => { }); }); - describe('form wrapper', () => { + describe('form', () => { describe.each` - searchContext | search | searchOptions - ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} - ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} - ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} - ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} - ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} - ${null} | ${null} | ${[]} - `('', ({ searchContext, search, searchOptions }) => { + searchContext | search | searchOptions | isFocused + ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false} + ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true} + ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true} + ${null} | ${null} | ${[]} | ${true} + `('wrapper', ({ searchContext, search, searchOptions, isFocused }) => { beforeEach(() => { window.gon.current_username = MOCK_USERNAME; - createComponent({ search, searchContext }, { searchOptions: () => searchOptions }); - - findHeaderSearchInput().vm.$emit('click'); + if (isFocused) { + findHeaderSearchInput().vm.$emit('click'); + } }); - const hasIcon = Boolean(searchContext?.group); - const isSearching = Boolean(search); - const isActive = Boolean(searchOptions.length > 0); + const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS; - it(`${hasIcon ? 'with' : 'without'} search context classes contain "${ - hasIcon ? 'has-icon' : 'has-no-icon' - }"`, () => { - const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon'; - expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => { + if (isSearching) { + expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING); + return; + } + if (!isSearching) { + expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING); + } }); - it(`${isSearching ? 'with' : 'without'} search string classes contain "${ - isSearching ? 'is-searching' : 'is-not-searching' + it(`classes ${isSearching ? 'contain' : 'do not contain'} "${ + isFocused ? IS_FOCUSED : IS_NOT_FOCUSED }"`, () => { - const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching'; - expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); - }); - - it(`${isActive ? 'with' : 'without'} search results classes contain "${ - isActive ? 'is-active' : 'is-not-active' - }"`, () => { - const iconClassRegex = isActive ? 'is-active' : 'is-not-active'; - expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + expect(findHeaderSearchForm().classes()).toContain( + isFocused ? IS_FOCUSED : IS_NOT_FOCUSED, + ); }); }); }); @@ -323,6 +332,7 @@ describe('HeaderSearchApp', () => { searchOptions: () => searchOptions, }, ); + findHeaderSearchInput().vm.$emit('click'); }); it(`icon for data set type "${searchOptions[0]?.html_id}" ${ diff --git a/spec/frontend/helpers/diffs_helper_spec.js b/spec/frontend/helpers/diffs_helper_spec.js index b223d48bf5c..c1ac7fac3fd 100644 --- a/spec/frontend/helpers/diffs_helper_spec.js +++ b/spec/frontend/helpers/diffs_helper_spec.js @@ -14,45 +14,45 @@ describe('diffs helper', () => { describe('hasInlineLines', () => { it('is false when the file does not exist', () => { - expect(diffsHelper.hasInlineLines()).toBeFalsy(); + expect(diffsHelper.hasInlineLines()).toBe(false); }); it('is false when the file does not have the highlighted_diff_lines property', () => { const missingInline = getDiffFile({ highlighted_diff_lines: undefined }); - expect(diffsHelper.hasInlineLines(missingInline)).toBeFalsy(); + expect(diffsHelper.hasInlineLines(missingInline)).toBe(false); }); it('is false when the file has zero highlighted_diff_lines', () => { const emptyInline = getDiffFile({ highlighted_diff_lines: [] }); - expect(diffsHelper.hasInlineLines(emptyInline)).toBeFalsy(); + expect(diffsHelper.hasInlineLines(emptyInline)).toBe(false); }); it('is true when the file has at least 1 highlighted_diff_lines', () => { - expect(diffsHelper.hasInlineLines(getDiffFile())).toBeTruthy(); + expect(diffsHelper.hasInlineLines(getDiffFile())).toBe(true); }); }); describe('hasParallelLines', () => { it('is false when the file does not exist', () => { - expect(diffsHelper.hasParallelLines()).toBeFalsy(); + expect(diffsHelper.hasParallelLines()).toBe(false); }); it('is false when the file does not have the parallel_diff_lines property', () => { const missingInline = getDiffFile({ parallel_diff_lines: undefined }); - expect(diffsHelper.hasParallelLines(missingInline)).toBeFalsy(); + expect(diffsHelper.hasParallelLines(missingInline)).toBe(false); }); it('is false when the file has zero parallel_diff_lines', () => { const emptyInline = getDiffFile({ parallel_diff_lines: [] }); - expect(diffsHelper.hasParallelLines(emptyInline)).toBeFalsy(); + expect(diffsHelper.hasParallelLines(emptyInline)).toBe(false); }); it('is true when the file has at least 1 parallel_diff_lines', () => { - expect(diffsHelper.hasParallelLines(getDiffFile())).toBeTruthy(); + expect(diffsHelper.hasParallelLines(getDiffFile())).toBe(true); }); }); @@ -61,16 +61,16 @@ describe('diffs helper', () => { const noParallelLines = getDiffFile({ parallel_diff_lines: undefined }); const emptyParallelLines = getDiffFile({ parallel_diff_lines: [] }); - expect(diffsHelper.isSingleViewStyle(noParallelLines)).toBeTruthy(); - expect(diffsHelper.isSingleViewStyle(emptyParallelLines)).toBeTruthy(); + expect(diffsHelper.isSingleViewStyle(noParallelLines)).toBe(true); + expect(diffsHelper.isSingleViewStyle(emptyParallelLines)).toBe(true); }); it('is true when the file has at least 1 parallel line but no inline lines for any reason', () => { const noInlineLines = getDiffFile({ highlighted_diff_lines: undefined }); const emptyInlineLines = getDiffFile({ highlighted_diff_lines: [] }); - expect(diffsHelper.isSingleViewStyle(noInlineLines)).toBeTruthy(); - expect(diffsHelper.isSingleViewStyle(emptyInlineLines)).toBeTruthy(); + expect(diffsHelper.isSingleViewStyle(noInlineLines)).toBe(true); + expect(diffsHelper.isSingleViewStyle(emptyInlineLines)).toBe(true); }); it('is true when the file does not have any inline lines or parallel lines for any reason', () => { @@ -83,13 +83,13 @@ describe('diffs helper', () => { parallel_diff_lines: [], }); - expect(diffsHelper.isSingleViewStyle(noLines)).toBeTruthy(); - expect(diffsHelper.isSingleViewStyle(emptyLines)).toBeTruthy(); - expect(diffsHelper.isSingleViewStyle()).toBeTruthy(); + expect(diffsHelper.isSingleViewStyle(noLines)).toBe(true); + expect(diffsHelper.isSingleViewStyle(emptyLines)).toBe(true); + expect(diffsHelper.isSingleViewStyle()).toBe(true); }); it('is false when the file has both inline and parallel lines', () => { - expect(diffsHelper.isSingleViewStyle(getDiffFile())).toBeFalsy(); + expect(diffsHelper.isSingleViewStyle(getDiffFile())).toBe(false); }); }); diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js index 39fe2c7e723..a97e883a8bf 100644 --- a/spec/frontend/ide/components/activity_bar_spec.js +++ b/spec/frontend/ide/components/activity_bar_spec.js @@ -1,86 +1,82 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { GlBadge } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import ActivityBar from '~/ide/components/activity_bar.vue'; import { leftSidebarViews } from '~/ide/constants'; import { createStore } from '~/ide/stores'; -describe('IDE activity bar', () => { - const Component = Vue.extend(ActivityBar); - let vm; +describe('IDE ActivityBar component', () => { + let wrapper; let store; - const findChangesBadge = () => vm.$el.querySelector('.badge'); + const findChangesBadge = () => wrapper.findComponent(GlBadge); - beforeEach(() => { + const mountComponent = (state) => { store = createStore(); - - Vue.set(store.state.projects, 'abcproject', { - web_url: 'testing', + store.replaceState({ + ...store.state, + projects: { abcproject: { web_url: 'testing' } }, + currentProjectId: 'abcproject', + ...state, }); - Vue.set(store.state, 'currentProjectId', 'abcproject'); - vm = createComponentWithStore(Component, store); - }); + wrapper = shallowMount(ActivityBar, { store }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('updateActivityBarView', () => { beforeEach(() => { - jest.spyOn(vm, 'updateActivityBarView').mockImplementation(() => {}); - - vm.$mount(); + mountComponent(); + jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {}); }); it('calls updateActivityBarView with edit value on click', () => { - vm.$el.querySelector('.js-ide-edit-mode').click(); + wrapper.find('.js-ide-edit-mode').trigger('click'); - expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name); + expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name); }); it('calls updateActivityBarView with commit value on click', () => { - vm.$el.querySelector('.js-ide-commit-mode').click(); + wrapper.find('.js-ide-commit-mode').trigger('click'); - expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name); + expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name); }); it('calls updateActivityBarView with review value on click', () => { - vm.$el.querySelector('.js-ide-review-mode').click(); + wrapper.find('.js-ide-review-mode').trigger('click'); - expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name); + expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name); }); }); describe('active item', () => { - beforeEach(() => { - vm.$mount(); - }); - it('sets edit item active', () => { - expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active'); + mountComponent(); + + expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active'); }); - it('sets commit item active', async () => { - vm.$store.state.currentActivityView = leftSidebarViews.commit.name; + it('sets commit item active', () => { + mountComponent({ currentActivityView: leftSidebarViews.commit.name }); - await nextTick(); - expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active'); + expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active'); }); }); describe('changes badge', () => { it('is rendered when files are staged', () => { - store.state.stagedFiles = [{ path: '/path/to/file' }]; - vm.$mount(); + mountComponent({ stagedFiles: [{ path: '/path/to/file' }] }); - expect(findChangesBadge()).toBeTruthy(); - expect(findChangesBadge().textContent.trim()).toBe('1'); + expect(findChangesBadge().exists()).toBe(true); + expect(findChangesBadge().text()).toBe('1'); }); it('is not rendered when no changes are present', () => { - vm.$mount(); - expect(findChangesBadge()).toBeFalsy(); + mountComponent(); + + expect(findChangesBadge().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js index 271d0600e16..3dbd1210916 100644 --- a/spec/frontend/ide/components/branches/item_spec.js +++ b/spec/frontend/ide/components/branches/item_spec.js @@ -44,8 +44,8 @@ describe('IDE branch item', () => { }); it('renders branch name and timeago', () => { expect(wrapper.text()).toContain(TEST_BRANCH.name); - expect(wrapper.find(Timeago).props('time')).toBe(TEST_BRANCH.committedDate); - expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(Timeago).props('time')).toBe(TEST_BRANCH.committedDate); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); }); it('renders link to branch', () => { @@ -60,6 +60,6 @@ describe('IDE branch item', () => { it('renders icon if is not active', () => { createComponent({ isActive: true }); - expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js index b6e3274153a..bbde45d700f 100644 --- a/spec/frontend/ide/components/branches/search_list_spec.js +++ b/spec/frontend/ide/components/branches/search_list_spec.js @@ -47,7 +47,7 @@ describe('IDE branches search list', () => { it('renders loading icon when `isLoading` is true', () => { createComponent({ isLoading: true }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders branches not found when search is not empty and branches list is empty', async () => { @@ -61,7 +61,7 @@ describe('IDE branches search list', () => { describe('with branches', () => { it('renders list', () => { createComponent({ branches }); - const items = wrapper.findAll(Item); + const items = wrapper.findAllComponents(Item); expect(items.length).toBe(branches.length); }); @@ -69,7 +69,7 @@ describe('IDE branches search list', () => { it('renders check next to active branch', () => { const activeBranch = 'regular'; createComponent({ branches }, activeBranch); - const items = wrapper.findAll(Item).filter((w) => w.props('isActive')); + const items = wrapper.findAllComponents(Item).filter((w) => w.props('isActive')); expect(items.length).toBe(1); expect(items.at(0).props('item').name).toBe(activeBranch); diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js index d77e8e3d04c..f6d5833edee 100644 --- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js @@ -26,8 +26,8 @@ describe('IDE commit editor header', () => { }); }; - const findDiscardModal = () => wrapper.find({ ref: 'discardModal' }); - const findDiscardButton = () => wrapper.find({ ref: 'discardButton' }); + const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' }); + const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' }); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index 28f62a9775a..a8ee81afa0b 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -58,7 +58,7 @@ describe('IDE commit form', () => { }); const findForm = () => wrapper.find('form'); const submitForm = () => findForm().trigger('submit'); - const findCommitMessageInput = () => wrapper.find(CommitMessageField); + const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField); const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val); const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]'); @@ -302,7 +302,7 @@ describe('IDE commit form', () => { ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }} ${createUnexpectedCommitError} | ${{ actionPrimary: null }} `('opens error modal if commitError with $error', async ({ createError, props }) => { - const modal = wrapper.find(GlModal); + const modal = wrapper.findComponent(GlModal); modal.vm.show = jest.fn(); const error = createError(); @@ -343,7 +343,7 @@ describe('IDE commit form', () => { await nextTick(); - wrapper.find(GlModal).vm.$emit('ok'); + wrapper.findComponent(GlModal).vm.$emit('ok'); await waitForPromises(); diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js index 17568158131..204d39de741 100644 --- a/spec/frontend/ide/components/error_message_spec.js +++ b/spec/frontend/ide/components/error_message_spec.js @@ -105,7 +105,7 @@ describe('IDE error message component', () => { findActionButton().trigger('click'); await nextTick(); - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true); resolveAction(); }); @@ -113,7 +113,7 @@ describe('IDE error message component', () => { findActionButton().trigger('click'); await actionMock(); await nextTick(); - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js index e54b322b9db..ee90d87357c 100644 --- a/spec/frontend/ide/components/file_templates/dropdown_spec.js +++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js @@ -94,7 +94,7 @@ describe('IDE file templates dropdown component', () => { it('shows loader when isLoading is true', () => { createComponent({ props: defaultAsyncProps, state: { isLoading: true } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders templates', () => { diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js index baf3d7cca9d..aa66224fa19 100644 --- a/spec/frontend/ide/components/ide_file_row_spec.js +++ b/spec/frontend/ide/components/ide_file_row_spec.js @@ -39,8 +39,8 @@ describe('Ide File Row component', () => { wrapper = null; }); - const findFileRowExtra = () => wrapper.find(FileRowExtra); - const findFileRow = () => wrapper.find(FileRow); + const findFileRowExtra = () => wrapper.findComponent(FileRowExtra); + const findFileRow = () => wrapper.findComponent(FileRow); const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen'); it('fileRow component has listeners', async () => { diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js index fc39651c661..d0636352a3f 100644 --- a/spec/frontend/ide/components/ide_project_header_spec.js +++ b/spec/frontend/ide/components/ide_project_header_spec.js @@ -3,6 +3,7 @@ import IDEProjectHeader from '~/ide/components/ide_project_header.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; const mockProject = { + id: 1, name: 'test proj', avatar_url: 'https://gitlab.com', path_with_namespace: 'path/with-namespace', @@ -30,6 +31,7 @@ describe('IDE project header', () => { it('renders ProjectAvatar with correct props', () => { expect(findProjectAvatar().props()).toMatchObject({ + projectId: mockProject.id, projectName: mockProject.name, projectAvatarUrl: mockProject.avatar_url, }); diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js index 13d20761263..0759f957374 100644 --- a/spec/frontend/ide/components/ide_review_spec.js +++ b/spec/frontend/ide/components/ide_review_spec.js @@ -42,7 +42,7 @@ describe('IDE review mode', () => { let inititializeSpy; beforeEach(async () => { - inititializeSpy = jest.spyOn(wrapper.find(IdeReview).vm, 'initialize'); + inititializeSpy = jest.spyOn(wrapper.findComponent(IdeReview).vm, 'initialize'); store.state.viewer = 'editor'; await wrapper.vm.reactivate(); @@ -85,7 +85,7 @@ describe('IDE review mode', () => { }); it('renders edit dropdown', () => { - expect(wrapper.find(EditorModeDropdown).exists()).toBe(true); + expect(wrapper.findComponent(EditorModeDropdown).exists()).toBe(true); }); it('renders merge request link & IID', async () => { diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js index 4469c3fc901..4784d6c516f 100644 --- a/spec/frontend/ide/components/ide_side_bar_spec.js +++ b/spec/frontend/ide/components/ide_side_bar_spec.js @@ -47,32 +47,32 @@ describe('IdeSidebar', () => { await nextTick(); - expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(3); + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); }); describe('deferred rendering components', () => { it('fetches components on demand', async () => { wrapper = createComponent(); - expect(wrapper.find(IdeTree).exists()).toBe(true); - expect(wrapper.find(IdeReview).exists()).toBe(false); - expect(wrapper.find(RepoCommitSection).exists()).toBe(false); + expect(wrapper.findComponent(IdeTree).exists()).toBe(true); + expect(wrapper.findComponent(IdeReview).exists()).toBe(false); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false); store.state.currentActivityView = leftSidebarViews.review.name; await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(false); - expect(wrapper.find(IdeReview).exists()).toBe(true); - expect(wrapper.find(RepoCommitSection).exists()).toBe(false); + expect(wrapper.findComponent(IdeTree).exists()).toBe(false); + expect(wrapper.findComponent(IdeReview).exists()).toBe(true); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false); store.state.currentActivityView = leftSidebarViews.commit.name; await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(false); - expect(wrapper.find(IdeReview).exists()).toBe(false); - expect(wrapper.find(RepoCommitSection).exists()).toBe(true); + expect(wrapper.findComponent(IdeTree).exists()).toBe(false); + expect(wrapper.findComponent(IdeReview).exists()).toBe(false); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true); }); it.each` view | tree | review | commit @@ -86,23 +86,23 @@ describe('IdeSidebar', () => { await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(tree); - expect(wrapper.find(IdeReview).exists()).toBe(review); - expect(wrapper.find(RepoCommitSection).exists()).toBe(commit); + expect(wrapper.findComponent(IdeTree).exists()).toBe(tree); + expect(wrapper.findComponent(IdeReview).exists()).toBe(review); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(commit); }); }); it('keeps the current activity view components alive', async () => { wrapper = createComponent(); - const ideTreeComponent = wrapper.find(IdeTree).element; + const ideTreeComponent = wrapper.findComponent(IdeTree).element; store.state.currentActivityView = leftSidebarViews.commit.name; await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(false); - expect(wrapper.find(RepoCommitSection).exists()).toBe(true); + expect(wrapper.findComponent(IdeTree).exists()).toBe(false); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true); store.state.currentActivityView = leftSidebarViews.edit.name; @@ -110,6 +110,6 @@ describe('IdeSidebar', () => { await nextTick(); // reference to the elements remains the same, meaning the components were kept alive - expect(wrapper.find(IdeTree).element).toEqual(ideTreeComponent); + expect(wrapper.findComponent(IdeTree).element).toEqual(ideTreeComponent); }); }); diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js index 2ea0c250794..80e8aba4072 100644 --- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js +++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js @@ -8,12 +8,12 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; const TEST_TABS = [ { title: 'Lorem', - icon: 'angle-up', + icon: 'chevron-lg-up', views: [{ name: 'lorem-1' }, { name: 'lorem-2' }], }, { title: 'Ipsum', - icon: 'angle-down', + icon: 'chevron-lg-down', views: [{ name: 'ipsum-1' }, { name: 'ipsum-2' }], }, ]; @@ -55,7 +55,7 @@ describe('ide/components/ide_sidebar_nav', () => { ariaLabel: button.attributes('aria-label'), classes: button.classes(), qaSelector: button.attributes('data-qa-selector'), - icon: button.find(GlIcon).props('name'), + icon: button.findComponent(GlIcon).props('name'), tooltip: getBinding(button.element, 'tooltip').value, }; }); diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index 9172c69b10e..48c670757a2 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -82,7 +82,7 @@ describe('WebIDE', () => { await waitForPromises(); - expect(wrapper.find(ErrorMessage).exists()).toBe(exists); + expect(wrapper.findComponent(ErrorMessage).exists()).toBe(exists); }, ); }); diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js index 17a5aa17b1f..e6e0ebaf1e8 100644 --- a/spec/frontend/ide/components/ide_status_bar_spec.js +++ b/spec/frontend/ide/components/ide_status_bar_spec.js @@ -1,8 +1,8 @@ +import { mount } from '@vue/test-utils'; import _ from 'lodash'; -import Vue, { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import IdeStatusBar from '~/ide/components/ide_status_bar.vue'; +import IdeStatusMR from '~/ide/components/ide_status_mr.vue'; import { rightSidebarViews } from '~/ide/constants'; import { createStore } from '~/ide/stores'; import { projectData } from '../mock_data'; @@ -13,42 +13,48 @@ const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_ jest.mock('~/lib/utils/poll'); -describe('ideStatusBar', () => { - let store; - let vm; +describe('IdeStatusBar component', () => { + let wrapper; + + const findMRStatus = () => wrapper.findComponent(IdeStatusMR); + + const mountComponent = (state = {}) => { + const store = createStore(); + store.replaceState({ + ...store.state, + currentBranchId: 'main', + currentProjectId: TEST_PROJECT_ID, + projects: { + ...store.state.projects, + [TEST_PROJECT_ID]: _.clone(projectData), + }, + ...state, + }); - const createComponent = () => { - vm = createComponentWithStore(Vue.extend(IdeStatusBar), store).$mount(); + wrapper = mount(IdeStatusBar, { store }); }; - const findMRStatus = () => vm.$el.querySelector('.js-ide-status-mr'); - - beforeEach(() => { - store = createStore(); - store.state.currentProjectId = TEST_PROJECT_ID; - store.state.projects[TEST_PROJECT_ID] = _.clone(projectData); - store.state.currentBranchId = 'main'; - }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('default', () => { - beforeEach(() => { - createComponent(); - }); - it('triggers a setInterval', () => { - expect(vm.intervalId).not.toBe(null); + mountComponent(); + + expect(wrapper.vm.intervalId).not.toBe(null); }); it('renders the statusbar', () => { - expect(vm.$el.className).toBe('ide-status-bar'); + mountComponent(); + + expect(wrapper.classes()).toEqual(['ide-status-bar']); }); describe('commitAgeUpdate', () => { beforeEach(() => { - jest.spyOn(vm, 'commitAgeUpdate').mockImplementation(() => {}); + mountComponent(); + jest.spyOn(wrapper.vm, 'commitAgeUpdate').mockImplementation(() => {}); }); afterEach(() => { @@ -56,70 +62,82 @@ describe('ideStatusBar', () => { }); it('gets called every second', () => { - expect(vm.commitAgeUpdate).not.toHaveBeenCalled(); + expect(wrapper.vm.commitAgeUpdate).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000); - expect(vm.commitAgeUpdate.mock.calls.length).toEqual(1); + expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(1); jest.advanceTimersByTime(1000); - expect(vm.commitAgeUpdate.mock.calls.length).toEqual(2); + expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(2); }); }); describe('getCommitPath', () => { it('returns the path to the commit details', () => { - expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de'); + mountComponent(); + + expect(wrapper.vm.getCommitPath('abc123de')).toBe('/commit/abc123de'); }); }); describe('pipeline status', () => { - it('opens right sidebar on clicking icon', async () => { - jest.spyOn(vm, 'openRightPane').mockImplementation(() => {}); - Vue.set(vm.$store.state.pipelines, 'latestPipeline', { - details: { - status: { - text: 'success', - details_path: 'test', - icon: 'status_success', + it('opens right sidebar on clicking icon', () => { + const pipelines = { + latestPipeline: { + details: { + status: { + text: 'success', + details_path: 'test', + icon: 'status_success', + }, + }, + commit: { + author_gravatar_url: 'www', }, }, - commit: { - author_gravatar_url: 'www', - }, - }); + }; + mountComponent({ pipelines }); + jest.spyOn(wrapper.vm, 'openRightPane').mockImplementation(() => {}); - await nextTick(); - vm.$el.querySelector('.ide-status-pipeline button').click(); + wrapper.find('button').trigger('click'); - expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines); + expect(wrapper.vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines); }); }); it('does not show merge request status', () => { - expect(findMRStatus()).toBe(null); + mountComponent(); + + expect(findMRStatus().exists()).toBe(false); }); }); describe('with merge request in store', () => { beforeEach(() => { - store.state.projects[TEST_PROJECT_ID].mergeRequests = { - [TEST_MERGE_REQUEST_ID]: { - web_url: TEST_MERGE_REQUEST_URL, - references: { - short: `!${TEST_MERGE_REQUEST_ID}`, + const state = { + currentMergeRequestId: TEST_MERGE_REQUEST_ID, + projects: { + [TEST_PROJECT_ID]: { + ..._.clone(projectData), + mergeRequests: { + [TEST_MERGE_REQUEST_ID]: { + web_url: TEST_MERGE_REQUEST_URL, + references: { + short: `!${TEST_MERGE_REQUEST_ID}`, + }, + }, + }, }, }, }; - store.state.currentMergeRequestId = TEST_MERGE_REQUEST_ID; - - createComponent(); + mountComponent(state); }); it('shows merge request status', () => { - expect(findMRStatus().textContent.trim()).toEqual(`Merge request !${TEST_MERGE_REQUEST_ID}`); - expect(findMRStatus().querySelector('a').href).toEqual(TEST_MERGE_REQUEST_URL); + expect(findMRStatus().text()).toBe(`Merge request !${TEST_MERGE_REQUEST_ID}`); + expect(findMRStatus().find('a').attributes('href')).toBe(TEST_MERGE_REQUEST_URL); }); }); }); diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js index 371fbc6becd..0b54e8b6afb 100644 --- a/spec/frontend/ide/components/ide_status_list_spec.js +++ b/spec/frontend/ide/components/ide_status_list_spec.js @@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => { let store; let wrapper; - const findLink = () => wrapper.find(GlLink); + const findLink = () => wrapper.findComponent(GlLink); const createComponent = (options = {}) => { store = new Vuex.Store({ getters: { @@ -98,6 +98,6 @@ describe('ide/components/ide_status_list', () => { it('renders terminal sync status', () => { createComponent(); - expect(wrapper.find(TerminalSyncStatusSafe).exists()).toBe(true); + expect(wrapper.findComponent(TerminalSyncStatusSafe).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js index 0526d4653f8..0b9111c0e2a 100644 --- a/spec/frontend/ide/components/ide_status_mr_spec.js +++ b/spec/frontend/ide/components/ide_status_mr_spec.js @@ -14,8 +14,8 @@ describe('ide/components/ide_status_mr', () => { propsData: props, }); }; - const findIcon = () => wrapper.find(GlIcon); - const findLink = () => wrapper.find(GlLink); + const findIcon = () => wrapper.findComponent(GlIcon); + const findLink = () => wrapper.findComponent(GlLink); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js index 8465ef9f5f3..f00017a2736 100644 --- a/spec/frontend/ide/components/ide_tree_spec.js +++ b/spec/frontend/ide/components/ide_tree_spec.js @@ -41,7 +41,7 @@ describe('IdeTree', () => { let inititializeSpy; beforeEach(async () => { - inititializeSpy = jest.spyOn(wrapper.find(IdeTree).vm, 'initialize'); + inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize'); store.state.viewer = 'diff'; await wrapper.vm.reactivate(); diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js index d632a34266a..5eb66f75978 100644 --- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js +++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js @@ -27,7 +27,7 @@ describe('IDE job log scroll button', () => { beforeEach(() => createComponent({ direction })); it('returns proper icon name', () => { - expect(wrapper.find(GlIcon).props('name')).toBe(icon); + expect(wrapper.findComponent(GlIcon).props('name')).toBe(icon); }); it('returns proper title', () => { diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js index cb2c9f8f04f..b4c7eb51781 100644 --- a/spec/frontend/ide/components/jobs/list_spec.js +++ b/spec/frontend/ide/components/jobs/list_spec.js @@ -58,29 +58,29 @@ describe('IDE stages list', () => { it('renders loading icon when no stages & loading', () => { createComponent({ loading: true, stages: [] }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders stages components for each stage', () => { createComponent({ stages }); - expect(wrapper.findAll(Stage).length).toBe(stages.length); + expect(wrapper.findAllComponents(Stage).length).toBe(stages.length); }); it('triggers fetchJobs action when stage emits fetch event', () => { createComponent({ stages }); - wrapper.find(Stage).vm.$emit('fetch'); + wrapper.findComponent(Stage).vm.$emit('fetch'); expect(storeActions.fetchJobs).toHaveBeenCalled(); }); it('triggers toggleStageCollapsed action when stage emits toggleCollapsed event', () => { createComponent({ stages }); - wrapper.find(Stage).vm.$emit('toggleCollapsed'); + wrapper.findComponent(Stage).vm.$emit('toggleCollapsed'); expect(storeActions.toggleStageCollapsed).toHaveBeenCalled(); }); it('triggers setDetailJob action when stage emits clickViewLog event', () => { createComponent({ stages }); - wrapper.find(Stage).vm.$emit('clickViewLog'); + wrapper.findComponent(Stage).vm.$emit('clickViewLog'); expect(storeActions.setDetailJob).toHaveBeenCalled(); }); diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js index f158c59cd32..1d5e5743a4d 100644 --- a/spec/frontend/ide/components/jobs/stage_spec.js +++ b/spec/frontend/ide/components/jobs/stage_spec.js @@ -18,8 +18,8 @@ describe('IDE pipeline stage', () => { }, }; - const findHeader = () => wrapper.find({ ref: 'cardHeader' }); - const findJobList = () => wrapper.find({ ref: 'jobList' }); + const findHeader = () => wrapper.findComponent({ ref: 'cardHeader' }); + const findJobList = () => wrapper.findComponent({ ref: 'jobList' }); const createComponent = (props) => { wrapper = shallowMount(Stage, { @@ -45,7 +45,7 @@ describe('IDE pipeline stage', () => { stage: { ...defaultProps.stage, isLoading: true, jobs: [] }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('emits toggleCollaped event with stage id when clicking header', async () => { @@ -60,7 +60,7 @@ describe('IDE pipeline stage', () => { it('emits clickViewLog entity with job', async () => { const [job] = defaultProps.stage.jobs; createComponent(); - wrapper.findAll(Item).at(0).vm.$emit('clickViewLog', job); + wrapper.findAllComponents(Item).at(0).vm.$emit('clickViewLog', job); await nextTick(); expect(wrapper.emitted().clickViewLog[0][0]).toBe(job); }); diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js index 583671a0af6..ea6e2741a85 100644 --- a/spec/frontend/ide/components/merge_requests/list_spec.js +++ b/spec/frontend/ide/components/merge_requests/list_spec.js @@ -14,7 +14,7 @@ describe('IDE merge requests list', () => { let fetchMergeRequestsMock; const findSearchTypeButtons = () => wrapper.findAll('button'); - const findTokenedInput = () => wrapper.find(TokenedInput); + const findTokenedInput = () => wrapper.findComponent(TokenedInput); const createComponent = (state = {}) => { const { mergeRequests = {}, ...restOfState } = state; @@ -63,7 +63,7 @@ describe('IDE merge requests list', () => { it('renders loading icon when merge request is loading', () => { createComponent({ mergeRequests: { isLoading: true } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders no search results text when search is not empty', async () => { @@ -107,8 +107,8 @@ describe('IDE merge requests list', () => { it('renders list', () => { createComponent(defaultStateWithMergeRequests); - expect(wrapper.findAll(Item).length).toBe(1); - expect(wrapper.find(Item).props('item')).toBe( + expect(wrapper.findAllComponents(Item).length).toBe(1); + expect(wrapper.findComponent(Item).props('item')).toBe( defaultStateWithMergeRequests.mergeRequests.mergeRequests[0], ); }); diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js index 19dcd9569b3..747c099db33 100644 --- a/spec/frontend/ide/components/new_dropdown/index_spec.js +++ b/spec/frontend/ide/components/new_dropdown/index_spec.js @@ -1,70 +1,66 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import newDropdown from '~/ide/components/new_dropdown/index.vue'; +import { mount } from '@vue/test-utils'; +import NewDropdown from '~/ide/components/new_dropdown/index.vue'; +import Button from '~/ide/components/new_dropdown/button.vue'; import { createStore } from '~/ide/stores'; describe('new dropdown component', () => { - let store; - let vm; - - beforeEach(() => { - store = createStore(); - - const component = Vue.extend(newDropdown); - - vm = createComponentWithStore(component, store, { - branch: 'main', - path: '', - mouseOver: false, - type: 'tree', + let wrapper; + + const findAllButtons = () => wrapper.findAllComponents(Button); + + const mountComponent = () => { + const store = createStore(); + store.state.currentProjectId = 'abcproject'; + store.state.path = ''; + store.state.trees['abcproject/mybranch'] = { tree: [] }; + + wrapper = mount(NewDropdown, { + store, + propsData: { + branch: 'main', + path: '', + mouseOver: false, + type: 'tree', + }, }); + }; - vm.$store.state.currentProjectId = 'abcproject'; - vm.$store.state.path = ''; - vm.$store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - - vm.$mount(); - - jest.spyOn(vm.$refs.newModal, 'open').mockImplementation(() => {}); + beforeEach(() => { + mountComponent(); + jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {}); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('renders new file, upload and new directory links', () => { - const buttons = vm.$el.querySelectorAll('.dropdown-menu button'); - - expect(buttons[0].textContent.trim()).toBe('New file'); - expect(buttons[1].textContent.trim()).toBe('Upload file'); - expect(buttons[2].textContent.trim()).toBe('New directory'); + expect(findAllButtons().at(0).text()).toBe('New file'); + expect(findAllButtons().at(1).text()).toBe('Upload file'); + expect(findAllButtons().at(2).text()).toBe('New directory'); }); describe('createNewItem', () => { it('opens modal for a blob when new file is clicked', () => { - vm.$el.querySelectorAll('.dropdown-menu button')[0].click(); + findAllButtons().at(0).trigger('click'); - expect(vm.$refs.newModal.open).toHaveBeenCalledWith('blob', ''); + expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('blob', ''); }); it('opens modal for a tree when new directory is clicked', () => { - vm.$el.querySelectorAll('.dropdown-menu button')[2].click(); + findAllButtons().at(2).trigger('click'); - expect(vm.$refs.newModal.open).toHaveBeenCalledWith('tree', ''); + expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('tree', ''); }); }); describe('isOpen', () => { it('scrolls dropdown into view', async () => { - jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {}); - - vm.isOpen = true; + jest.spyOn(wrapper.vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {}); - await nextTick(); + await wrapper.setProps({ isOpen: true }); - expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({ + expect(wrapper.vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({ block: 'nearest', }); }); @@ -72,11 +68,11 @@ describe('new dropdown component', () => { describe('delete entry', () => { it('calls delete action', () => { - jest.spyOn(vm, 'deleteEntry').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'deleteEntry').mockImplementation(() => {}); - vm.$el.querySelectorAll('.dropdown-menu button')[4].click(); + findAllButtons().at(4).trigger('click'); - expect(vm.deleteEntry).toHaveBeenCalledWith(''); + expect(wrapper.vm.deleteEntry).toHaveBeenCalledWith(''); }); }); }); diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js index 7f2ee0fe7d9..1d38231a767 100644 --- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js +++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js @@ -27,7 +27,7 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => { }); }; - const findSidebarNav = () => wrapper.find(IdeSidebarNav); + const findSidebarNav = () => wrapper.findComponent(IdeSidebarNav); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js index d12acd6dc4c..4555f519bc2 100644 --- a/spec/frontend/ide/components/panes/right_spec.js +++ b/spec/frontend/ide/components/panes/right_spec.js @@ -37,7 +37,7 @@ describe('ide/components/panes/right.vue', () => { it('is always shown', () => { createComponent(); - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: true, @@ -65,7 +65,7 @@ describe('ide/components/panes/right.vue', () => { createComponent(); - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: true, @@ -90,7 +90,7 @@ describe('ide/components/panes/right.vue', () => { store.state.terminal.isVisible = true; await nextTick(); - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: true, @@ -103,7 +103,7 @@ describe('ide/components/panes/right.vue', () => { it('hides terminal tab when not visible', () => { store.state.terminal.isVisible = false; - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: false, diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js index f7409fc36be..31081e8f9d5 100644 --- a/spec/frontend/ide/components/pipelines/empty_state_spec.js +++ b/spec/frontend/ide/components/pipelines/empty_state_spec.js @@ -32,7 +32,7 @@ describe('~/ide/components/pipelines/empty_state.vue', () => { }); it('renders empty state', () => { - expect(wrapper.find(GlEmptyState).props()).toMatchObject({ + expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ title: EmptyState.i18n.title, description: EmptyState.i18n.description, primaryButtonText: EmptyState.i18n.primaryButtonText, diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 8a3606e27eb..545924c9c11 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -99,7 +99,7 @@ describe('IDE pipelines list', () => { }, ); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); it('renders loading state', () => { @@ -111,7 +111,7 @@ describe('IDE pipelines list', () => { }, ); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); @@ -128,7 +128,7 @@ describe('IDE pipelines list', () => { it('renders empty state when no latestPipeline', () => { createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null }); - expect(wrapper.find(EmptyState).exists()).toBe(true); + expect(wrapper.findComponent(EmptyState).exists()).toBe(true); expect(wrapper.element).toMatchSnapshot(); }); @@ -144,7 +144,7 @@ describe('IDE pipelines list', () => { it('renders ci icon', () => { createComponent({}, withLatestPipelineState); - expect(wrapper.find(CiIcon).exists()).toBe(true); + expect(wrapper.findComponent(CiIcon).exists()).toBe(true); }); it('renders pipeline data', () => { @@ -158,7 +158,7 @@ describe('IDE pipelines list', () => { const isLoadingJobs = true; createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs }); - const jobProps = wrapper.findAll(GlTab).at(0).find(JobsList).props(); + const jobProps = wrapper.findAllComponents(GlTab).at(0).findComponent(JobsList).props(); expect(jobProps.stages).toBe(stages); expect(jobProps.loading).toBe(isLoadingJobs); }); @@ -169,7 +169,7 @@ describe('IDE pipelines list', () => { const isLoadingJobs = true; createComponent({}, { ...withLatestPipelineState, isLoadingJobs }); - const jobProps = wrapper.findAll(GlTab).at(1).find(JobsList).props(); + const jobProps = wrapper.findAllComponents(GlTab).at(1).findComponent(JobsList).props(); expect(jobProps.stages).toBe(failedStages); expect(jobProps.loading).toBe(isLoadingJobs); }); diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js index 426fbd5c04c..cf768114e70 100644 --- a/spec/frontend/ide/components/preview/clientside_spec.js +++ b/spec/frontend/ide/components/preview/clientside_spec.js @@ -396,7 +396,7 @@ describe('IDE clientside preview', () => { wrapper.setData({ loading: true }); await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js index a199f4704f7..9c4f825ccf5 100644 --- a/spec/frontend/ide/components/preview/navigator_spec.js +++ b/spec/frontend/ide/components/preview/navigator_spec.js @@ -37,13 +37,13 @@ describe('IDE clientside preview navigator', () => { }); it('renders loading icon by default', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('removes loading icon when done event is fired', async () => { listenHandler({ type: 'done' }); await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); it('does not count visiting same url multiple times', async () => { diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js index db4181395d3..d3312358402 100644 --- a/spec/frontend/ide/components/repo_commit_section_spec.js +++ b/spec/frontend/ide/components/repo_commit_section_spec.js @@ -77,8 +77,10 @@ describe('RepoCommitSection', () => { }); it('renders no changes text', () => { - expect(wrapper.find(EmptyState).text().trim()).toContain('No changes'); - expect(wrapper.find(EmptyState).find('img').attributes('src')).toBe(TEST_NO_CHANGES_SVG); + expect(wrapper.findComponent(EmptyState).text().trim()).toContain('No changes'); + expect(wrapper.findComponent(EmptyState).find('img').attributes('src')).toBe( + TEST_NO_CHANGES_SVG, + ); }); }); @@ -111,7 +113,7 @@ describe('RepoCommitSection', () => { }); it('does not show empty state', () => { - expect(wrapper.find(EmptyState).exists()).toBe(false); + expect(wrapper.findComponent(EmptyState).exists()).toBe(false); }); }); @@ -157,7 +159,7 @@ describe('RepoCommitSection', () => { }); it('does not show empty state', () => { - expect(wrapper.find(EmptyState).exists()).toBe(false); + expect(wrapper.findComponent(EmptyState).exists()).toBe(false); }); }); @@ -167,7 +169,7 @@ describe('RepoCommitSection', () => { beforeEach(async () => { createComponent(); - inititializeSpy = jest.spyOn(wrapper.find(RepoCommitSection).vm, 'initialize'); + inititializeSpy = jest.spyOn(wrapper.findComponent(RepoCommitSection).vm, 'initialize'); store.state.viewer = 'diff'; await wrapper.vm.reactivate(); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 7a0bcda1b7a..9921d8cba18 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -145,8 +145,7 @@ describe('RepoEditor', () => { jest.clearAllMocks(); // create a new model each time, otherwise tests conflict with each other // because of same model being used in multiple tests - // eslint-disable-next-line no-undef - monaco.editor.getModels().forEach((model) => model.dispose()); + monacoEditor.getModels().forEach((model) => model.dispose()); wrapper.destroy(); wrapper = null; }); @@ -212,7 +211,7 @@ describe('RepoEditor', () => { it('renders markdown for tempFile', async () => { findPreviewTab().vm.$emit('click'); await waitForPromises(); - expect(wrapper.find(ContentViewer).html()).toContain(dummyFile.text.content); + expect(wrapper.findComponent(ContentViewer).html()).toContain(dummyFile.text.content); }); describe('when file changes to non-markdown file', () => { diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js index b16fd8f80ba..b26edc5a85b 100644 --- a/spec/frontend/ide/components/repo_tab_spec.js +++ b/spec/frontend/ide/components/repo_tab_spec.js @@ -19,7 +19,7 @@ describe('RepoTab', () => { let store; let router; - const findTab = () => wrapper.find(GlTabStub); + const findTab = () => wrapper.findComponent(GlTabStub); function createComponent(propsData) { wrapper = mount(RepoTab, { @@ -164,7 +164,7 @@ describe('RepoTab', () => { await wrapper.find('.multi-file-tab-close').trigger('click'); - expect(tab.opened).toBeFalsy(); + expect(tab.opened).toBe(false); expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1); }); @@ -180,7 +180,7 @@ describe('RepoTab', () => { await wrapper.find('.multi-file-tab-close').trigger('click'); - expect(tab.opened).toBeFalsy(); + expect(tab.opened).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js index 55b9423aba8..fe2a128c9c8 100644 --- a/spec/frontend/ide/components/resizable_panel_spec.js +++ b/spec/frontend/ide/components/resizable_panel_spec.js @@ -35,7 +35,7 @@ describe('~/ide/components/resizable_panel', () => { store, }); }; - const findResizer = () => wrapper.find(PanelResizer); + const findResizer = () => wrapper.findComponent(PanelResizer); const findInlineStyle = () => wrapper.element.style.cssText; const createInlineStyle = (width) => `width: ${width}px;`; diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js index f4f9b95b233..94da06f4cb2 100644 --- a/spec/frontend/ide/components/shared/commit_message_field_spec.js +++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js @@ -79,7 +79,7 @@ describe('CommitMessageField', () => { await fillText(text); expect(findHighlightsText().text()).toEqual(text); - expect(findHighlightsMark().text()).toBeFalsy(); + expect(findHighlightsMark().text()).toBe(''); }); it('highlights characters over 50 length', async () => { diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js index 57c816747aa..15fb0fe9013 100644 --- a/spec/frontend/ide/components/terminal/empty_state_spec.js +++ b/spec/frontend/ide/components/terminal/empty_state_spec.js @@ -46,7 +46,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('when not loading, does not show loading icon', () => { @@ -56,7 +56,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); describe('when valid', () => { @@ -71,7 +71,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - button = wrapper.find(GlButton); + button = wrapper.findComponent(GlButton); }); it('shows button', () => { @@ -100,7 +100,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - expect(wrapper.find(GlButton).props('disabled')).toBe(true); - expect(wrapper.find(GlAlert).html()).toContain(TEST_HTML_MESSAGE); + expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true); + expect(wrapper.findComponent(GlAlert).html()).toContain(TEST_HTML_MESSAGE); }); }); diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js index 6a70ddb46a8..7e4a56b0610 100644 --- a/spec/frontend/ide/components/terminal/session_spec.js +++ b/spec/frontend/ide/components/terminal/session_spec.js @@ -38,7 +38,7 @@ describe('IDE TerminalSession', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); beforeEach(() => { state = { @@ -60,7 +60,7 @@ describe('IDE TerminalSession', () => { it('shows terminal', () => { factory(); - expect(wrapper.find(Terminal).props()).toEqual({ + expect(wrapper.findComponent(Terminal).props()).toEqual({ terminalPath: TEST_TERMINAL_PATH, status: RUNNING, }); diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js index 71ec0dca89d..c18934f0f3b 100644 --- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js +++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js @@ -12,7 +12,7 @@ describe('IDE TerminalControls', () => { ...options, }); - buttons = wrapper.findAll(ScrollButton); + buttons = wrapper.findAllComponents(ScrollButton); }; it('shows an up and down scroll button', () => { diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js index afc49e22c83..4da3e1910e9 100644 --- a/spec/frontend/ide/components/terminal/terminal_spec.js +++ b/spec/frontend/ide/components/terminal/terminal_spec.js @@ -68,7 +68,7 @@ describe('IDE Terminal', () => { it(`shows when starting (${status})`, () => { factory({ status }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find('.top-bar').text()).toBe('Starting...'); }); }); @@ -76,7 +76,7 @@ describe('IDE Terminal', () => { it(`shows when stopping`, () => { factory({ status: STOPPING }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find('.top-bar').text()).toBe('Stopping...'); }); @@ -84,7 +84,7 @@ describe('IDE Terminal', () => { it('hides when not loading', () => { factory({ status }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find('.top-bar').text()).toBe(''); }); }); @@ -107,23 +107,23 @@ describe('IDE Terminal', () => { }); it('is visible if terminal is created', () => { - expect(wrapper.find(TerminalControls).exists()).toBe(true); + expect(wrapper.findComponent(TerminalControls).exists()).toBe(true); }); it('scrolls glterminal on scroll-up', () => { - wrapper.find(TerminalControls).vm.$emit('scroll-up'); + wrapper.findComponent(TerminalControls).vm.$emit('scroll-up'); expect(wrapper.vm.glterminal.scrollToTop).toHaveBeenCalled(); }); it('scrolls glterminal on scroll-down', () => { - wrapper.find(TerminalControls).vm.$emit('scroll-down'); + wrapper.findComponent(TerminalControls).vm.$emit('scroll-down'); expect(wrapper.vm.glterminal.scrollToBottom).toHaveBeenCalled(); }); it('has props set', () => { - expect(wrapper.find(TerminalControls).props()).toEqual({ + expect(wrapper.findComponent(TerminalControls).props()).toEqual({ canScrollUp: false, canScrollDown: false, }); @@ -133,7 +133,7 @@ describe('IDE Terminal', () => { wrapper.setData({ canScrollUp: true, canScrollDown: true }); return nextTick().then(() => { - expect(wrapper.find(TerminalControls).props()).toEqual({ + expect(wrapper.findComponent(TerminalControls).props()).toEqual({ canScrollUp: true, canScrollDown: true, }); diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js index 49f9513d2ac..57c8da9f5b7 100644 --- a/spec/frontend/ide/components/terminal/view_spec.js +++ b/spec/frontend/ide/components/terminal/view_spec.js @@ -66,7 +66,7 @@ describe('IDE TerminalView', () => { it('renders empty state', async () => { await factory(); - expect(wrapper.find(TerminalEmptyState).props()).toEqual({ + expect(wrapper.findComponent(TerminalEmptyState).props()).toEqual({ helpPath: TEST_HELP_PATH, illustrationPath: TEST_SVG_PATH, ...getters.allCheck(), @@ -79,7 +79,7 @@ describe('IDE TerminalView', () => { expect(actions.startSession).not.toHaveBeenCalled(); expect(actions.hideSplash).not.toHaveBeenCalled(); - wrapper.find(TerminalEmptyState).vm.$emit('start'); + wrapper.findComponent(TerminalEmptyState).vm.$emit('start'); expect(actions.startSession).toHaveBeenCalled(); expect(actions.hideSplash).toHaveBeenCalled(); @@ -89,7 +89,7 @@ describe('IDE TerminalView', () => { state.isShowSplash = false; await factory(); - expect(wrapper.find(TerminalEmptyState).exists()).toBe(false); - expect(wrapper.find(TerminalSession).exists()).toBe(true); + expect(wrapper.findComponent(TerminalEmptyState).exists()).toBe(false); + expect(wrapper.findComponent(TerminalSession).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js index f921037d744..5b1502cc190 100644 --- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js +++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js @@ -34,13 +34,13 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => { }); it('renders terminal sync status', () => { - expect(wrapper.find(TerminalSyncStatus).exists()).toBe(true); + expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(true); }); }); describe('without terminal sync module', () => { it('does not render terminal sync status', () => { - expect(wrapper.find(TerminalSyncStatus).exists()).toBe(false); + expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js index 3a326b08fff..147235abc8e 100644 --- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js +++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js @@ -78,19 +78,19 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => { if (!icon) { it('does not render icon', () => { - expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); }); it('renders loading icon', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); } else { it('renders icon', () => { - expect(wrapper.find(GlIcon).props('name')).toEqual(icon); + expect(wrapper.findComponent(GlIcon).props('name')).toEqual(icon); }); it('does not render loading icon', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); } }); diff --git a/spec/frontend/ide/lib/common/model_manager_spec.js b/spec/frontend/ide/lib/common/model_manager_spec.js index 08e4ab0f113..e485873e8da 100644 --- a/spec/frontend/ide/lib/common/model_manager_spec.js +++ b/spec/frontend/ide/lib/common/model_manager_spec.js @@ -59,7 +59,7 @@ describe('Multi-file editor library model manager', () => { describe('hasCachedModel', () => { it('returns false when no models exist', () => { - expect(instance.hasCachedModel('path')).toBeFalsy(); + expect(instance.hasCachedModel('path')).toBe(false); }); it('returns true when model exists', () => { @@ -67,7 +67,7 @@ describe('Multi-file editor library model manager', () => { instance.addModel(f); - expect(instance.hasCachedModel(f.key)).toBeTruthy(); + expect(instance.hasCachedModel(f.key)).toBe(true); }); }); diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js index 901f9e7cfd1..208ed9bf759 100644 --- a/spec/frontend/ide/lib/diff/diff_spec.js +++ b/spec/frontend/ide/lib/diff/diff_spec.js @@ -18,8 +18,8 @@ describe('Multi-file editor library diff calculator', () => { ({ originalContent, newContent, lineNumber }) => { const diff = computeDiff(originalContent, newContent)[0]; - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); + expect(diff.added).toBe(true); + expect(diff.modified).toBe(true); expect(diff.removed).toBeUndefined(); expect(diff.lineNumber).toBe(lineNumber); }, @@ -36,7 +36,7 @@ describe('Multi-file editor library diff calculator', () => { ({ originalContent, newContent, lineNumber }) => { const diff = computeDiff(originalContent, newContent)[0]; - expect(diff.added).toBeTruthy(); + expect(diff.added).toBe(true); expect(diff.modified).toBeUndefined(); expect(diff.removed).toBeUndefined(); expect(diff.lineNumber).toBe(lineNumber); @@ -56,7 +56,7 @@ describe('Multi-file editor library diff calculator', () => { expect(diff.added).toBeUndefined(); expect(diff.modified).toBe(modified); - expect(diff.removed).toBeTruthy(); + expect(diff.removed).toBe(true); expect(diff.lineNumber).toBe(lineNumber); }, ); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js index 6c1dee1e5ca..d1c31cd412b 100644 --- a/spec/frontend/ide/stores/actions/file_spec.js +++ b/spec/frontend/ide/stores/actions/file_spec.js @@ -60,8 +60,8 @@ describe('IDE store file actions', () => { it('closes open files', () => { return store.dispatch('closeFile', localFile).then(() => { - expect(localFile.opened).toBeFalsy(); - expect(localFile.active).toBeFalsy(); + expect(localFile.opened).toBe(false); + expect(localFile.active).toBe(false); expect(store.state.openFiles.length).toBe(0); }); }); @@ -269,7 +269,7 @@ describe('IDE store file actions', () => { it('sets the file as active', () => { return store.dispatch('getFileData', { path: localFile.path }).then(() => { - expect(localFile.active).toBeTruthy(); + expect(localFile.active).toBe(true); }); }); @@ -277,7 +277,7 @@ describe('IDE store file actions', () => { return store .dispatch('getFileData', { path: localFile.path, makeFileActive: false }) .then(() => { - expect(localFile.active).toBeFalsy(); + expect(localFile.active).toBe(false); }); }); diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js index d43393875eb..6e8a03b47ad 100644 --- a/spec/frontend/ide/stores/actions/tree_spec.js +++ b/spec/frontend/ide/stores/actions/tree_spec.js @@ -134,7 +134,7 @@ describe('Multi-file store tree actions', () => { it('toggles the tree open', async () => { await store.dispatch('toggleTreeOpen', tree.path); - expect(tree.opened).toBeTruthy(); + expect(tree.opened).toBe(true); }); }); diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js index 53d161ae5c9..24661e21cd0 100644 --- a/spec/frontend/ide/stores/getters_spec.js +++ b/spec/frontend/ide/stores/getters_spec.js @@ -268,7 +268,7 @@ describe('IDE store getters', () => { currentProject: undefined, }; - expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy(); + expect(getters.isOnDefaultBranch({}, localGetters)).toBe(undefined); }); it("returns true when project's default branch matches current branch", () => { @@ -279,7 +279,7 @@ describe('IDE store getters', () => { branchName: 'main', }; - expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy(); + expect(getters.isOnDefaultBranch({}, localGetters)).toBe(true); }); it("returns false when project's default branch doesn't match current branch", () => { @@ -290,7 +290,7 @@ describe('IDE store getters', () => { branchName: 'feature', }; - expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy(); + expect(getters.isOnDefaultBranch({}, localGetters)).toBe(false); }); }); diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js index 1e34087b290..38ebe36c2c5 100644 --- a/spec/frontend/ide/stores/modules/commit/getters_spec.js +++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js @@ -14,21 +14,21 @@ describe('IDE commit module getters', () => { describe('discardDraftButtonDisabled', () => { it('returns true when commitMessage is empty', () => { - expect(getters.discardDraftButtonDisabled(state)).toBeTruthy(); + expect(getters.discardDraftButtonDisabled(state)).toBe(true); }); it('returns false when commitMessage is not empty & loading is false', () => { state.commitMessage = 'test'; state.submitCommitLoading = false; - expect(getters.discardDraftButtonDisabled(state)).toBeFalsy(); + expect(getters.discardDraftButtonDisabled(state)).toBe(false); }); it('returns true when commitMessage is not empty & loading is true', () => { state.commitMessage = 'test'; state.submitCommitLoading = true; - expect(getters.discardDraftButtonDisabled(state)).toBeTruthy(); + expect(getters.discardDraftButtonDisabled(state)).toBe(true); }); }); @@ -152,13 +152,13 @@ describe('IDE commit module getters', () => { it('returns false if NOT creating a new branch', () => { state.commitAction = COMMIT_TO_CURRENT_BRANCH; - expect(getters.isCreatingNewBranch(state)).toBeFalsy(); + expect(getters.isCreatingNewBranch(state)).toBe(false); }); it('returns true if creating a new branch', () => { state.commitAction = COMMIT_TO_NEW_BRANCH; - expect(getters.isCreatingNewBranch(state)).toBeTruthy(); + expect(getters.isCreatingNewBranch(state)).toBe(true); }); }); @@ -183,7 +183,7 @@ describe('IDE commit module getters', () => { }); it('should never hide "New MR" option', () => { - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull(); }); }); @@ -195,13 +195,13 @@ describe('IDE commit module getters', () => { it('should NOT hide "New MR" option if user can NOT push to the current branch', () => { rootGetters.canPushToBranch = false; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); }); it('should hide "New MR" option if user can push to the current branch', () => { rootGetters.canPushToBranch = true; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); }); }); @@ -211,7 +211,7 @@ describe('IDE commit module getters', () => { }); it('should never hide "New MR" option', () => { - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull(); }); }); @@ -223,13 +223,13 @@ describe('IDE commit module getters', () => { it('should NOT hide "New MR" option if there is NO existing MR for the current branch', () => { rootGetters.hasMergeRequest = false; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull(); }); it('should hide "New MR" option if there is existing MR for the current branch', () => { rootGetters.hasMergeRequest = true; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); }); }); @@ -247,17 +247,13 @@ describe('IDE commit module getters', () => { it('should hide "New MR" when there is an existing MR', () => { rootGetters.hasMergeRequest = true; - expect( - getters.shouldHideNewMrOption(state, localGetters, null, rootGetters), - ).toBeTruthy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); }); it('should hide "New MR" when there is no existing MR', () => { rootGetters.hasMergeRequest = false; - expect( - getters.shouldHideNewMrOption(state, localGetters, null, rootGetters), - ).toBeTruthy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); }); }); @@ -270,17 +266,17 @@ describe('IDE commit module getters', () => { rootGetters.hasMergeRequest = false; rootGetters.canPushToBranch = true; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); rootGetters.hasMergeRequest = true; rootGetters.canPushToBranch = true; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); rootGetters.hasMergeRequest = false; rootGetters.canPushToBranch = false; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); }); }); }); @@ -292,7 +288,7 @@ describe('IDE commit module getters', () => { rootGetters.hasMergeRequest = true; rootGetters.canPushToBranch = true; - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); }); }); diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js index 1453f26c1d9..69ec2e7a6f5 100644 --- a/spec/frontend/ide/stores/mutations/file_spec.js +++ b/spec/frontend/ide/stores/mutations/file_spec.js @@ -22,7 +22,7 @@ describe('IDE store file mutations', () => { active: true, }); - expect(localFile.active).toBeTruthy(); + expect(localFile.active).toBe(true); }); it('sets pending tab as not active', () => { @@ -41,7 +41,7 @@ describe('IDE store file mutations', () => { it('adds into opened files', () => { mutations.TOGGLE_FILE_OPEN(localState, localFile.path); - expect(localFile.opened).toBeTruthy(); + expect(localFile.opened).toBe(true); expect(localState.openFiles.length).toBe(1); }); @@ -50,7 +50,7 @@ describe('IDE store file mutations', () => { mutations.TOGGLE_FILE_OPEN(localState, localFile.path); mutations.TOGGLE_FILE_OPEN(localState, localFile.path); - expect(localFile.opened).toBeFalsy(); + expect(localFile.opened).toBe(false); expect(localState.openFiles.length).toBe(0); }); }); @@ -162,7 +162,7 @@ describe('IDE store file mutations', () => { callMutationForFile(localFile); - expect(localFile.raw).toBeFalsy(); + expect(localFile.raw).toEqual(''); expect(localState.stagedFiles[0].raw).toBe('testing'); }); @@ -172,7 +172,7 @@ describe('IDE store file mutations', () => { callMutationForFile(localFile); - expect(localFile.raw).toBeFalsy(); + expect(localFile.raw).toEqual(''); expect(localFile.content).toBe('testing'); }); @@ -202,7 +202,7 @@ describe('IDE store file mutations', () => { callMutationForFile(localFile); - expect(localFile.raw).toBeFalsy(); + expect(localFile.raw).toEqual(''); expect(localState.stagedFiles[0].raw).toBe('testing'); }); }); @@ -239,7 +239,7 @@ describe('IDE store file mutations', () => { }); expect(localFile.content).toBe('testing'); - expect(localFile.changed).toBeTruthy(); + expect(localFile.changed).toBe(true); }); it('sets changed if file is a temp file', () => { @@ -250,7 +250,7 @@ describe('IDE store file mutations', () => { content: '', }); - expect(localFile.changed).toBeTruthy(); + expect(localFile.changed).toBe(true); }); }); @@ -329,7 +329,7 @@ describe('IDE store file mutations', () => { mutations.DISCARD_FILE_CHANGES(localState, localFile.path); expect(localFile.content).toBe(''); - expect(localFile.changed).toBeFalsy(); + expect(localFile.changed).toBe(false); }); it('adds to root tree if deleted', () => { @@ -527,7 +527,7 @@ describe('IDE store file mutations', () => { changed: true, }); - expect(localFile.changed).toBeTruthy(); + expect(localFile.changed).toBe(true); }); }); diff --git a/spec/frontend/ide/stores/mutations/merge_request_spec.js b/spec/frontend/ide/stores/mutations/merge_request_spec.js index afbe6770c0d..2af06835181 100644 --- a/spec/frontend/ide/stores/mutations/merge_request_spec.js +++ b/spec/frontend/ide/stores/mutations/merge_request_spec.js @@ -30,7 +30,7 @@ describe('IDE store merge request mutations', () => { const newMr = localState.projects.abcproject.mergeRequests[1]; expect(newMr.title).toBe('mr'); - expect(newMr.active).toBeTruthy(); + expect(newMr.active).toBe(true); }); it('keeps original data', () => { diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index 2f8447af518..fd9d481251d 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -46,7 +46,7 @@ describe('WebIDE utils', () => { content: 'SELECT "éêė" from tablename', mimeType: 'application/sql', }), - ).toBeFalsy(); + ).toBe(false); }); it('returns true for ASCII only content for unknown types', () => { @@ -56,7 +56,7 @@ describe('WebIDE utils', () => { content: 'plain text', mimeType: 'application/x-new-type', }), - ).toBeTruthy(); + ).toBe(true); }); it('returns false for non-ASCII content for unknown types', () => { @@ -66,7 +66,7 @@ describe('WebIDE utils', () => { content: '{"éêė":"value"}', mimeType: 'application/octet-stream', }), - ).toBeFalsy(); + ).toBe(false); }); it.each` diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js index ee2f6541b03..5af0e272285 100644 --- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js +++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js @@ -204,7 +204,7 @@ describe('DynamicField', () => { }); expect(findGlFormGroup().find('small').html()).toContain( - '[<code>1</code> <a>3</a> <a target="_blank" href="foo">4</a>]', + '[<code>1</code> <a>3</a> <a href="foo">4</a>]', ); }); }); diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js index 6aa3e661677..fd60d7f817f 100644 --- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js +++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js @@ -15,6 +15,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue'; const mockOverrides = Array(DEFAULT_PER_PAGE * 3) .fill(1) .map((_, index) => ({ + id: index, name: `test-proj-${index}`, avatar_url: `avatar-${index}`, full_path: `test-proj-${index}`, @@ -59,6 +60,7 @@ describe('IntegrationOverrides', () => { const avatar = link.findComponent(ProjectAvatar); return { + id: avatar.props('projectId'), href: link.attributes('href'), avatarUrl: avatar.props('projectAvatarUrl'), avatarName: avatar.props('projectName'), @@ -90,7 +92,7 @@ describe('IntegrationOverrides', () => { const table = findGlTable(); expect(table.exists()).toBe(true); - expect(table.attributes('busy')).toBeFalsy(); + expect(table.attributes('busy')).toBeUndefined(); }); it('renders IntegrationTabs with count', async () => { @@ -109,6 +111,7 @@ describe('IntegrationOverrides', () => { it('renders overrides as rows in table', () => { expect(findRowsAsModel()).toEqual( mockOverrides.map((x) => ({ + id: x.id, href: x.full_path, avatarUrl: x.avatar_url, avatarName: x.name, diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 045a454e63a..2058784b033 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -1,4 +1,4 @@ -import { GlLink, GlModal, GlSprintf, GlFormGroup } from '@gitlab/ui'; +import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; @@ -18,6 +18,7 @@ import { MEMBERS_PLACEHOLDER_DISABLED, MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT, LEARN_GITLAB, + EXPANDED_ERRORS, } from '~/invite_members/constants'; import eventHub from '~/invite_members/event_hub'; import ContentTransition from '~/vue_shared/components/content_transition.vue'; @@ -36,6 +37,7 @@ import { user3, user4, user5, + user6, GlEmoji, } from '../mock_data/member_modal'; @@ -95,9 +97,12 @@ describe('InviteMembersModal', () => { const findBase = () => wrapper.findComponent(InviteModalBase); const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error'); + const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button'); + const findAccordion = () => wrapper.findComponent(GlCollapse); + const findErrorsIcon = () => wrapper.findComponent(GlIcon); const findMemberErrorMessage = (element) => - `${Object.keys(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]}: ${ - Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element] + `${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${ + Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element] }`; const emitEventFromModal = (eventName) => () => findModal().vm.$emit(eventName, { preventDefault: jest.fn() }); @@ -666,8 +671,8 @@ describe('InviteMembersModal', () => { it('displays errors for multiple and allows clearing', async () => { createInviteMembersToGroupWrapper(); - await triggerMembersTokenSelect([user3, user4, user5]); - mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED); + await triggerMembersTokenSelect([user3, user4, user5, user6]); + mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EXPANDED_RESTRICTED); clickInviteButton(); @@ -675,19 +680,44 @@ describe('InviteMembersModal', () => { expect(findMemberErrorAlert().exists()).toBe(true); expect(findMemberErrorAlert().props('title')).toContain( - "The following 3 members couldn't be invited", + "The following 4 members couldn't be invited", ); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(0)); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(1)); expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(2)); + expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(3)); + expect(findAccordion().exists()).toBe(true); + expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)'); + expect(findErrorsIcon().attributes('class')).not.toContain('gl-rotate-180'); + expect(findAccordion().attributes('visible')).toBeUndefined(); + + await findMoreInviteErrorsButton().vm.$emit('click'); + + expect(findMoreInviteErrorsButton().text()).toContain(EXPANDED_ERRORS); + expect(findErrorsIcon().attributes('class')).toContain('gl-rotate-180'); + expect(findAccordion().attributes('visible')).toBeDefined(); + + await findMoreInviteErrorsButton().vm.$emit('click'); + + expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)'); + expect(findAccordion().attributes('visible')).toBeUndefined(); await removeMembersToken(user3); + expect(findMoreInviteErrorsButton().text()).toContain('Show more (1)'); expect(findMemberErrorAlert().props('title')).toContain( - "The following 2 members couldn't be invited", + "The following 3 members couldn't be invited", ); expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0)); + await removeMembersToken(user6); + + expect(findMoreInviteErrorsButton().exists()).toBe(false); + expect(findMemberErrorAlert().props('title')).toContain( + "The following 2 members couldn't be invited", + ); + expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(2)); + await removeMembersToken(user4); expect(findMemberErrorAlert().props('title')).toContain( diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 6375d0f7e2e..0455460918c 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -5,6 +5,7 @@ import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import * as UserApi from '~/api/user_api'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; +import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants'; const label = 'testgroup'; const placeholder = 'Search for a member'; @@ -49,6 +50,39 @@ describe('MembersTokenSelect', () => { }); }); + describe('when there are invalidMembers', () => { + it('adds in the correct class values for the tokens', async () => { + const badToken = { ...user1, class: INVALID_TOKEN_BACKGROUND }; + const goodToken = { ...user2, class: VALID_TOKEN_BACKGROUND }; + + wrapper = createComponent(); + + findTokenSelector().vm.$emit('input', [user1, user2]); + + await waitForPromises(); + + expect(findTokenSelector().props('selectedTokens')).toEqual([user1, user2]); + + await wrapper.setProps({ invalidMembers: { one_1: 'bad stuff' } }); + + expect(findTokenSelector().props('selectedTokens')).toEqual([badToken, goodToken]); + }); + + it('does not change class when invalid members are cleared', async () => { + // arrange - invalidMembers is non-empty and then tokens are added + wrapper = createComponent(); + await wrapper.setProps({ invalidMembers: { one_1: 'bad stuff' } }); + findTokenSelector().vm.$emit('input', [user1, user2]); + await waitForPromises(); + + // act - invalidMembers clears out + await wrapper.setProps({ invalidMembers: {} }); + + // assert - we didn't try to update the tokens + expect(findTokenSelector().props('selectedTokens')).toEqual([user1, user2]); + }); + }); + describe('users', () => { beforeEach(() => { jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers }); diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js index bbc17932a49..543fc28a342 100644 --- a/spec/frontend/invite_members/components/user_limit_notification_spec.js +++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js @@ -9,6 +9,8 @@ import { import { freeUsersLimit, membersCount } from '../mock_data/member_modal'; +const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name'; + describe('UserLimitNotification', () => { let wrapper; @@ -33,7 +35,7 @@ describe('UserLimitNotification', () => { }, ...props, }, - provide: { name: 'my group' }, + provide: { name: 'name' }, stubs: { GlSprintf }, }); }; @@ -50,7 +52,7 @@ describe('UserLimitNotification', () => { }); }); - describe('when close to limit with a personal namepace', () => { + describe('when close to limit within a personal namepace', () => { beforeEach(() => { createComponent(true, false, { membersCount: 3, userNamespace: true }); }); @@ -58,27 +60,24 @@ describe('UserLimitNotification', () => { it('renders the limit for a personal namespace', () => { const alert = findAlert(); - expect(alert.attributes('title')).toEqual( - 'You only have space for 2 more members in your personal projects', - ); + expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE); + expect(alert.text()).toEqual( 'To make more space, you can remove members who no longer need access.', ); }); }); - describe('when close to limit', () => { + describe('when close to limit within a group', () => { it("renders user's limit notification", () => { createComponent(true, false, { membersCount: 3 }); const alert = findAlert(); - expect(alert.attributes('title')).toEqual( - 'You only have space for 2 more members in my group', - ); + expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE); expect(alert.text()).toEqual( - 'To get more members an owner of this namespace can start a trial or upgrade to a paid tier.', + 'To get more members an owner of the group can start a trial or upgrade to a paid tier.', ); }); }); @@ -89,7 +88,7 @@ describe('UserLimitNotification', () => { const alert = findAlert(); - expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for my group"); + expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name"); expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE); }); diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js index 4ad3b6aeb66..6fe06decb6b 100644 --- a/spec/frontend/invite_members/mock_data/api_responses.js +++ b/spec/frontend/invite_members/mock_data/api_responses.js @@ -26,6 +26,20 @@ const MULTIPLE_RESTRICTED = { status: 'error', }; +const EXPANDED_RESTRICTED = { + message: { + 'email@example.com': + "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.", + 'email4@example.com': + "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.", + 'email5@example.com': + "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.", + root: + "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.", + }, + status: 'error', +}; + const EMAIL_TAKEN = { message: { 'email@example.org': "The member's email address has already been taken", @@ -41,4 +55,5 @@ export const invitationsApiResponse = { EMAIL_RESTRICTED, MULTIPLE_RESTRICTED, EMAIL_TAKEN, + EXPANDED_RESTRICTED, }; diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js index 7d675b6206c..4f4e9345e46 100644 --- a/spec/frontend/invite_members/mock_data/member_modal.js +++ b/spec/frontend/invite_members/mock_data/member_modal.js @@ -39,5 +39,10 @@ export const user5 = { name: 'root', avatar_url: '', }; +export const user6 = { + id: 'user-defined-token3', + name: 'email5@example.com', + avatar_url: '', +}; export const GlEmoji = { template: '<img/>' }; diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js index 6b48f83041a..3f9f048605a 100644 --- a/spec/frontend/issuable/components/related_issuable_item_spec.js +++ b/spec/frontend/issuable/components/related_issuable_item_spec.js @@ -1,23 +1,25 @@ -import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { GlIcon, GlLink, GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; import IssueDueDate from '~/boards/components/issue_due_date.vue'; import { formatDate } from '~/lib/utils/datetime_utility'; +import { updateHistory } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue'; +import IssueMilestone from '~/issuable/components/issue_milestone.vue'; +import IssueAssignees from '~/issuable/components/issue_assignees.vue'; +import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data'; +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + updateHistory: jest.fn(), +})); + describe('RelatedIssuableItem', () => { let wrapper; - function mountComponent({ mountMethod = mount, stubs = {}, props = {}, slots = {} } = {}) { - wrapper = mountMethod(RelatedIssuableItem, { - propsData: props, - slots, - stubs, - }); - } - - const props = { + const defaultProps = { idKey: 1, displayReference: 'gitlab-org/gitlab-test#1', pathIdSeparator: '#', @@ -31,84 +33,94 @@ describe('RelatedIssuableItem', () => { assignees: defaultAssignees, eventNamespace: 'relatedIssue', }; - const slots = { - dueDate: '<div class="js-due-date-slot"></div>', - weight: '<div class="js-weight-slot"></div>', - }; - - const findRemoveButton = () => wrapper.find({ ref: 'removeButton' }); - const findLockIcon = () => wrapper.find({ ref: 'lockIcon' }); - beforeEach(() => { - mountComponent({ props, slots }); - }); + const findIcon = () => wrapper.findComponent(GlIcon); + const findIssueDueDate = () => wrapper.findComponent(IssueDueDate); + const findLockIcon = () => wrapper.find('[data-testid="lockIcon"]'); + const findRemoveButton = () => wrapper.findComponent(GlButton); + const findTitleLink = () => wrapper.findComponent(GlLink); + const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); + + function mountComponent({ data = {}, props = {} } = {}) { + wrapper = shallowMount(RelatedIssuableItem, { + propsData: { + ...defaultProps, + ...props, + }, + data() { + return data; + }, + }); + } afterEach(() => { wrapper.destroy(); }); it('contains issuable-info-container class when canReorder is false', () => { - expect(wrapper.props('canReorder')).toBe(false); - expect(wrapper.find('.issuable-info-container').exists()).toBe(true); + mountComponent({ props: { canReorder: false } }); + + expect(wrapper.classes('issuable-info-container')).toBe(true); }); it('does not render token state', () => { + mountComponent(); + expect(wrapper.find('.text-secondary svg').exists()).toBe(false); }); it('does not render remove button', () => { - expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false); + mountComponent(); + + expect(findRemoveButton().exists()).toBe(false); }); describe('token title', () => { + beforeEach(() => { + mountComponent(); + }); + it('links to computedPath', () => { - expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path')); + expect(findTitleLink().attributes('href')).toBe(defaultProps.path); }); it('renders confidential icon', () => { - expect(wrapper.find('.confidential-icon').exists()).toBe(true); + expect(findIcon().attributes('title')).toBe(__('Confidential')); }); it('renders title', () => { - expect(wrapper.find('.item-title a').text()).toEqual(props.title); + expect(findTitleLink().text()).toBe(defaultProps.title); }); }); describe('token state', () => { - const tokenState = () => wrapper.find({ ref: 'iconElementXL' }); - - beforeEach(() => { - wrapper.setProps({ state: 'opened' }); - }); - - it('renders if hasState', () => { - expect(tokenState().exists()).toBe(true); - }); - it('renders state title', () => { - const stateTitle = tokenState().attributes('title'); - const formattedCreateDate = formatDate(props.createdAt); + mountComponent({ props: { state: 'opened' } }); + const stateTitle = findIcon().attributes('title'); + const formattedCreateDate = formatDate(defaultProps.createdAt); expect(stateTitle).toContain('<span class="bold">Created</span>'); expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`); }); it('renders aria label', () => { - expect(tokenState().attributes('aria-label')).toEqual('opened'); + mountComponent({ props: { state: 'opened' } }); + + expect(findIcon().attributes('arialabel')).toBe('opened'); }); it('renders open icon when open state', () => { - expect(tokenState().classes('issue-token-state-icon-open')).toBe(true); + mountComponent({ props: { state: 'opened' } }); + + expect(findIcon().props('name')).toBe('issue-open-m'); + expect(findIcon().classes('issue-token-state-icon-open')).toBe(true); }); - it('renders close icon when close state', async () => { - wrapper.setProps({ - state: 'closed', - closedAt: '2018-12-01T00:00:00.00Z', - }); - await nextTick(); + it('renders close icon when close state', () => { + mountComponent({ props: { state: 'closed', closedAt: '2018-12-01T00:00:00.00Z' } }); - expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true); + expect(findIcon().props('name')).toBe('issue-close'); + expect(findIcon().classes('issue-token-state-icon-closed')).toBe(true); }); }); @@ -116,75 +128,66 @@ describe('RelatedIssuableItem', () => { const tokenMetadata = () => wrapper.find('.item-meta'); it('renders item path and ID', () => { + mountComponent(); const pathAndID = tokenMetadata().find('.item-path-id').text(); expect(pathAndID).toContain('gitlab-org/gitlab-test'); expect(pathAndID).toContain('#1'); }); - it('renders milestone icon and name', () => { - const milestoneIcon = tokenMetadata().find('.item-milestone svg'); - const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title'); + it('renders milestone', () => { + mountComponent(); - expect(milestoneIcon.attributes('data-testid')).toBe('clock-icon'); - expect(milestoneTitle.text()).toContain('Milestone title'); + expect(wrapper.findComponent(IssueMilestone).props('milestone')).toEqual( + defaultProps.milestone, + ); }); it('renders due date component with correct due date', () => { - expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate); + mountComponent(); + + expect(findIssueDueDate().props('date')).toBe(defaultProps.dueDate); }); - it('does not render red icon for overdue issue that is closed', async () => { - mountComponent({ - props: { - ...props, - closedAt: '2018-12-01T00:00:00.00Z', - }, - }); - await nextTick(); + it('does not render red icon for overdue issue that is closed', () => { + mountComponent({ props: { closedAt: '2018-12-01T00:00:00.00Z' } }); - expect(wrapper.find(IssueDueDate).props('closed')).toBe(true); + expect(findIssueDueDate().props('closed')).toBe(true); }); }); describe('token assignees', () => { it('renders assignees avatars', () => { - // Expect 2 times 2 because assignees are rendered twice, due to layout issues - expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBeDefined(); + mountComponent(); - expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2'); + expect(wrapper.findComponent(IssueAssignees).props('assignees')).toEqual( + defaultProps.assignees, + ); }); }); describe('remove button', () => { beforeEach(() => { - wrapper.setProps({ canRemove: true }); + mountComponent({ props: { canRemove: true }, data: { removeDisabled: true } }); }); it('renders if canRemove', () => { - expect(findRemoveButton().exists()).toBe(true); + expect(findRemoveButton().props('icon')).toBe('close'); + expect(findRemoveButton().attributes('aria-label')).toBe(__('Remove')); }); it('does not render the lock icon', () => { expect(findLockIcon().exists()).toBe(false); }); - it('renders disabled button when removeDisabled', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ removeDisabled: true }); - await nextTick(); - - expect(findRemoveButton().attributes('disabled')).toEqual('disabled'); + it('renders disabled button when removeDisabled', () => { + expect(findRemoveButton().attributes('disabled')).toBe('true'); }); - it('triggers onRemoveRequest when clicked', async () => { - findRemoveButton().trigger('click'); - await nextTick(); - const { relatedIssueRemoveRequest } = wrapper.emitted(); + it('triggers onRemoveRequest when clicked', () => { + findRemoveButton().vm.$emit('click'); - expect(relatedIssueRemoveRequest.length).toBe(1); - expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]); + expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[defaultProps.idKey]]); }); }); @@ -192,10 +195,7 @@ describe('RelatedIssuableItem', () => { const lockedMessage = 'Issues created from a vulnerability cannot be removed'; beforeEach(() => { - wrapper.setProps({ - isLocked: true, - lockedMessage, - }); + mountComponent({ props: { isLocked: true, lockedMessage } }); }); it('does not render the remove button', () => { @@ -206,4 +206,67 @@ describe('RelatedIssuableItem', () => { expect(findLockIcon().attributes('title')).toBe(lockedMessage); }); }); + + describe('work item modal', () => { + const workItem = 'gid://gitlab/WorkItem/1'; + + it('renders', () => { + mountComponent(); + + expect(findWorkItemDetailModal().props('workItemId')).toBe(workItem); + }); + + describe('when work item is issue and the related issue title is clicked', () => { + it('does not open', () => { + mountComponent({ props: { workItemType: 'ISSUE' } }); + wrapper.vm.$refs.modal.show = jest.fn(); + + findTitleLink().vm.$emit('click', { preventDefault: () => {} }); + + expect(wrapper.vm.$refs.modal.show).not.toHaveBeenCalled(); + }); + }); + + describe('when work item is task and the related issue title is clicked', () => { + beforeEach(() => { + mountComponent({ props: { workItemType: 'TASK' } }); + wrapper.vm.$refs.modal.show = jest.fn(); + findTitleLink().vm.$emit('click', { preventDefault: () => {} }); + }); + + it('opens', () => { + expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled(); + }); + + it('updates the url params with the work item id', () => { + expect(updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?work_item_id=1`, + replace: true, + }); + }); + }); + + describe('when it emits "workItemDeleted" event', () => { + it('emits "relatedIssueRemoveRequest" event', () => { + mountComponent(); + + findWorkItemDetailModal().vm.$emit('workItemDeleted', workItem); + + expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[workItem]]); + }); + }); + + describe('when it emits "close" event', () => { + it('removes the work item id from the url params', () => { + mountComponent(); + + findWorkItemDetailModal().vm.$emit('close'); + + expect(updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/`, + replace: true, + }); + }); + }); + }); }); diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js index 3e77e750f3a..444165f61c7 100644 --- a/spec/frontend/issuable/popover/components/issue_popover_spec.js +++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js @@ -1,33 +1,23 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlIcon, GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import issueQueryResponse from 'test_fixtures/graphql/issuable/popover/queries/issue.query.graphql.json'; +import issueQuery from 'ee_else_ce/issuable/popover/queries/issue.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; +import IssueMilestone from '~/issuable/components/issue_milestone.vue'; import StatusBox from '~/issuable/components/status_box.vue'; import IssuePopover from '~/issuable/popover/components/issue_popover.vue'; -import issueQuery from '~/issuable/popover/queries/issue.query.graphql'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; describe('Issue Popover', () => { let wrapper; Vue.use(VueApollo); - const issueQueryResponse = { - data: { - project: { - __typename: 'Project', - id: '1', - issue: { - __typename: 'Issue', - id: 'gid://gitlab/Issue/1', - createdAt: '2020-07-01T04:08:01Z', - state: 'opened', - title: 'Issue title', - }, - }, - }, - }; + const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon); const mountComponent = ({ queryResponse = jest.fn().mockResolvedValue(issueQueryResponse), @@ -53,6 +43,12 @@ describe('Issue Popover', () => { expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); }); + it('should not show any work item icon while apollo is loading', () => { + mountComponent(); + + expect(findWorkItemIcon().exists()).toBe(false); + }); + describe('when loaded', () => { beforeEach(() => { mountComponent(); @@ -74,8 +70,40 @@ describe('Issue Popover', () => { expect(wrapper.find('h5').text()).toBe(issueQueryResponse.data.project.issue.title); }); + it('shows the work type icon', () => { + expect(findWorkItemIcon().props('workItemType')).toBe( + issueQueryResponse.data.project.issue.type, + ); + }); + it('shows reference', () => { expect(wrapper.text()).toContain('foo/bar#1'); }); + + it('shows confidential icon', () => { + const icon = wrapper.findComponent(GlIcon); + + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('eye-slash'); + }); + + it('shows due date', () => { + const component = wrapper.findComponent(IssueDueDate); + + expect(component.exists()).toBe(true); + expect(component.props('date')).toBe('2020-07-05'); + expect(component.props('closed')).toBe(false); + }); + + it('shows milestone', () => { + const component = wrapper.findComponent(IssueMilestone); + + expect(component.exists()).toBe(true); + expect(component.props('milestone')).toMatchObject({ + title: '15.2', + startDate: '2020-07-01', + dueDate: '2020-07-30', + }); + }); }); }); diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js index ce98a16dbb7..16d4459f597 100644 --- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js +++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js @@ -157,8 +157,8 @@ describe('AddIssuableForm', () => { describe('categorized issuables', () => { it.each` issuableType | pathIdSeparator | contextHeader | contextFooter - ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issue(s)'} - ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epic(s)'} + ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'} + ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'} `( 'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType', ({ issuableType, contextHeader, contextFooter }) => { diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js index 7a350df0ba6..772cc75a205 100644 --- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js +++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js @@ -1,5 +1,6 @@ -import { GlButton, GlIcon } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { GlIcon } from '@gitlab/ui'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { issuable1, issuable2, @@ -17,7 +18,9 @@ import { describe('RelatedIssuesBlock', () => { let wrapper; - const findIssueCountBadgeAddButton = () => wrapper.find(GlButton); + const findToggleButton = () => wrapper.findByTestId('toggle-links'); + const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body'); + const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button'); afterEach(() => { if (wrapper) { @@ -28,7 +31,7 @@ describe('RelatedIssuesBlock', () => { describe('with defaults', () => { beforeEach(() => { - wrapper = mount(RelatedIssuesBlock, { + wrapper = mountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, issuableType: issuableTypesMap.ISSUE, @@ -37,13 +40,13 @@ describe('RelatedIssuesBlock', () => { }); it.each` - issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText - ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked issues'} | ${'Read more about related issues'} | ${'Add a related issue'} - ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'} + issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText + ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Read more about related issues'} | ${'Add a related issue'} + ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'} `( 'displays "$titleText" in the header, "$helpLinkText" aria-label for help link, and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"', ({ issuableType, pathIdSeparator, titleText, helpLinkText, addButtonText }) => { - wrapper = mount(RelatedIssuesBlock, { + wrapper = mountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator, issuableType, @@ -73,7 +76,7 @@ describe('RelatedIssuesBlock', () => { it('displays header text slot data', () => { const headerText = '<div>custom header text</div>'; - wrapper = shallowMount(RelatedIssuesBlock, { + wrapper = shallowMountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, issuableType: 'issue', @@ -89,7 +92,7 @@ describe('RelatedIssuesBlock', () => { it('displays header actions slot data', () => { const headerActions = '<button data-testid="custom-button">custom button</button>'; - wrapper = shallowMount(RelatedIssuesBlock, { + wrapper = shallowMountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, issuableType: 'issue', @@ -103,7 +106,7 @@ describe('RelatedIssuesBlock', () => { describe('with isFetching=true', () => { beforeEach(() => { - wrapper = mount(RelatedIssuesBlock, { + wrapper = mountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, isFetching: true, @@ -119,7 +122,7 @@ describe('RelatedIssuesBlock', () => { describe('with canAddRelatedIssues=true', () => { beforeEach(() => { - wrapper = mount(RelatedIssuesBlock, { + wrapper = mountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, canAdmin: true, @@ -135,7 +138,7 @@ describe('RelatedIssuesBlock', () => { describe('with isFormVisible=true', () => { beforeEach(() => { - wrapper = mount(RelatedIssuesBlock, { + wrapper = mountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, isFormVisible: true, @@ -159,7 +162,7 @@ describe('RelatedIssuesBlock', () => { const categorizedHeadings = () => wrapper.findAll('h4'); const headingTextAt = (index) => categorizedHeadings().at(index).text(); const mountComponent = (showCategorizedIssues) => { - wrapper = mount(RelatedIssuesBlock, { + wrapper = mountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, relatedIssues: [issuable1, issuable2, issuable3], @@ -217,7 +220,7 @@ describe('RelatedIssuesBlock', () => { }, ].forEach(({ issuableType, icon }) => { it(`issuableType=${issuableType} is passed`, () => { - wrapper = shallowMount(RelatedIssuesBlock, { + wrapper = shallowMountExtended(RelatedIssuesBlock, { propsData: { pathIdSeparator: PathIdSeparator.Issue, issuableType, @@ -230,4 +233,42 @@ describe('RelatedIssuesBlock', () => { }); }); }); + + describe('toggle', () => { + beforeEach(() => { + wrapper = shallowMountExtended(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + relatedIssues: [issuable1, issuable2, issuable3], + issuableType: issuableTypesMap.ISSUE, + }, + }); + }); + + it('is expanded by default', () => { + expect(findToggleButton().props('icon')).toBe('chevron-lg-up'); + expect(findToggleButton().props('disabled')).toBe(false); + expect(findRelatedIssuesBody().exists()).toBe(true); + }); + + it('expands on click toggle button', async () => { + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findToggleButton().props('icon')).toBe('chevron-lg-down'); + expect(findRelatedIssuesBody().exists()).toBe(false); + }); + }); + + it('toggle button is disabled when issue has no related items', () => { + wrapper = shallowMountExtended(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + relatedIssues: [], + issuableType: 'issue', + }, + }); + + expect(findToggleButton().props('disabled')).toBe(true); + }); }); diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js index 1a03ea58b60..b518d2fbdec 100644 --- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js +++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js @@ -1,4 +1,4 @@ -import { mount, shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; @@ -9,8 +9,9 @@ import { } from 'jest/issuable/components/related_issuable_mock_data'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue'; import { linkedIssueTypesMap } from '~/related_issues/constants'; +import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue'; +import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue'; import relatedIssuesService from '~/related_issues/services/related_issues_service'; jest.mock('~/flash'); @@ -19,6 +20,8 @@ describe('RelatedIssuesRoot', () => { let wrapper; let mock; + const findRelatedIssuesBlock = () => wrapper.findComponent(RelatedIssuesBlock); + beforeEach(() => { mock = new MockAdapter(axios); mock.onGet(defaultProps.endpoint).reply(200, []); @@ -26,100 +29,114 @@ describe('RelatedIssuesRoot', () => { afterEach(() => { mock.restore(); - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } + wrapper.destroy(); }); - const createComponent = (mountFn = mount) => { - wrapper = mountFn(RelatedIssuesRoot, { - propsData: defaultProps, + const createComponent = ({ props = {}, data = {} } = {}) => { + wrapper = mount(RelatedIssuesRoot, { + propsData: { + ...defaultProps, + ...props, + }, + data() { + return data; + }, }); // Wait for fetch request `fetchRelatedIssues` to complete before starting to test return waitForPromises(); }; - describe('methods', () => { - describe('onRelatedIssueRemoveRequest', () => { - beforeEach(() => { - jest - .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues') - .mockReturnValue(Promise.reject()); - - return createComponent().then(() => { + describe('events', () => { + describe('when "relatedIssueRemoveRequest" event is emitted', () => { + describe('when emitted value is a numerical issue', () => { + beforeEach(async () => { + jest + .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues') + .mockReturnValue(Promise.reject()); + await createComponent(); wrapper.vm.store.setRelatedIssues([issuable1]); }); - }); - it('remove related issue and succeeds', () => { - mock.onDelete(issuable1.referencePath).reply(200, { issues: [] }); + it('removes related issue on API success', async () => { + mock.onDelete(issuable1.referencePath).reply(200, { issues: [] }); + + findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id); + await axios.waitForAll(); + + expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]); + }); + + it('does not remove related issue on API error', async () => { + mock.onDelete(issuable1.referencePath).reply(422, {}); - wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id); + findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id); + await axios.waitForAll(); - return axios.waitForAll().then(() => { - expect(wrapper.vm.state.relatedIssues).toEqual([]); + expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([ + expect.objectContaining({ id: issuable1.id }), + ]); }); }); - it('remove related issue, fails, and restores to related issues', () => { - mock.onDelete(issuable1.referencePath).reply(422, {}); + describe('when emitted value is a work item id', () => { + it('removes related issue', async () => { + const workItem = `gid://gitlab/WorkItem/${issuable1.id}`; + createComponent({ data: { state: { relatedIssues: [issuable1] } } }); - wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id); + findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem); + await nextTick(); - return axios.waitForAll().then(() => { - expect(wrapper.vm.state.relatedIssues).toHaveLength(1); - expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); + expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]); }); }); }); - describe('onToggleAddRelatedIssuesForm', () => { - beforeEach(() => createComponent(shallowMount)); + describe('when "toggleAddRelatedIssuesForm" event is emitted', () => { + it('toggles related issues form to visible from hidden', async () => { + createComponent(); - it('toggle related issues form to visible', () => { - wrapper.vm.onToggleAddRelatedIssuesForm(); + findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm'); + await nextTick(); - expect(wrapper.vm.isFormVisible).toEqual(true); + expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(true); }); - it('show add related issues form to hidden', () => { - wrapper.vm.isFormVisible = true; + it('toggles related issues form to hidden from visible', async () => { + createComponent({ data: { isFormVisible: true } }); - wrapper.vm.onToggleAddRelatedIssuesForm(); + findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm'); + await nextTick(); - expect(wrapper.vm.isFormVisible).toEqual(false); + expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false); }); }); - describe('onPendingIssueRemoveRequest', () => { - beforeEach(() => - createComponent().then(() => { - wrapper.vm.store.setPendingReferences([issuable1.reference]); - }), - ); + describe('when "pendingIssuableRemoveRequest" event is emitted', () => { + beforeEach(() => { + createComponent(); + wrapper.vm.store.setPendingReferences([issuable1.reference]); + }); - it('remove pending related issue', () => { - expect(wrapper.vm.state.pendingReferences).toHaveLength(1); + it('removes pending related issue', async () => { + expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(1); - wrapper.vm.onPendingIssueRemoveRequest(0); + findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0); + await nextTick(); - expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0); }); }); - describe('onPendingFormSubmit', () => { - beforeEach(() => { + describe('when "addIssuableFormSubmit" event is emitted', () => { + beforeEach(async () => { jest .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues') .mockReturnValue(Promise.reject()); - - return createComponent().then(() => { - jest.spyOn(wrapper.vm, 'processAllReferences'); - jest.spyOn(wrapper.vm.service, 'addRelatedIssues'); - createFlash.mockClear(); - }); + await createComponent(); + jest.spyOn(wrapper.vm, 'processAllReferences'); + jest.spyOn(wrapper.vm.service, 'addRelatedIssues'); + createFlash.mockClear(); }); it('processes references before submitting', () => { @@ -130,23 +147,22 @@ describe('RelatedIssuesRoot', () => { linkedIssueType, }; - wrapper.vm.onPendingFormSubmit(emitObj); + findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj); expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input); expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType); }); - it('submit zero pending issue as related issue', () => { + it('submits zero pending issues as related issue', () => { wrapper.vm.store.setPendingReferences([]); - wrapper.vm.onPendingFormSubmit({}); - return waitForPromises().then(() => { - expect(wrapper.vm.state.pendingReferences).toHaveLength(0); - expect(wrapper.vm.state.relatedIssues).toHaveLength(0); - }); + findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {}); + + expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0); + expect(findRelatedIssuesBlock().props('relatedIssues')).toHaveLength(0); }); - it('submit pending issue as related issue', () => { + it('submits pending issue as related issue', async () => { mock.onPost(defaultProps.endpoint).reply(200, { issuables: [issuable1], result: { @@ -154,18 +170,18 @@ describe('RelatedIssuesRoot', () => { status: 'success', }, }); - wrapper.vm.store.setPendingReferences([issuable1.reference]); - wrapper.vm.onPendingFormSubmit({}); - return waitForPromises().then(() => { - expect(wrapper.vm.state.pendingReferences).toHaveLength(0); - expect(wrapper.vm.state.relatedIssues).toHaveLength(1); - expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); - }); + findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {}); + await waitForPromises(); + + expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0); + expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([ + expect.objectContaining({ id: issuable1.id }), + ]); }); - it('submit multiple pending issues as related issues', () => { + it('submits multiple pending issues as related issues', async () => { mock.onPost(defaultProps.endpoint).reply(200, { issuables: [issuable1, issuable2], result: { @@ -173,201 +189,148 @@ describe('RelatedIssuesRoot', () => { status: 'success', }, }); - wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]); - wrapper.vm.onPendingFormSubmit({}); - return waitForPromises().then(() => { - expect(wrapper.vm.state.pendingReferences).toHaveLength(0); - expect(wrapper.vm.state.relatedIssues).toHaveLength(2); - expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); - expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id); - }); + findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {}); + await waitForPromises(); + + expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0); + expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([ + expect.objectContaining({ id: issuable1.id }), + expect.objectContaining({ id: issuable2.id }), + ]); }); - it('displays a message from the backend upon error', () => { + it('displays a message from the backend upon error', async () => { const input = '#123'; const message = 'error'; - mock.onPost(defaultProps.endpoint).reply(409, { message }); wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]); expect(createFlash).not.toHaveBeenCalled(); - wrapper.vm.onPendingFormSubmit(input); - - return waitForPromises().then(() => { - expect(createFlash).toHaveBeenCalledWith({ - message, - }); - }); - }); - }); - describe('onPendingFormCancel', () => { - beforeEach(() => - createComponent().then(() => { - wrapper.vm.isFormVisible = true; - wrapper.vm.inputValue = 'foo'; - }), - ); - - it('when canceling and hiding add issuable form', async () => { - wrapper.vm.onPendingFormCancel(); + findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input); + await waitForPromises(); - await nextTick(); - expect(wrapper.vm.isFormVisible).toEqual(false); - expect(wrapper.vm.inputValue).toEqual(''); - expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + expect(createFlash).toHaveBeenCalledWith({ message }); }); }); - describe('fetchRelatedIssues', () => { - beforeEach(() => createComponent()); - - it('sets isFetching while fetching', async () => { - wrapper.vm.fetchRelatedIssues(); + describe('when "addIssuableFormCancel" event is emitted', () => { + beforeEach(() => createComponent({ data: { isFormVisible: true, inputValue: 'foo' } })); - expect(wrapper.vm.isFetching).toEqual(true); + it('hides form and resets input', async () => { + findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel'); + await nextTick(); - await waitForPromises(); - expect(wrapper.vm.isFetching).toEqual(false); - }); - - it('should fetch related issues', async () => { - mock.onGet(defaultProps.endpoint).reply(200, [issuable1, issuable2]); - - wrapper.vm.fetchRelatedIssues(); - - await waitForPromises(); - expect(wrapper.vm.state.relatedIssues).toHaveLength(2); - expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); - expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id); + expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false); + expect(findRelatedIssuesBlock().props('inputValue')).toBe(''); + expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0); }); }); - describe('onInput', () => { - beforeEach(() => createComponent()); - - it('fill in issue number reference and adds to pending related issues', () => { + describe('when "addIssuableFormInput" event is emitted', () => { + it('updates pending references with issue reference', async () => { const input = '#123 '; - wrapper.vm.onInput({ + createComponent(); + + findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', { untouchedRawReferences: [input.trim()], touchedReference: input, }); + await nextTick(); - expect(wrapper.vm.state.pendingReferences).toHaveLength(1); - expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123'); + expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]); }); - it('fill in with full reference', () => { + it('updates pending references with full reference', async () => { const input = 'asdf/qwer#444 '; - wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input }); + createComponent(); + + findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', { + untouchedRawReferences: [input.trim()], + touchedReference: input, + }); + await nextTick(); - expect(wrapper.vm.state.pendingReferences).toHaveLength(1); - expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444'); + expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]); }); - it('fill in with issue link', () => { + it('updates pending references with issue link', async () => { const link = 'http://localhost:3000/foo/bar/issues/111'; const input = `${link} `; - wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input }); + createComponent(); - expect(wrapper.vm.state.pendingReferences).toHaveLength(1); - expect(wrapper.vm.state.pendingReferences[0]).toEqual(link); + findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', { + untouchedRawReferences: [input.trim()], + touchedReference: input, + }); + await nextTick(); + + expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([link]); }); - it('fill in with multiple references', () => { + it('updates pending references with multiple references', async () => { const input = 'asdf/qwer#444 #12 '; - wrapper.vm.onInput({ + createComponent(); + + findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', { untouchedRawReferences: input.trim().split(/\s/), touchedReference: '2', }); + await nextTick(); - expect(wrapper.vm.state.pendingReferences).toHaveLength(2); - expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444'); - expect(wrapper.vm.state.pendingReferences[1]).toEqual('#12'); + expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([ + 'asdf/qwer#444', + '#12', + ]); }); - it('fill in with some invalid things', () => { + it('updates pending references with invalid values', async () => { const input = 'something random '; - wrapper.vm.onInput({ + createComponent(); + + findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', { untouchedRawReferences: input.trim().split(/\s/), touchedReference: '2', }); + await nextTick(); - expect(wrapper.vm.state.pendingReferences).toHaveLength(2); - expect(wrapper.vm.state.pendingReferences[0]).toEqual('something'); - expect(wrapper.vm.state.pendingReferences[1]).toEqual('random'); + expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([ + 'something', + 'random', + ]); }); - it.each` - pathIdSeparator - ${'#'} - ${'&'} - `( - 'prepends $pathIdSeparator when user enters a numeric value [0-9]', - async ({ pathIdSeparator }) => { + it.each(['#', '&'])( + 'prepends %s when user enters a numeric value [0-9]', + async (pathIdSeparator) => { const input = '23'; + createComponent({ props: { pathIdSeparator } }); - await wrapper.setProps({ - pathIdSeparator, - }); - - wrapper.vm.onInput({ + findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', { untouchedRawReferences: input.trim().split(/\s/), touchedReference: input, }); + await nextTick(); - expect(wrapper.vm.inputValue).toBe(`${pathIdSeparator}${input}`); + expect(findRelatedIssuesBlock().props('inputValue')).toBe(`${pathIdSeparator}${input}`); }, ); - - it('prepends # when user enters a number', async () => { - const input = 23; - - wrapper.vm.onInput({ - untouchedRawReferences: String(input).trim().split(/\s/), - touchedReference: input, - }); - - expect(wrapper.vm.inputValue).toBe(`#${input}`); - }); }); - describe('onBlur', () => { - beforeEach(() => - createComponent().then(() => { - jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {}); - }), - ); - - it('add any references to pending when blurring', () => { - const input = '#123'; - - wrapper.vm.onBlur(input); - - expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input); + describe('when "addIssuableFormBlur" event is emitted', () => { + beforeEach(() => { + createComponent(); + jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {}); }); - }); - - describe('processAllReferences', () => { - beforeEach(() => createComponent()); - it('add valid reference to pending', () => { + it('adds any references to pending when blurring', () => { const input = '#123'; - wrapper.vm.processAllReferences(input); - expect(wrapper.vm.state.pendingReferences).toHaveLength(1); - expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123'); - }); + findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input); - it('add any valid references to pending', () => { - const input = 'asdf #123'; - wrapper.vm.processAllReferences(input); - - expect(wrapper.vm.state.pendingReferences).toHaveLength(2); - expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf'); - expect(wrapper.vm.state.pendingReferences[1]).toEqual('#123'); + expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input); }); }); }); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 3d3dbfa6853..a39853fd29c 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -52,6 +52,12 @@ import { getSortKey, getSortOptions } from '~/issues/list/utils'; import axios from '~/lib/utils/axios_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; import { joinPaths } from '~/lib/utils/url_utility'; +import { + WORK_ITEM_TYPE_ENUM_INCIDENT, + WORK_ITEM_TYPE_ENUM_ISSUE, + WORK_ITEM_TYPE_ENUM_TASK, + WORK_ITEM_TYPE_ENUM_TEST_CASE, +} from '~/work_items/constants'; jest.mock('@sentry/browser'); jest.mock('~/flash'); @@ -123,6 +129,7 @@ describe('CE IssuesListApp component', () => { const mountComponent = ({ provide = {}, data = {}, + workItems = false, issuesQueryResponse = mockIssuesQueryResponse, issuesCountsQueryResponse = mockIssuesCountsQueryResponse, sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse), @@ -141,6 +148,9 @@ describe('CE IssuesListApp component', () => { apolloProvider: createMockApollo(requestHandlers), router, provide: { + glFeatures: { + workItems, + }, ...defaultProvide, ...provide, }, @@ -168,22 +178,6 @@ describe('CE IssuesListApp component', () => { return waitForPromises(); }); - it('queries list with types `ISSUE` and `INCIDENT', () => { - const expectedTypes = ['ISSUE', 'INCIDENT', 'TEST_CASE']; - - expect(mockIssuesQueryResponse).toHaveBeenCalledWith( - expect.objectContaining({ - types: expectedTypes, - }), - ); - - expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith( - expect.objectContaining({ - types: expectedTypes, - }), - ); - }); - it('renders', () => { expect(findIssuableList().props()).toMatchObject({ namespace: defaultProvide.fullPath, @@ -1024,6 +1018,21 @@ describe('CE IssuesListApp component', () => { }); }); }); + + describe('when "page-size-change" event is emitted by IssuableList', () => { + it('updates url params with new page size', async () => { + wrapper = mountComponent(); + router.push = jest.fn(); + + findIssuableList().vm.$emit('page-size-change', 50); + await nextTick(); + + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith({ + query: expect.objectContaining({ first_page_size: 50 }), + }); + }); + }); }); describe('public visibility', () => { @@ -1045,17 +1054,45 @@ describe('CE IssuesListApp component', () => { }); }); - describe('when "page-size-change" event is emitted by IssuableList', () => { - it('updates url params with new page size', async () => { - wrapper = mountComponent(); - router.push = jest.fn(); + describe('fetching issues', () => { + describe('when work_items feature flag is disabled', () => { + beforeEach(() => { + wrapper = mountComponent({ workItems: false }); + jest.runOnlyPendingTimers(); + }); - findIssuableList().vm.$emit('page-size-change', 50); - await nextTick(); + it('fetches issue, incident, and test case types', () => { + const types = [ + WORK_ITEM_TYPE_ENUM_ISSUE, + WORK_ITEM_TYPE_ENUM_INCIDENT, + WORK_ITEM_TYPE_ENUM_TEST_CASE, + ]; - expect(router.push).toHaveBeenCalledTimes(1); - expect(router.push).toHaveBeenCalledWith({ - query: expect.objectContaining({ first_page_size: 50 }), + expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types })); + expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith( + expect.objectContaining({ types }), + ); + }); + }); + + describe('when work_items feature flag is enabled', () => { + beforeEach(() => { + wrapper = mountComponent({ workItems: true }); + jest.runOnlyPendingTimers(); + }); + + it('fetches issue, incident, test case, and task types', () => { + const types = [ + WORK_ITEM_TYPE_ENUM_ISSUE, + WORK_ITEM_TYPE_ENUM_INCIDENT, + WORK_ITEM_TYPE_ENUM_TEST_CASE, + WORK_ITEM_TYPE_ENUM_TASK, + ]; + + expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types })); + expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith( + expect.objectContaining({ types }), + ); }); }); }); diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index 4347c580a4d..42e9d348b16 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -37,6 +37,7 @@ export const getIssuesQueryResponse = { userDiscussionsCount: 4, webPath: 'project/-/issues/789', webUrl: 'project/-/issues/789', + type: 'issue', assignees: { nodes: [ { diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 27604b8ccf3..12f9707da04 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -119,7 +119,7 @@ describe('Issuable output', () => { expect(findEdited().exists()).toBe(true); expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); - expect(findEdited().props('updatedAt')).toBeTruthy(); + expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at); expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version); }) .then(() => { @@ -133,7 +133,7 @@ describe('Issuable output', () => { expect(findEdited().exists()).toBe(true); expect(findEdited().props('updatedByName')).toBe('Other User'); expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); - expect(findEdited().props('updatedAt')).toBeTruthy(); + expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at); }); }); diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index 8ee57f97754..bdb1448148e 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -249,7 +249,7 @@ describe('Description component', () => { await nextTick(); expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe( - '1/1 task', + '1/1 checklist item', ); }); @@ -266,7 +266,7 @@ describe('Description component', () => { }); }); - describe('with work items feature flag is enabled', () => { + describe('with work_items_create_from_markdown feature flag enabled', () => { describe('empty description', () => { beforeEach(() => { createComponent({ @@ -275,7 +275,7 @@ describe('Description component', () => { }, provide: { glFeatures: { - workItems: true, + workItemsCreateFromMarkdown: true, }, }, }); @@ -295,7 +295,7 @@ describe('Description component', () => { }, provide: { glFeatures: { - workItems: true, + workItemsCreateFromMarkdown: true, }, }, }); @@ -344,7 +344,7 @@ describe('Description component', () => { descriptionHtml: descriptionHtmlWithTask, }, provide: { - glFeatures: { workItems: true }, + glFeatures: { workItemsCreateFromMarkdown: true }, }, }); return nextTick(); @@ -406,7 +406,7 @@ describe('Description component', () => { createComponent({ props: { descriptionHtml: descriptionHtmlWithTask }, - provide: { glFeatures: { workItems: true } }, + provide: { glFeatures: { workItemsCreateFromMarkdown: true } }, }); expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened); @@ -422,7 +422,7 @@ describe('Description component', () => { descriptionHtml: descriptionHtmlWithTask, }, provide: { - glFeatures: { workItems: true }, + glFeatures: { workItemsCreateFromMarkdown: true }, }, }); return nextTick(); diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js index 79368023d76..d58bf1be812 100644 --- a/spec/frontend/issues/show/components/edit_actions_spec.js +++ b/spec/frontend/issues/show/components/edit_actions_spec.js @@ -75,7 +75,7 @@ describe('Edit Actions component', () => { it('renders all buttons as enabled', () => { const buttons = findEditButtons().wrappers; buttons.forEach((button) => { - expect(button.attributes('disabled')).toBeFalsy(); + expect(button.attributes('disabled')).toBeUndefined(); }); }); diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js new file mode 100644 index 00000000000..3ab2bb3460b --- /dev/null +++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js @@ -0,0 +1,189 @@ +import VueApollo from 'vue-apollo'; +import Vue from 'vue'; +import { GlDatepicker } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue'; +import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; +import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql'; +import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createAlert } from '~/flash'; +import { useFakeDate } from 'helpers/fake_date'; +import { + timelineEventsCreateEventResponse, + timelineEventsCreateEventError, + mockGetTimelineData, +} from './mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/flash'); + +const fakeDate = '2020-07-08T00:00:00.000Z'; + +const mockInputData = { + incidentId: 'gid://gitlab/Issue/1', + note: 'test', + occurredAt: '2020-07-08T00:00:00.000Z', +}; + +describe('Create Timeline events', () => { + useFakeDate(fakeDate); + let wrapper; + let responseSpy; + let apolloProvider; + + const findSubmitButton = () => wrapper.findByText(__('Save')); + const findSubmitAndAddButton = () => + wrapper.findByText(s__('Incident|Save and add another event')); + const findCancelButton = () => wrapper.findByText(__('Cancel')); + const findDatePicker = () => wrapper.findComponent(GlDatepicker); + const findNoteInput = () => wrapper.findByTestId('input-note'); + const setNoteInput = () => { + const textarea = findNoteInput().element; + textarea.value = mockInputData.note; + textarea.dispatchEvent(new Event('input')); + }; + const findHourInput = () => wrapper.findByTestId('input-hours'); + const findMinuteInput = () => wrapper.findByTestId('input-minutes'); + const setDatetime = () => { + const inputDate = new Date(mockInputData.occurredAt); + findDatePicker().vm.$emit('input', inputDate); + findHourInput().vm.$emit('input', inputDate.getHours()); + findMinuteInput().vm.$emit('input', inputDate.getMinutes()); + }; + const fillForm = () => { + setDatetime(); + setNoteInput(); + }; + + function createMockApolloProvider() { + const requestHandlers = [[createTimelineEventMutation, responseSpy]]; + const mockApollo = createMockApollo(requestHandlers); + + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getTimelineEvents, + data: mockGetTimelineData, + variables: { + fullPath: 'group/project', + incidentId: 'gid://gitlab/Issue/1', + }, + }); + + return mockApollo; + } + + const mountComponent = () => { + wrapper = mountExtended(CreateTimelineEvent, { + propsData: { + hasTimelineEvents: true, + }, + provide: { + fullPath: 'group/project', + issuableId: '1', + }, + apolloProvider, + }); + }; + + beforeEach(() => { + responseSpy = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse); + apolloProvider = createMockApolloProvider(); + }); + + afterEach(() => { + createAlert.mockReset(); + wrapper.destroy(); + }); + + describe('createIncidentTimelineEvent', () => { + const closeFormEvent = { 'hide-new-timeline-events-form': [[]] }; + + const expectedData = { + input: mockInputData, + }; + + beforeEach(() => { + mountComponent(); + fillForm(); + }); + + describe('on submit', () => { + beforeEach(async () => { + findSubmitButton().trigger('click'); + await waitForPromises(); + }); + + it('should call the mutation with the right variables', () => { + expect(responseSpy).toHaveBeenCalledWith(expectedData); + }); + + it('should close the form on successful addition', () => { + expect(wrapper.emitted()).toEqual(closeFormEvent); + }); + }); + + describe('on submit and add', () => { + beforeEach(async () => { + findSubmitAndAddButton().trigger('click'); + await waitForPromises(); + }); + + it('should keep the form open for save and add another', () => { + expect(wrapper.emitted()).toEqual({}); + }); + }); + + describe('on cancel', () => { + beforeEach(async () => { + findCancelButton().trigger('click'); + await waitForPromises(); + }); + + it('should close the form', () => { + expect(wrapper.emitted()).toEqual(closeFormEvent); + }); + }); + }); + + describe('error handling', () => { + it('should show an error when submission returns an error', async () => { + const expectedAlertArgs = { + message: `Error creating incident timeline event: ${timelineEventsCreateEventError.data.timelineEventCreate.errors[0]}`, + }; + responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError); + mountComponent(); + + findSubmitButton().trigger('click'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); + }); + + it('should show an error when submission fails', async () => { + const expectedAlertArgs = { + captureError: true, + error: new Error(), + message: 'Something went wrong while creating the incident timeline event.', + }; + responseSpy.mockRejectedValueOnce(); + mountComponent(); + + findSubmitButton().trigger('click'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); + }); + + it('should keep the form open on failed addition', async () => { + responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError); + mountComponent(); + + await wrapper.findComponent(TimelineEventsForm).vm.$emit('save-event', mockInputData); + await waitForPromises; + expect(wrapper.emitted()).toEqual({}); + }); + }); +}); diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js index afc6099caf4..75c0a7350ae 100644 --- a/spec/frontend/issues/show/components/incidents/mock_data.js +++ b/spec/frontend/issues/show/components/incidents/mock_data.js @@ -72,10 +72,14 @@ export const timelineEventsQueryEmptyResponse = { }; export const timelineEventsCreateEventResponse = { - timelineEvent: { - ...mockEvents[0], + data: { + timelineEventCreate: { + timelineEvent: { + ...mockEvents[0], + }, + errors: [], + }, }, - errors: [], }; export const timelineEventsCreateEventError = { @@ -103,3 +107,21 @@ const timelineEventDeleteData = (errors = []) => { export const timelineEventsDeleteEventResponse = timelineEventDeleteData(); export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']); + +export const mockGetTimelineData = { + project: { + id: 'gid://gitlab/Project/19', + incidentManagementTimelineEvents: { + nodes: [ + { + id: 'gid://gitlab/IncidentManagement::TimelineEvent/8', + note: 'another one2', + noteHtml: '<p>another one2</p>', + action: 'comment', + occurredAt: '2022-07-01T12:47:00Z', + createdAt: '2022-07-20T12:47:40Z', + }, + ], + }, + }, +}; diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js index 620cdfc53b0..cd2cbb63246 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js @@ -3,49 +3,33 @@ import Vue, { nextTick } from 'vue'; import { GlDatepicker } from '@gitlab/ui'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue'; -import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql'; -import createMockApollo from 'helpers/mock_apollo_helper'; +import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; import { createAlert } from '~/flash'; import { useFakeDate } from 'helpers/fake_date'; -import { timelineEventsCreateEventResponse, timelineEventsCreateEventError } from './mock_data'; Vue.use(VueApollo); jest.mock('~/flash'); -const addEventResponse = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse); - -function createMockApolloProvider(response = addEventResponse) { - const requestHandlers = [[createTimelineEventMutation, response]]; - return createMockApollo(requestHandlers); -} +const fakeDate = '2020-07-08T00:00:00.000Z'; describe('Timeline events form', () => { // July 8 2020 - useFakeDate(2020, 6, 8); + useFakeDate(fakeDate); let wrapper; - const mountComponent = ({ mockApollo, mountMethod = shallowMountExtended, stubs }) => { - wrapper = mountMethod(IncidentTimelineEventForm, { + const mountComponent = ({ mountMethod = shallowMountExtended }) => { + wrapper = mountMethod(TimelineEventsForm, { propsData: { hasTimelineEvents: true, + isEventProcessed: false, }, - provide: { - fullPath: 'group/project', - issuableId: '1', - }, - apolloProvider: mockApollo, - stubs, }); }; afterEach(() => { - addEventResponse.mockReset(); createAlert.mockReset(); - if (wrapper) { - wrapper.destroy(); - } + wrapper.destroy(); }); const findSubmitButton = () => wrapper.findByText('Save'); @@ -75,24 +59,28 @@ describe('Timeline events form', () => { }; describe('form button behaviour', () => { - const closeFormEvent = { 'hide-incident-timeline-event-form': [[]] }; beforeEach(() => { - mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended }); + mountComponent({ mountMethod: mountExtended }); }); - it('should close the form on submit', async () => { + it('should save event on submit', async () => { await submitForm(); - expect(wrapper.emitted()).toEqual(closeFormEvent); + + expect(wrapper.emitted()).toEqual({ + 'save-event': [[{ note: '', occurredAt: fakeDate }, false]], + }); }); - it('should not close the form on "submit and add another"', async () => { + it('should save event on "submit and add another"', async () => { await submitFormAndAddAnother(); - expect(wrapper.emitted()).toEqual({}); + expect(wrapper.emitted()).toEqual({ + 'save-event': [[{ note: '', occurredAt: fakeDate }, true]], + }); }); - it('should close the form on cancel', async () => { + it('should emit cancel on cancel', async () => { await cancelForm(); - expect(wrapper.emitted()).toEqual(closeFormEvent); + expect(wrapper.emitted()).toEqual({ cancel: [[]] }); }); it('should clear the form', async () => { @@ -111,71 +99,4 @@ describe('Timeline events form', () => { expect(findMinuteInput().element.value).toBe('0'); }); }); - - describe('addTimelineEventQuery', () => { - const expectedData = { - input: { - incidentId: 'gid://gitlab/Issue/1', - note: '', - occurredAt: '2020-07-08T00:00:00.000Z', - }, - }; - - let mockApollo; - - beforeEach(() => { - mockApollo = createMockApolloProvider(); - mountComponent({ mockApollo, mountMethod: mountExtended }); - }); - - it('should call the mutation with the right variables', async () => { - await submitForm(); - - expect(addEventResponse).toHaveBeenCalledWith(expectedData); - }); - - it('should call the mutation with user selected variables', async () => { - const expectedUserSelectedData = { - input: { - ...expectedData.input, - occurredAt: '2021-08-12T05:45:00.000Z', - }, - }; - - setDatetime(); - - await nextTick(); - await submitForm(); - - expect(addEventResponse).toHaveBeenCalledWith(expectedUserSelectedData); - }); - }); - - describe('error handling', () => { - it('should show an error when submission returns an error', async () => { - const expectedAlertArgs = { - message: 'Error creating incident timeline event: Create error', - }; - addEventResponse.mockResolvedValueOnce(timelineEventsCreateEventError); - mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended }); - - await submitForm(); - - expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); - }); - - it('should show an error when submission fails', async () => { - const expectedAlertArgs = { - captureError: true, - error: new Error(), - message: 'Something went wrong while creating the incident timeline event.', - }; - addEventResponse.mockRejectedValueOnce(); - mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended }); - - await submitForm(); - - expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); - }); - }); }); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js index e686f2eb4ec..90e55003ab3 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js @@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock'; import { GlIcon, GlDropdown } from '@gitlab/ui'; import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue'; +import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue'; import { mockEvents } from './mock_data'; describe('IncidentTimelineEventList', () => { @@ -10,7 +10,7 @@ describe('IncidentTimelineEventList', () => { const mountComponent = ({ propsData, provide } = {}) => { const { action, noteHtml, occurredAt } = mockEvents[0]; - wrapper = mountExtended(IncidentTimelineEventListItem, { + wrapper = mountExtended(IncidentTimelineEventItem, { propsData: { action, noteHtml, diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js index ae07237cf7d..4d2d53c990e 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js @@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue'; -import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue'; +import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_item.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql'; diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js index 2d87851a761..2cdb971395d 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js @@ -5,7 +5,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help import waitForPromises from 'helpers/wait_for_promises'; import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue'; import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue'; -import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue'; +import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue'; import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createAlert } from '~/flash'; @@ -53,7 +53,7 @@ describe('TimelineEventsTab', () => { const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList); - const findTimelineEventForm = () => wrapper.findComponent(IncidentTimelineEventForm); + const findCreateTimelineEvent = () => wrapper.findComponent(CreateTimelineEvent); const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton); describe('Timeline events tab', () => { @@ -143,18 +143,18 @@ describe('TimelineEventsTab', () => { }); it('should not show a form by default', () => { - expect(findTimelineEventForm().isVisible()).toBe(false); + expect(findCreateTimelineEvent().isVisible()).toBe(false); }); it('should show a form when button is clicked', async () => { await findAddEventButton().trigger('click'); - expect(findTimelineEventForm().isVisible()).toBe(true); + expect(findCreateTimelineEvent().isVisible()).toBe(true); }); it('should clear the form when button is clicked', async () => { const mockClear = jest.fn(); - wrapper.vm.$refs.eventForm.clear = mockClear; + wrapper.vm.$refs.createEventForm.clearForm = mockClear; await findAddEventButton().trigger('click'); @@ -165,9 +165,9 @@ describe('TimelineEventsTab', () => { // open the form await findAddEventButton().trigger('click'); - await findTimelineEventForm().vm.$emit('hide-incident-timeline-event-form'); + await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form'); - expect(findTimelineEventForm().isVisible()).toBe(false); + expect(findCreateTimelineEvent().isVisible()).toBe(false); }); }); }); diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js index 0da0114c654..d3a86680f14 100644 --- a/spec/frontend/issues/show/components/incidents/utils_spec.js +++ b/spec/frontend/issues/show/components/incidents/utils_spec.js @@ -24,7 +24,7 @@ describe('incident utils', () => { describe('get event icon', () => { it('should display a matching event icon name', () => { - ['comment', 'issues', 'status'].forEach((name) => { + ['comment', 'issues', 'label', 'status'].forEach((name) => { expect(getEventIcon(name)).toBe(name); }); }); diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js index 136a5967ee4..b0218a9df12 100644 --- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js @@ -148,7 +148,7 @@ describe('ProjectDropdown', () => { }); it('emits `error` event', () => { - expect(wrapper.emitted('error')).toBeTruthy(); + expect(wrapper.emitted('error')).toHaveLength(1); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js index 5ec1b7b7932..9f92ad2adc1 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js @@ -38,7 +38,6 @@ describe('AddNamespaceButton', () => { it('button is bound to the modal', () => { const { value } = getBinding(findButton().element, 'gl-modal'); - expect(value).toBeTruthy(); expect(value).toBe(ADD_NAMESPACE_MODAL_ID); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js index 8f79c74368f..ed0abaaf576 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js @@ -128,7 +128,7 @@ describe('SignInOauthButton', () => { }); it('does not emit `sign-in` event', () => { - expect(wrapper.emitted('sign-in')).toBeFalsy(); + expect(wrapper.emitted('sign-in')).toBeUndefined(); }); it('sets `loading` prop of button to `false`', () => { @@ -179,7 +179,7 @@ describe('SignInOauthButton', () => { }); it('emits `sign-in` event with user data', () => { - expect(wrapper.emitted('sign-in')[0]).toBeTruthy(); + expect(wrapper.emitted('sign-in')).toHaveLength(1); }); }); @@ -200,7 +200,7 @@ describe('SignInOauthButton', () => { }); it('does not emit `sign-in` event', () => { - expect(wrapper.emitted('sign-in')).toBeFalsy(); + expect(wrapper.emitted('sign-in')).toBeUndefined(); }); it('sets `loading` prop of button to `false`', () => { diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js index 65b08fba592..c12a45b2f41 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js @@ -68,7 +68,7 @@ describe('SignInPage', () => { describe('when error event is emitted', () => { it('emits another error event', () => { findSignInGitlabCom().vm.$emit('error'); - expect(wrapper.emitted('error')[0]).toBeTruthy(); + expect(wrapper.emitted('error')).toHaveLength(1); }); }); diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js index cc97d111c06..aa85253a177 100644 --- a/spec/frontend/jobs/components/job_log_controllers_spec.js +++ b/spec/frontend/jobs/components/job_log_controllers_spec.js @@ -1,8 +1,9 @@ import { GlSearchBoxByClick } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import JobLogControllers from '~/jobs/components/job_log_controllers.vue'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import { backoffMockImplementation } from 'helpers/backoff_helper'; +import * as commonUtils from '~/lib/utils/common_utils'; import { mockJobLog } from '../mock_data'; const mockToastShow = jest.fn(); @@ -10,10 +11,15 @@ const mockToastShow = jest.fn(); describe('Job log controllers', () => { let wrapper; + beforeEach(() => { + jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation); + }); + afterEach(() => { if (wrapper?.destroy) { wrapper.destroy(); } + commonUtils.backOff.mockReset(); }); const defaultProps = { @@ -24,10 +30,11 @@ describe('Job log controllers', () => { isScrollBottomDisabled: false, isScrollingDown: true, isJobLogSizeVisible: true, + isComplete: true, jobLog: mockJobLog, }; - const createWrapper = (props, jobLogSearch = false) => { + const createWrapper = (props, { jobLogJumpToFailures = false } = {}) => { wrapper = mount(JobLogControllers, { propsData: { ...defaultProps, @@ -35,7 +42,7 @@ describe('Job log controllers', () => { }, provide: { glFeatures: { - jobLogSearch, + jobLogJumpToFailures, }, }, data() { @@ -58,6 +65,7 @@ describe('Job log controllers', () => { const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); const findJobLogSearch = () => wrapper.findComponent(GlSearchBoxByClick); const findSearchHelp = () => wrapper.findComponent(HelpPopover); + const findScrollFailure = () => wrapper.find('[data-testid="job-controller-scroll-to-failure"]'); describe('Truncate information', () => { describe('with isJobLogSizeVisible', () => { @@ -109,9 +117,7 @@ describe('Job log controllers', () => { }); it('emits scrollJobLogTop event on click', async () => { - findScrollTop().trigger('click'); - - await nextTick(); + await findScrollTop().trigger('click'); expect(wrapper.emitted().scrollJobLogTop).toHaveLength(1); }); @@ -131,9 +137,7 @@ describe('Job log controllers', () => { }); it('does not emit scrollJobLogTop event on click', async () => { - findScrollTop().trigger('click'); - - await nextTick(); + await findScrollTop().trigger('click'); expect(wrapper.emitted().scrollJobLogTop).toBeUndefined(); }); @@ -147,9 +151,7 @@ describe('Job log controllers', () => { }); it('emits scrollJobLogBottom event on click', async () => { - findScrollBottom().trigger('click'); - - await nextTick(); + await findScrollBottom().trigger('click'); expect(wrapper.emitted().scrollJobLogBottom).toHaveLength(1); }); @@ -169,9 +171,7 @@ describe('Job log controllers', () => { }); it('does not emit scrollJobLogBottom event on click', async () => { - findScrollBottom().trigger('click'); - - await nextTick(); + await findScrollBottom().trigger('click'); expect(wrapper.emitted().scrollJobLogBottom).toBeUndefined(); }); @@ -201,41 +201,115 @@ describe('Job log controllers', () => { }); }); }); - }); - describe('Job log search', () => { - describe('with feature flag off', () => { - it('does not display job log search', () => { - createWrapper(); + describe('scroll to failure button', () => { + describe('with feature flag disabled', () => { + it('does not display button', () => { + createWrapper(); - expect(findJobLogSearch().exists()).toBe(false); - expect(findSearchHelp().exists()).toBe(false); + expect(findScrollFailure().exists()).toBe(false); + }); }); - }); - describe('with feature flag on', () => { - beforeEach(() => { - createWrapper({}, { jobLogSearch: true }); - }); + describe('with red text failures on the page', () => { + let firstFailure; + let secondFailure; + + beforeEach(() => { + jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']); + + createWrapper({}, { jobLogJumpToFailures: true }); + + firstFailure = document.createElement('div'); + firstFailure.className = 'term-fg-l-red'; + document.body.appendChild(firstFailure); + + secondFailure = document.createElement('div'); + secondFailure.className = 'term-fg-l-red'; + document.body.appendChild(secondFailure); + }); + + afterEach(() => { + if (firstFailure) { + firstFailure.remove(); + firstFailure = null; + } + + if (secondFailure) { + secondFailure.remove(); + secondFailure = null; + } + }); + + it('is enabled', () => { + expect(findScrollFailure().props('disabled')).toBe(false); + }); + + it('scrolls to each failure', async () => { + jest.spyOn(firstFailure, 'scrollIntoView'); - it('displays job log search', () => { - expect(findJobLogSearch().exists()).toBe(true); - expect(findSearchHelp().exists()).toBe(true); + await findScrollFailure().trigger('click'); + + expect(firstFailure.scrollIntoView).toHaveBeenCalled(); + + await findScrollFailure().trigger('click'); + + expect(secondFailure.scrollIntoView).toHaveBeenCalled(); + + await findScrollFailure().trigger('click'); + + expect(firstFailure.scrollIntoView).toHaveBeenCalled(); + }); }); - it('emits search results', () => { - const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; + describe('with no red text failures on the page', () => { + beforeEach(() => { + jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce([]); - findJobLogSearch().vm.$emit('submit'); + createWrapper({}, { jobLogJumpToFailures: true }); + }); - expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); + it('is disabled', () => { + expect(findScrollFailure().props('disabled')).toBe(true); + }); }); - it('clears search results', () => { - findJobLogSearch().vm.$emit('clear'); + describe('when the job log is not complete', () => { + beforeEach(() => { + jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']); + + createWrapper({ isComplete: false }, { jobLogJumpToFailures: true }); + }); - expect(wrapper.emitted('searchResults')).toEqual([[[]]]); + it('is enabled', () => { + expect(findScrollFailure().props('disabled')).toBe(false); + }); }); }); }); + + describe('Job log search', () => { + beforeEach(() => { + createWrapper(); + }); + + it('displays job log search', () => { + expect(findJobLogSearch().exists()).toBe(true); + expect(findSearchHelp().exists()).toBe(true); + }); + + it('emits search results', () => { + const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; + + findJobLogSearch().vm.$emit('submit'); + + expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); + }); + + it('clears search results', () => { + findJobLogSearch().vm.$emit('clear'); + + expect(wrapper.emitted('searchResults')).toEqual([[[]]]); + }); + }); }); diff --git a/spec/frontend/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js index 43f2e022dd8..8d2680608ab 100644 --- a/spec/frontend/jobs/components/sidebar_detail_row_spec.js +++ b/spec/frontend/jobs/components/sidebar_detail_row_spec.js @@ -7,7 +7,7 @@ describe('Sidebar detail row', () => { const title = 'this is the title'; const value = 'this is the value'; - const helpUrl = '/help/ci/runners/index.html'; + const helpUrl = 'https://docs.gitlab.com/runner/register/index.html'; const findHelpLink = () => wrapper.findComponent(GlLink); diff --git a/spec/frontend/labels/labels_select_spec.js b/spec/frontend/labels/labels_select_spec.js index f6e280564cc..63f7c725bc7 100644 --- a/spec/frontend/labels/labels_select_spec.js +++ b/spec/frontend/labels/labels_select_spec.js @@ -101,6 +101,12 @@ describe('LabelsSelect', () => { expect($labelEl.find('a').attr('data-html')).toBe('true'); }); + it('generated label item template has correct title for tooltip', () => { + expect($labelEl.find('a').attr('title')).toBe( + "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span><br>Foobar", + ); + }); + it('generated label item template has correct label styles and classes', () => { expect($labelEl.find('span.gl-label-text').attr('style')).toBe( `background-color: ${label.color};`, diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js index b585c69e911..29b927ef628 100644 --- a/spec/frontend/lib/dompurify_spec.js +++ b/spec/frontend/lib/dompurify_spec.js @@ -173,4 +173,50 @@ describe('~/lib/dompurify', () => { expect(sanitize(html)).toBe(`<a>internal link</a>`); }); }); + + describe('links with target attribute', () => { + const getSanitizedNode = (html) => { + return document.createRange().createContextualFragment(sanitize(html)).firstElementChild; + }; + + it('adds secure context', () => { + const html = `<a href="https://example.com" target="_blank">link</a>`; + const el = getSanitizedNode(html); + + expect(el.getAttribute('target')).toBe('_blank'); + expect(el.getAttribute('rel')).toBe('noopener noreferrer'); + }); + + it('adds secure context and merge existing `rel` values', () => { + const html = `<a href="https://example.com" target="_blank" rel="help External">link</a>`; + const el = getSanitizedNode(html); + + expect(el.getAttribute('target')).toBe('_blank'); + expect(el.getAttribute('rel')).toBe('help external noopener noreferrer'); + }); + + it('does not duplicate noopener/noreferrer `rel` values', () => { + const html = `<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>`; + const el = getSanitizedNode(html); + + expect(el.getAttribute('target')).toBe('_blank'); + expect(el.getAttribute('rel')).toBe('noreferrer noopener'); + }); + + it('does not update `rel` values when target is not `_blank` ', () => { + const html = `<a href="https://example.com" target="_self" rel="help">internal</a>`; + const el = getSanitizedNode(html); + + expect(el.getAttribute('target')).toBe('_self'); + expect(el.getAttribute('rel')).toBe('help'); + }); + + it('does not update `rel` values when target attribute is not present', () => { + const html = `<a href="https://example.com">link</a>`; + const el = getSanitizedNode(html); + + expect(el.hasAttribute('target')).toBe(false); + expect(el.hasAttribute('rel')).toBe(false); + }); + }); }); diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js index b722315d63a..f53f809b799 100644 --- a/spec/frontend/lib/gfm/index_spec.js +++ b/spec/frontend/lib/gfm/index_spec.js @@ -96,26 +96,164 @@ describe('gfm', () => { ); }); }); - }); - describe('when skipping the rendering of code blocks', () => { - it('transforms code nodes into codeblock html tags', async () => { - const result = await markdownToAST( - ` + describe('when skipping the rendering of code blocks', () => { + it('transforms code nodes into codeblock html tags', async () => { + const result = await markdownToAST( + ` \`\`\`javascript console.log('Hola'); \`\`\`\ `, - ['code'], - ); + ['code'], + ); + + expectInRoot( + result, + expect.objectContaining({ + tagName: 'codeblock', + properties: { + language: 'javascript', + }, + }), + ); + }); + }); + + describe('when skipping the rendering of reference definitions', () => { + it('transforms code nodes into codeblock html tags', async () => { + const result = await markdownToAST( + ` +[gitlab][gitlab] + +[gitlab]: https://gitlab.com "GitLab" + `, + ['definition'], + ); + + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'referencedefinition', + properties: { + identifier: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com', + }, + children: [ + { + type: 'text', + value: '[gitlab]: https://gitlab.com "GitLab"', + }, + ], + }), + ); + }); + }); + + describe('when skipping the rendering of link and image references', () => { + it('transforms linkReference and imageReference nodes into html tags', async () => { + const result = await markdownToAST( + ` +[gitlab][gitlab] and ![GitLab Logo][gitlab-logo] + +[gitlab]: https://gitlab.com "GitLab" +[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo" + `, + ['linkReference', 'imageReference'], + ); + + expectInRoot( + result, + expect.objectContaining({ + tagName: 'p', + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'a', + properties: expect.objectContaining({ + href: 'https://gitlab.com', + isReference: 'true', + identifier: 'gitlab', + title: 'GitLab', + }), + }), + expect.objectContaining({ + type: 'element', + tagName: 'img', + properties: expect.objectContaining({ + src: 'https://gitlab.com/gitlab-logo.png', + isReference: 'true', + identifier: 'gitlab-logo', + title: 'GitLab Logo', + alt: 'GitLab Logo', + }), + }), + ]), + }), + ); + }); + + it('normalizes the urls extracted from the reference definitions', async () => { + const result = await markdownToAST( + ` +[gitlab][gitlab] and ![GitLab Logo][gitlab] + +[gitlab]: /url\\bar*baz + `, + ['linkReference', 'imageReference'], + ); + + expectInRoot( + result, + expect.objectContaining({ + tagName: 'p', + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'a', + properties: expect.objectContaining({ + href: '/url%5Cbar*baz', + }), + }), + expect.objectContaining({ + type: 'element', + tagName: 'img', + properties: expect.objectContaining({ + src: '/url%5Cbar*baz', + }), + }), + ]), + }), + ); + }); + }); + }); + + describe('when skipping the rendering of frontmatter types', () => { + it.each` + type | input + ${'yaml'} | ${'---\ntitle: page\n---'} + ${'toml'} | ${'+++\ntitle: page\n+++'} + ${'json'} | ${';;;\ntitle: page\n;;;'} + `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => { + const result = await markdownToAST(input, [type]); expectInRoot( result, expect.objectContaining({ - tagName: 'codeblock', + type: 'element', + tagName: 'frontmatter', properties: { - language: 'javascript', + language: type, }, + children: [ + { + type: 'text', + value: 'title: page', + }, + ], }), ); }); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 7cf101a5e59..a2ace8857ed 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -292,16 +292,11 @@ describe('common_utils', () => { const spy = jest.fn(); const debouncedSpy = commonUtils.debounceByAnimationFrame(spy); - return new Promise((resolve) => { - window.requestAnimationFrame(() => { - debouncedSpy(); - debouncedSpy(); - window.requestAnimationFrame(() => { - expect(spy).toHaveBeenCalledTimes(1); - resolve(); - }); - }); - }); + debouncedSpy(); + debouncedSpy(); + jest.runOnlyPendingTimers(); + + expect(spy).toHaveBeenCalledTimes(1); }); }); @@ -633,7 +628,7 @@ describe('common_utils', () => { it('returns an empty object if `conversionFunction` parameter is not a function', () => { const result = commonUtils.convertObjectProps(null, mockObjects.convertObjectProps.obj); - expect(isEmptyObject(result)).toBeTruthy(); + expect(isEmptyObject(result)).toBe(true); }); }); @@ -650,9 +645,9 @@ describe('common_utils', () => { : commonUtils[functionName]; it('returns an empty object if `obj` parameter is null, undefined or an empty object', () => { - expect(isEmptyObject(testFunction(null))).toBeTruthy(); - expect(isEmptyObject(testFunction())).toBeTruthy(); - expect(isEmptyObject(testFunction({}))).toBeTruthy(); + expect(isEmptyObject(testFunction(null))).toBe(true); + expect(isEmptyObject(testFunction())).toBe(true); + expect(isEmptyObject(testFunction({}))).toBe(true); }); it('converts object properties', () => { diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js index d6131b1a1d7..313e028d861 100644 --- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js +++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js @@ -42,12 +42,12 @@ describe('Confirm Modal', () => { it('should emit `confirmed` event on `primary` modal event', () => { findGlModal().vm.$emit('primary'); - expect(wrapper.emitted('confirmed')).toBeTruthy(); + expect(wrapper.emitted('confirmed')).toHaveLength(1); }); it('should emit closed` event on `hidden` modal event', () => { modal.vm.$emit('hidden'); - expect(wrapper.emitted('closed')).toBeTruthy(); + expect(wrapper.emitted('closed')).toHaveLength(1); }); }); diff --git a/spec/frontend/lib/utils/rails_ujs_spec.js b/spec/frontend/lib/utils/rails_ujs_spec.js index c10301523c9..da9cc5c6f3c 100644 --- a/spec/frontend/lib/utils/rails_ujs_spec.js +++ b/spec/frontend/lib/utils/rails_ujs_spec.js @@ -18,14 +18,12 @@ function mockXHRResponse({ responseText, responseContentType } = {}) { .mockReturnValue(responseContentType); jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() { - requestAnimationFrame(() => { - Object.defineProperties(this, { - readyState: { value: XMLHttpRequest.DONE }, - status: { value: 200 }, - response: { value: responseText }, - }); - this.onreadystatechange(); + Object.defineProperties(this, { + readyState: { value: XMLHttpRequest.DONE }, + status: { value: 200 }, + response: { value: responseText }, }); + this.onreadystatechange(); }); } diff --git a/spec/frontend/lib/utils/recurrence_spec.js b/spec/frontend/lib/utils/recurrence_spec.js index fc22529dffc..8bf3ea4e25a 100644 --- a/spec/frontend/lib/utils/recurrence_spec.js +++ b/spec/frontend/lib/utils/recurrence_spec.js @@ -211,9 +211,10 @@ describe('recurrence', () => { describe('eject', () => { it('removes the handler assigned to the particular count slot', () => { - recurInstance.handle(1, jest.fn()); + const func = jest.fn(); + recurInstance.handle(1, func); - expect(recurInstance.handlers[1]).toBeTruthy(); + expect(recurInstance.handlers[1]).toStrictEqual(func); recurInstance.eject(1); diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js index 01e8fe777af..ec9e746c838 100644 --- a/spec/frontend/lib/utils/sticky_spec.js +++ b/spec/frontend/lib/utils/sticky_spec.js @@ -34,13 +34,13 @@ describe('sticky', () => { isSticky(el, 0, el.offsetTop); isSticky(el, 0, el.offsetTop); - expect(el.classList.contains('is-stuck')).toBeTruthy(); + expect(el.classList.contains('is-stuck')).toBe(true); }); it('adds is-stuck class', () => { isSticky(el, 0, el.offsetTop); - expect(el.classList.contains('is-stuck')).toBeTruthy(); + expect(el.classList.contains('is-stuck')).toBe(true); }); it('inserts placeholder element', () => { @@ -64,7 +64,7 @@ describe('sticky', () => { it('does not add is-stuck class', () => { isSticky(el, 0, 0); - expect(el.classList.contains('is-stuck')).toBeFalsy(); + expect(el.classList.contains('is-stuck')).toBe(false); }); it('removes placeholder', () => { diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index d1bca3c73b6..733d89fe08c 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -193,6 +193,7 @@ describe('init markdown', () => { ${'- [ ] item'} | ${'- [ ] item\n- [ ] '} ${'- [x] item'} | ${'- [x] item\n- [ ] '} ${'- [X] item'} | ${'- [X] item\n- [ ] '} + ${'- [~] item'} | ${'- [~] item\n- [ ] '} ${'- [ ] nbsp (U+00A0)'} | ${'- [ ] nbsp (U+00A0)\n- [ ] '} ${'- item\n - second'} | ${'- item\n - second\n - '} ${'- - -'} | ${'- - -'} @@ -205,6 +206,7 @@ describe('init markdown', () => { ${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '} ${'1. [x] item'} | ${'1. [x] item\n2. [ ] '} ${'1. [X] item'} | ${'1. [X] item\n2. [ ] '} + ${'1. [~] item'} | ${'1. [~] item\n2. [ ] '} ${'108. item'} | ${'108. item\n109. '} ${'108. item\n - second'} | ${'108. item\n - second\n - '} ${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '} @@ -228,11 +230,13 @@ describe('init markdown', () => { ${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'} ${'- [x] item\n- [x] '} | ${'- [x] item\n'} ${'- [X] item\n- [X] '} | ${'- [X] item\n'} + ${'- [~] item\n- [~] '} | ${'- [~] item\n'} ${'- item\n - second\n - '} | ${'- item\n - second\n'} ${'1. item\n2. '} | ${'1. item\n'} ${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'} ${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'} ${'1. [X] item\n2. [X] '} | ${'1. [X] item\n'} + ${'1. [~] item\n2. [~] '} | ${'1. [~] item\n'} ${'108. item\n109. '} | ${'108. item\n'} ${'108. item\n - second\n - '} | ${'108. item\n - second\n'} ${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'} @@ -301,6 +305,129 @@ describe('init markdown', () => { }); }); + describe('shifting selected lines left or right', () => { + const indentEvent = new KeyboardEvent('keydown', { key: ']', metaKey: true }); + const outdentEvent = new KeyboardEvent('keydown', { key: '[', metaKey: true }); + + beforeEach(() => { + textArea.addEventListener('keydown', keypressNoteText); + textArea.addEventListener('compositionstart', compositionStartNoteText); + textArea.addEventListener('compositionend', compositionEndNoteText); + }); + + it.each` + selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd + ${0} | ${0} | ${' 012\n456\n89'} | ${2} | ${2} + ${5} | ${5} | ${'012\n 456\n89'} | ${7} | ${7} + ${10} | ${10} | ${'012\n456\n 89'} | ${12} | ${12} + ${0} | ${2} | ${' 012\n456\n89'} | ${0} | ${4} + ${1} | ${2} | ${' 012\n456\n89'} | ${3} | ${4} + ${5} | ${7} | ${'012\n 456\n89'} | ${7} | ${9} + ${0} | ${7} | ${' 012\n 456\n89'} | ${0} | ${11} + ${2} | ${9} | ${' 012\n 456\n 89'} | ${4} | ${15} + `( + 'indents the selected lines two spaces to the right', + ({ + selectionStart, + selectionEnd, + expected, + expectedSelectionStart, + expectedSelectionEnd, + }) => { + const text = '012\n456\n89'; + textArea.value = text; + textArea.setSelectionRange(selectionStart, selectionEnd); + + textArea.dispatchEvent(indentEvent); + + expect(textArea.value).toEqual(expected); + expect(textArea.selectionStart).toEqual(expectedSelectionStart); + expect(textArea.selectionEnd).toEqual(expectedSelectionEnd); + }, + ); + + it('indents a blank line two spaces to the right', () => { + textArea.value = '012\n\n89'; + textArea.setSelectionRange(4, 4); + + textArea.dispatchEvent(indentEvent); + + expect(textArea.value).toEqual('012\n \n89'); + expect(textArea.selectionStart).toEqual(6); + expect(textArea.selectionEnd).toEqual(6); + }); + + it.each` + selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd + ${0} | ${0} | ${'234\n 789\n 34'} | ${0} | ${0} + ${3} | ${3} | ${'234\n 789\n 34'} | ${1} | ${1} + ${7} | ${7} | ${' 234\n789\n 34'} | ${6} | ${6} + ${0} | ${3} | ${'234\n 789\n 34'} | ${0} | ${1} + ${8} | ${10} | ${' 234\n789\n 34'} | ${7} | ${9} + ${14} | ${15} | ${' 234\n 789\n34'} | ${12} | ${13} + ${0} | ${15} | ${'234\n789\n34'} | ${0} | ${10} + ${3} | ${13} | ${'234\n789\n34'} | ${1} | ${8} + ${6} | ${6} | ${' 234\n789\n 34'} | ${6} | ${6} + `( + 'outdents the selected lines two spaces to the left', + ({ + selectionStart, + selectionEnd, + expected, + expectedSelectionStart, + expectedSelectionEnd, + }) => { + const text = ' 234\n 789\n 34'; + textArea.value = text; + textArea.setSelectionRange(selectionStart, selectionEnd); + + textArea.dispatchEvent(outdentEvent); + + expect(textArea.value).toEqual(expected); + expect(textArea.selectionStart).toEqual(expectedSelectionStart); + expect(textArea.selectionEnd).toEqual(expectedSelectionEnd); + }, + ); + + it('outdent a blank line has no effect', () => { + textArea.value = '012\n\n89'; + textArea.setSelectionRange(4, 4); + + textArea.dispatchEvent(outdentEvent); + + expect(textArea.value).toEqual('012\n\n89'); + expect(textArea.selectionStart).toEqual(4); + expect(textArea.selectionEnd).toEqual(4); + }); + + it('does not indent if meta is not set', () => { + const indentNoMetaEvent = new KeyboardEvent('keydown', { key: ']' }); + const text = '012\n456\n89'; + textArea.value = text; + textArea.setSelectionRange(0, 0); + + textArea.dispatchEvent(indentNoMetaEvent); + + expect(textArea.value).toEqual(text); + }); + + it.each` + keyEvent + ${new KeyboardEvent('keydown', { key: ']', metaKey: false })} + ${new KeyboardEvent('keydown', { key: ']', metaKey: true, shiftKey: true })} + ${new KeyboardEvent('keydown', { key: ']', metaKey: true, altKey: true })} + ${new KeyboardEvent('keydown', { key: ']', metaKey: true, ctrlKey: true })} + `('does not indent if meta is not set', ({ keyEvent }) => { + const text = '012\n456\n89'; + textArea.value = text; + textArea.setSelectionRange(0, 0); + + textArea.dispatchEvent(keyEvent); + + expect(textArea.value).toEqual(text); + }); + }); + describe('with selection', () => { let text = 'initial selected value'; let selected = 'selected'; @@ -377,6 +504,15 @@ describe('init markdown', () => { expect(textArea.value).toEqual(text); }); + + it('does nothing if meta is set', () => { + const event = new KeyboardEvent('keydown', { key: '[', metaKey: true }); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(event); + + expect(textArea.value).toEqual(text); + }); }); describe('and text to be selected', () => { diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 81cf4bd293b..2c6b603197d 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -348,15 +348,13 @@ describe('URL utility', () => { describe('urlContainsSha', () => { it('returns true when there is a valid 40-character SHA1 hash in the URL', () => { shas.valid.forEach((sha) => { - expect( - urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` }), - ).toBeTruthy(); + expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` })).toBe(true); }); }); it('returns false when there is not a valid 40-character SHA1 hash in the URL', () => { shas.invalid.forEach((str) => { - expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBeFalsy(); + expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBe(false); }); }); }); @@ -555,18 +553,22 @@ describe('URL utility', () => { describe('relativePathToAbsolute', () => { it.each` - path | base | result - ${'./foo'} | ${'bar/'} | ${'/bar/foo'} - ${'../john.md'} | ${'bar/baz/foo.php'} | ${'/bar/john.md'} - ${'../images/img.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/img.png'} - ${'../images/Image 1.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/Image 1.png'} - ${'/images/img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'} - ${'/images/img.png'} | ${'/bar/baz/foo.php'} | ${'/images/img.png'} - ${'../john.md'} | ${'/bar/baz/foo.php'} | ${'/bar/john.md'} - ${'../john.md'} | ${'///bar/baz/foo.php'} | ${'/bar/john.md'} - ${'/images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'} - ${'../images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/img.png'} - ${'../images/Image 1.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/Image%201.png'} + path | base | result + ${'./foo'} | ${'bar/'} | ${'/bar/foo'} + ${'../john.md'} | ${'bar/baz/foo.php'} | ${'/bar/john.md'} + ${'../images/img.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/img.png'} + ${'../images/Image 1.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/Image 1.png'} + ${'/images/img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'} + ${'/images/img.png'} | ${'bar/baz//foo.php'} | ${'/images/img.png'} + ${'/images//img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'} + ${'/images/img.png'} | ${'/bar/baz/foo.php'} | ${'/images/img.png'} + ${'../john.md'} | ${'/bar/baz/foo.php'} | ${'/bar/john.md'} + ${'../john.md'} | ${'///bar/baz/foo.php'} | ${'/bar/john.md'} + ${'/images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'} + ${'/images/img.png'} | ${'https://gitlab.com////user/project/'} | ${'https://gitlab.com/images/img.png'} + ${'/images////img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'} + ${'../images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/img.png'} + ${'../images/Image 1.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/Image%201.png'} `( 'converts relative path "$path" with base "$base" to absolute path => "expected"', ({ path, base, result }) => { @@ -809,13 +811,13 @@ describe('URL utility', () => { }); it('should compare against the window location if no compare value is provided', () => { - expect(urlUtils.urlIsDifferent('different')).toBeTruthy(); - expect(urlUtils.urlIsDifferent(current)).toBeFalsy(); + expect(urlUtils.urlIsDifferent('different')).toBe(true); + expect(urlUtils.urlIsDifferent(current)).toBe(false); }); it('should use the provided compare value', () => { - expect(urlUtils.urlIsDifferent('different', current)).toBeTruthy(); - expect(urlUtils.urlIsDifferent(current, current)).toBeFalsy(); + expect(urlUtils.urlIsDifferent('different', current)).toBe(true); + expect(urlUtils.urlIsDifferent(current, current)).toBe(false); }); }); @@ -1058,4 +1060,28 @@ describe('URL utility', () => { expect(urlUtils.PROMO_URL).toBe(url); }); }); + + describe('removeUrlProtocol', () => { + it.each` + input | output + ${'http://gitlab.com'} | ${'gitlab.com'} + ${'https://gitlab.com'} | ${'gitlab.com'} + ${'foo:bar.com'} | ${'bar.com'} + ${'gitlab.com'} | ${'gitlab.com'} + `('transforms $input to $output', ({ input, output }) => { + expect(urlUtils.removeUrlProtocol(input)).toBe(output); + }); + }); + + describe('removeLastSlashInUrlPath', () => { + it.each` + input | output + ${'https://www.gitlab.com/path/'} | ${'https://www.gitlab.com/path'} + ${'https://www.gitlab.com/?query=search'} | ${'https://www.gitlab.com?query=search'} + ${'https://www.gitlab.com/#fragment'} | ${'https://www.gitlab.com#fragment'} + ${'https://www.gitlab.com/hello'} | ${'https://www.gitlab.com/hello'} + `('transforms $input to $output', ({ input, output }) => { + expect(urlUtils.removeLastSlashInUrlPath(input)).toBe(output); + }); + }); }); diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js index f1471f625f8..3dac47974e7 100644 --- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js @@ -17,8 +17,8 @@ describe('AccessRequestActionButtons', () => { }); }; - const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton); - const findApproveButton = () => wrapper.find(ApproveAccessRequestButton); + const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton); + const findApproveButton = () => wrapper.findComponent(ApproveAccessRequestButton); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js index 08d7cf3c932..15bb03480e1 100644 --- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js +++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js @@ -43,8 +43,8 @@ describe('ApproveAccessRequestButton', () => { }); }; - const findForm = () => wrapper.find(GlForm); - const findButton = () => findForm().find(GlButton); + const findForm = () => wrapper.findComponent(GlForm); + const findButton = () => findForm().findComponent(GlButton); beforeEach(() => { createComponent(); diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js index 79252456f67..ea819b4fb83 100644 --- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js @@ -16,8 +16,8 @@ describe('InviteActionButtons', () => { }); }; - const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton); - const findResendInviteButton = () => wrapper.find(ResendInviteButton); + const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton); + const findResendInviteButton = () => wrapper.findComponent(ResendInviteButton); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/members/components/action_buttons/leave_button_spec.js b/spec/frontend/members/components/action_buttons/leave_button_spec.js index 4859d033464..ecfbf4460a6 100644 --- a/spec/frontend/members/components/action_buttons/leave_button_spec.js +++ b/spec/frontend/members/components/action_buttons/leave_button_spec.js @@ -22,7 +22,7 @@ describe('LeaveButton', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); beforeEach(() => { createComponent(); @@ -44,7 +44,7 @@ describe('LeaveButton', () => { }); it('renders leave modal', () => { - const leaveModal = wrapper.find(LeaveModal); + const leaveModal = wrapper.findComponent(LeaveModal); expect(leaveModal.exists()).toBe(true); expect(leaveModal.props('member')).toEqual(member); diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js index ca655e36c42..b511cebdf28 100644 --- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js +++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js @@ -42,7 +42,7 @@ describe('RemoveGroupLinkButton', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); beforeEach(() => { createComponent(); diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js index 8e933d16463..51cfd47ddf4 100644 --- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js +++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js @@ -44,7 +44,7 @@ describe('ResendInviteButton', () => { }; const findForm = () => wrapper.find('form'); - const findButton = () => findForm().find(GlButton); + const findButton = () => findForm().findComponent(GlButton); beforeEach(() => { createComponent(); diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js index 3e4ffb6e61b..6ac46619bc9 100644 --- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js @@ -19,7 +19,7 @@ describe('UserActionButtons', () => { }); }; - const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton); + const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton); afterEach(() => { wrapper.destroy(); @@ -80,7 +80,7 @@ describe('UserActionButtons', () => { }, }); - expect(wrapper.find(LeaveButton).exists()).toBe(true); + expect(wrapper.findComponent(LeaveButton).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js index 4124a1870a6..d105a4d9fde 100644 --- a/spec/frontend/members/components/app_spec.js +++ b/spec/frontend/members/components/app_spec.js @@ -41,8 +41,8 @@ describe('MembersApp', () => { }); }; - const findAlert = () => wrapper.find(GlAlert); - const findFilterSortContainer = () => wrapper.find(FilterSortContainer); + const findAlert = () => wrapper.findComponent(GlAlert); + const findFilterSortContainer = () => wrapper.findComponent(FilterSortContainer); beforeEach(() => { commonUtils.scrollToElement = jest.fn(); diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js index 9c1574a84ee..13c50de9835 100644 --- a/spec/frontend/members/components/avatars/group_avatar_spec.js +++ b/spec/frontend/members/components/avatars/group_avatar_spec.js @@ -30,7 +30,7 @@ describe('MemberList', () => { }); it('renders link to group', () => { - const link = wrapper.find(GlAvatarLink); + const link = wrapper.findComponent(GlAvatarLink); expect(link.exists()).toBe(true); expect(link.attributes('href')).toBe(group.webUrl); diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js index 7bcf4a11413..9b908e5b6f0 100644 --- a/spec/frontend/members/components/avatars/user_avatar_spec.js +++ b/spec/frontend/members/components/avatars/user_avatar_spec.js @@ -33,7 +33,7 @@ describe('UserAvatar', () => { it("renders link to user's profile", () => { createComponent(); - const link = wrapper.find(GlAvatarLink); + const link = wrapper.findComponent(GlAvatarLink); expect(link.exists()).toBe(true); expect(link.attributes()).toMatchObject({ @@ -77,7 +77,7 @@ describe('UserAvatar', () => { `('renders the "$badgeText" badge', ({ member, badgeText }) => { createComponent({ member }); - expect(wrapper.find(GlBadge).text()).toBe(badgeText); + expect(wrapper.findComponent(GlBadge).text()).toBe(badgeText); }); it('renders the "It\'s you" badge when member is current user', () => { diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js index 4ca8a3bdc36..de2f6e6dd47 100644 --- a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js +++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js @@ -60,7 +60,7 @@ describe('FilterSortContainer', () => { }, }); - expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true); + expect(wrapper.findComponent(MembersFilteredSearchBar).exists()).toBe(true); }); }); @@ -70,7 +70,7 @@ describe('FilterSortContainer', () => { tableSortableFields: ['account'], }); - expect(wrapper.find(SortDropdown).exists()).toBe(true); + expect(wrapper.findComponent(SortDropdown).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js index b692eea4aa5..4580fdb06f2 100644 --- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js +++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js @@ -56,7 +56,7 @@ describe('MembersFilteredSearchBar', () => { }); }; - const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar); + const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar); it('passes correct props to `FilteredSearchBar` component', () => { createComponent(); diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js index 709ad907a38..5581fd52458 100644 --- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js +++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js @@ -43,13 +43,13 @@ describe('SortDropdown', () => { }); }; - const findSortingComponent = () => wrapper.find(GlSorting); + const findSortingComponent = () => wrapper.findComponent(GlSorting); const findSortDirectionToggle = () => findSortingComponent().find('button[title="Sort direction"]'); const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); const findDropdownItemByText = (text) => wrapper - .findAll(GlSortingItem) + .findAllComponents(GlSortingItem) .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text); beforeEach(() => { diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js index 447496910b8..af96396f09f 100644 --- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js +++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js @@ -47,8 +47,8 @@ describe('RemoveGroupLinkModal', () => { }); }; - const findModal = () => wrapper.find(GlModal); - const findForm = () => findModal().find(GlForm); + const findModal = () => wrapper.findComponent(GlModal); + const findForm = () => findModal().findComponent(GlForm); const getByText = (text, options) => createWrapper(within(findModal().element).getByText(text, options)); diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js index 1d39c4b3175..59b112492b8 100644 --- a/spec/frontend/members/components/modals/remove_member_modal_spec.js +++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js @@ -46,7 +46,7 @@ describe('RemoveMemberModal', () => { }); }; - const findForm = () => wrapper.find({ ref: 'form' }); + const findForm = () => wrapper.findComponent({ ref: 'form' }); const findGlModal = () => wrapper.findComponent(GlModal); const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList); diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js index 74b71e22893..793c122587d 100644 --- a/spec/frontend/members/components/table/created_at_spec.js +++ b/spec/frontend/members/components/table/created_at_spec.js @@ -39,7 +39,7 @@ describe('CreatedAt', () => { }); it('uses `TimeAgoTooltip` component to display tooltip', () => { - expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true); + expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true); }); }); diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js index 4fb43fbd888..9b8f053348b 100644 --- a/spec/frontend/members/components/table/expiration_datepicker_spec.js +++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js @@ -56,7 +56,7 @@ describe('ExpirationDatepicker', () => { }; const findInput = () => wrapper.find('input'); - const findDatepicker = () => wrapper.find(GlDatepicker); + const findDatepicker = () => wrapper.findComponent(GlDatepicker); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js index 1379b2d26ce..f3f50bf620a 100644 --- a/spec/frontend/members/components/table/member_action_buttons_spec.js +++ b/spec/frontend/members/components/table/member_action_buttons_spec.js @@ -38,7 +38,7 @@ describe('MemberActionButtons', () => { ({ memberType, member, expectedComponent }) => { createComponent({ memberType, member }); - expect(wrapper.find(expectedComponent).exists()).toBe(true); + expect(wrapper.findComponent(expectedComponent).exists()).toBe(true); }, ); }); diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js index 3cce64effbc..35f82c28fc5 100644 --- a/spec/frontend/members/components/table/member_avatar_spec.js +++ b/spec/frontend/members/components/table/member_avatar_spec.js @@ -33,7 +33,7 @@ describe('MemberList', () => { ({ memberType, member, expectedComponent }) => { createComponent({ memberType, member }); - expect(wrapper.find(expectedComponent).exists()).toBe(true); + expect(wrapper.findComponent(expectedComponent).exists()).toBe(true); }, ); }); diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js index 6575a7c7126..fd56699602e 100644 --- a/spec/frontend/members/components/table/members_table_cell_spec.js +++ b/spec/frontend/members/components/table/members_table_cell_spec.js @@ -69,7 +69,7 @@ describe('MembersTableCell', () => { }); }; - const findWrappedComponent = () => wrapper.find(WrappedComponent); + const findWrappedComponent = () => wrapper.findComponent(WrappedComponent); const memberCurrentUser = { ...memberMock, diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 08baa663bf0..0ed01396fcb 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -81,13 +81,13 @@ describe('MembersTable', () => { const url = 'https://localhost/foo-bar/-/project_members?tab=invited'; - const findTable = () => wrapper.find(GlTable); + const findTable = () => wrapper.findComponent(GlTable); const findTableCellByMemberId = (tableCellLabel, memberId) => wrapper .findByTestId(`members-table-row-${memberId}`) .find(`[data-label="${tableCellLabel}"][role="cell"]`); - const findPagination = () => extendedWrapper(wrapper.find(GlPagination)); + const findPagination = () => extendedWrapper(wrapper.findComponent(GlPagination)); const expectCorrectLinkToPage2 = () => { expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe( @@ -126,7 +126,10 @@ describe('MembersTable', () => { if (expectedComponent) { expect( - wrapper.find(`[data-label="${label}"][role="cell"]`).find(expectedComponent).exists(), + wrapper + .find(`[data-label="${label}"][role="cell"]`) + .findComponent(expectedComponent) + .exists(), ).toBe(true); } }); @@ -179,7 +182,10 @@ describe('MembersTable', () => { expect(actionField.exists()).toBe(true); expect(actionField.classes('gl-sr-only')).toBe(true); expect( - wrapper.find(`[data-label="Actions"][role="cell"]`).find(MemberActionButtons).exists(), + wrapper + .find(`[data-label="Actions"][role="cell"]`) + .findComponent(MemberActionButtons) + .exists(), ).toBe(true); }); @@ -250,9 +256,9 @@ describe('MembersTable', () => { it('renders badge in "Max role" field', () => { createComponent({ members: [memberMock], tableFields: ['maxRole'] }); - expect(wrapper.find(`[data-label="Max role"][role="cell"]`).find(GlBadge).text()).toBe( - memberMock.accessLevel.stringValue, - ); + expect( + wrapper.find(`[data-label="Max role"][role="cell"]`).findComponent(GlBadge).text(), + ).toBe(memberMock.accessLevel.stringValue); }); }); diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js index 2f1626a7044..b254cce4d72 100644 --- a/spec/frontend/members/components/table/role_dropdown_spec.js +++ b/spec/frontend/members/components/table/role_dropdown_spec.js @@ -57,11 +57,11 @@ describe('RoleDropdown', () => { ); const getCheckedDropdownItem = () => wrapper - .findAll(GlDropdownItem) + .findAllComponents(GlDropdownItem) .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked')); const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js index 251a8b0b774..5c813eb2a67 100644 --- a/spec/frontend/members/index_spec.js +++ b/spec/frontend/members/index_spec.js @@ -39,7 +39,7 @@ describe('initMembersApp', () => { it('renders `MembersTabs`', () => { setup(); - expect(wrapper.find(MembersTabs).exists()).toBe(true); + expect(wrapper.findComponent(MembersTabs).exists()).toBe(true); }); it('parses and sets `members` in Vuex store', () => { diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js index 8dc6132709e..3674a49f42c 100644 --- a/spec/frontend/monitoring/components/charts/anomaly_spec.js +++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js @@ -20,7 +20,7 @@ describe('Anomaly chart component', () => { propsData: { ...props }, }); }; - const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart); + const findTimeSeries = () => wrapper.findComponent(MonitorTimeSeriesChart); const getTimeSeriesProps = () => findTimeSeries().props(); describe('wrapped monitor-time-series-chart component', () => { diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js index 6368c53943a..5339a7a525b 100644 --- a/spec/frontend/monitoring/components/charts/bar_spec.js +++ b/spec/frontend/monitoring/components/charts/bar_spec.js @@ -33,7 +33,7 @@ describe('Bar component', () => { let chartData; beforeEach(() => { - glbarChart = barChart.find(GlBarChart); + glbarChart = barChart.findComponent(GlBarChart); chartData = barChart.vm.chartData[graphData.metrics[0].label]; }); diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js index e10cb3a456a..0158966997f 100644 --- a/spec/frontend/monitoring/components/charts/column_spec.js +++ b/spec/frontend/monitoring/components/charts/column_spec.js @@ -44,7 +44,7 @@ describe('Column component', () => { }, }); }; - const findChart = () => wrapper.find(GlColumnChart); + const findChart = () => wrapper.findComponent(GlColumnChart); const chartProps = (prop) => findChart().props(prop); beforeEach(() => { diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js index c8f67d5d8c7..484199698ea 100644 --- a/spec/frontend/monitoring/components/charts/gauge_spec.js +++ b/spec/frontend/monitoring/components/charts/gauge_spec.js @@ -8,7 +8,7 @@ describe('Gauge Chart component', () => { let wrapper; - const findGaugeChart = () => wrapper.find(GlGaugeChart); + const findGaugeChart = () => wrapper.findComponent(GlGaugeChart); const createWrapper = ({ ...graphProps } = {}) => { wrapper = shallowMount(GaugeChart, { diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js index 841b7e0648a..e163d4e73a0 100644 --- a/spec/frontend/monitoring/components/charts/heatmap_spec.js +++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js @@ -8,7 +8,7 @@ describe('Heatmap component', () => { let wrapper; let store; - const findChart = () => wrapper.find(GlHeatmap); + const findChart = () => wrapper.findComponent(GlHeatmap); const graphData = heatmapGraphData(); diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js index 8633b49565f..62a0b7e6ad3 100644 --- a/spec/frontend/monitoring/components/charts/single_stat_spec.js +++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js @@ -15,7 +15,7 @@ describe('Single Stat Chart component', () => { }); }; - const findChart = () => wrapper.find(GlSingleStat); + const findChart = () => wrapper.findComponent(GlSingleStat); beforeEach(() => { createComponent(); diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js index 9cab3650f28..91fe36bc6e4 100644 --- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js +++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js @@ -15,8 +15,8 @@ describe('Stacked column chart component', () => { let wrapper; - const findChart = () => wrapper.find(GlStackedColumnChart); - const findLegend = () => wrapper.find(GlChartLegend); + const findChart = () => wrapper.findComponent(GlStackedColumnChart); + const findLegend = () => wrapper.findComponent(GlChartLegend); const createWrapper = (props = {}, mountingMethod = shallowMount) => mountingMethod(StackedColumnChart, { diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index f4bca26f659..503dee7b937 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -9,7 +9,6 @@ import { mount, shallowMount } from '@vue/test-utils'; import timezoneMock from 'timezone-mock'; import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; -import { setTestTimeout } from 'helpers/timeout'; import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; import TimeSeries from '~/monitoring/components/charts/time_series.vue'; import { panelTypes, chartHeight } from '~/monitoring/constants'; @@ -59,17 +58,13 @@ describe('Time series component', () => { }); }; - beforeEach(() => { - setTestTimeout(1000); - }); - afterEach(() => { wrapper.destroy(); }); describe('With a single time series', () => { describe('general functions', () => { - const findChart = () => wrapper.find({ ref: 'chart' }); + const findChart = () => wrapper.findComponent({ ref: 'chart' }); beforeEach(async () => { createWrapper({}, mount); @@ -215,7 +210,7 @@ describe('Time series component', () => { const name = 'Metric 1'; const value = '5.556'; const dataIndex = 0; - const seriesLabel = wrapper.find(GlChartSeriesLabel); + const seriesLabel = wrapper.findComponent(GlChartSeriesLabel); expect(seriesLabel.vm.color).toBe(''); @@ -225,7 +220,11 @@ describe('Time series component', () => { ]); expect( - shallowWrapperContainsSlotText(wrapper.find(GlLineChart), 'tooltip-content', value), + shallowWrapperContainsSlotText( + wrapper.findComponent(GlLineChart), + 'tooltip-content', + value, + ), ).toBe(true); }); @@ -598,7 +597,7 @@ describe('Time series component', () => { glChartComponents.forEach((dynamicComponent) => { describe(`GitLab UI: ${dynamicComponent.chartType}`, () => { - const findChartComponent = () => wrapper.find(dynamicComponent.component); + const findChartComponent = () => wrapper.findComponent(dynamicComponent.component); beforeEach(async () => { createWrapper( @@ -656,7 +655,7 @@ describe('Time series component', () => { wrapper.vm.tooltip.commitUrl = commitUrl; await nextTick(); - const commitLink = wrapper.find(GlLink); + const commitLink = wrapper.findComponent(GlLink); expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true); expect(commitLink.attributes('href')).toEqual(commitUrl); @@ -680,7 +679,9 @@ describe('Time series component', () => { let lineColors; beforeEach(() => { - lineColors = wrapper.find(GlAreaChart).vm.series.map((item) => item.lineStyle.color); + lineColors = wrapper + .findComponent(GlAreaChart) + .vm.series.map((item) => item.lineStyle.color); }); it('should contain different colors for contiguous time series', () => { @@ -690,7 +691,7 @@ describe('Time series component', () => { }); it('should match series color with tooltip label color', () => { - const labels = wrapper.findAll(GlChartSeriesLabel); + const labels = wrapper.findAllComponents(GlChartSeriesLabel); lineColors.forEach((color, index) => { const labelColor = labels.at(index).props('color'); @@ -700,7 +701,7 @@ describe('Time series component', () => { it('should match series color with legend color', () => { const legendColors = wrapper - .find(GlChartLegend) + .findComponent(GlChartLegend) .props('seriesInfo') .map((item) => item.color); @@ -713,7 +714,7 @@ describe('Time series component', () => { }); describe('legend layout', () => { - const findLegend = () => wrapper.find(GlChartLegend); + const findLegend = () => wrapper.findComponent(GlChartLegend); beforeEach(async () => { createWrapper({}, mount); diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js index d74f959ac0f..bb57420d406 100644 --- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js +++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js @@ -92,7 +92,7 @@ describe('Actions menu', () => { }); it('renders custom metrics form fields', () => { - expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true); + expect(wrapper.findComponent(CustomMetricsFormFields).exists()).toBe(true); }); }); @@ -316,7 +316,7 @@ describe('Actions menu', () => { }); it('is not disabled', () => { - expect(findStarDashboardItem().attributes('disabled')).toBeFalsy(); + expect(findStarDashboardItem().attributes('disabled')).toBeUndefined(); }); it('is disabled when starring is taking place', async () => { diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js index e28c2913949..18ccda2c41c 100644 --- a/spec/frontend/monitoring/components/dashboard_header_spec.js +++ b/spec/frontend/monitoring/components/dashboard_header_spec.js @@ -29,18 +29,19 @@ describe('Dashboard header', () => { let store; let wrapper; - const findDashboardDropdown = () => wrapper.find(DashboardsDropdown); + const findDashboardDropdown = () => wrapper.findComponent(DashboardsDropdown); - const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' }); - const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlDropdownItem); - const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType); - const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }); - const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon); + const findEnvsDropdown = () => wrapper.findComponent({ ref: 'monitorEnvironmentsDropdown' }); + const findEnvsDropdownItems = () => findEnvsDropdown().findAllComponents(GlDropdownItem); + const findEnvsDropdownSearch = () => findEnvsDropdown().findComponent(GlSearchBoxByType); + const findEnvsDropdownSearchMsg = () => + wrapper.findComponent({ ref: 'monitorEnvironmentsDropdownMsg' }); + const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().findComponent(GlLoadingIcon); - const findDateTimePicker = () => wrapper.find(DateTimePicker); - const findRefreshButton = () => wrapper.find(RefreshButton); + const findDateTimePicker = () => wrapper.findComponent(DateTimePicker); + const findRefreshButton = () => wrapper.findComponent(RefreshButton); - const findActionsMenu = () => wrapper.find(ActionsMenu); + const findActionsMenu = () => wrapper.findComponent(ActionsMenu); const setSearchTerm = (searchTerm) => { store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js index f19ef6c6fb7..d71f6374967 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js @@ -32,14 +32,14 @@ describe('dashboard invalid url parameters', () => { }); }; - const findForm = () => wrapper.find(GlForm); - const findTxtArea = () => findForm().find(GlFormTextarea); + const findForm = () => wrapper.findComponent(GlForm); + const findTxtArea = () => findForm().findComponent(GlFormTextarea); const findSubmitBtn = () => findForm().find('[type="submit"]'); - const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' }); - const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' }); - const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' }); - const findPanel = () => wrapper.find(DashboardPanel); - const findTimeRangePicker = () => wrapper.find(DateTimePicker); + const findClipboardCopyBtn = () => wrapper.findComponent({ ref: 'clipboardCopyBtn' }); + const findViewDocumentationBtn = () => wrapper.findComponent({ ref: 'viewDocumentationBtn' }); + const findOpenRepositoryBtn = () => wrapper.findComponent({ ref: 'openRepositoryBtn' }); + const findPanel = () => wrapper.findComponent(DashboardPanel); + const findTimeRangePicker = () => wrapper.findComponent(DateTimePicker); const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]'); beforeEach(() => { @@ -192,8 +192,8 @@ describe('dashboard invalid url parameters', () => { }); it('displays an alert', () => { - expect(wrapper.find(GlAlert).exists()).toBe(true); - expect(wrapper.find(GlAlert).text()).toBe(mockError); + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); + expect(wrapper.findComponent(GlAlert).text()).toBe(mockError); }); it('displays an empty dashboard panel', () => { @@ -215,11 +215,11 @@ describe('dashboard invalid url parameters', () => { }); it('displays no alert', () => { - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); it('displays panel with data', () => { - const { title, type } = wrapper.find(DashboardPanel).props('graphData'); + const { title, type } = wrapper.findComponent(DashboardPanel).props('graphData'); expect(title).toBe(mockPanel.title); expect(type).toBe(mockPanel.type); diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js index 7c54a4742ac..d797d9e2ad0 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js @@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import Vuex from 'vuex'; import { nextTick } from 'vue'; -import { setTestTimeout } from 'helpers/timeout'; import axios from '~/lib/utils/axios_utils'; import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue'; @@ -42,11 +41,11 @@ describe('Dashboard Panel', () => { const exampleText = 'example_text'; - const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' }); - const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' }); - const findTitle = () => wrapper.find({ ref: 'graphTitle' }); - const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' }); - const findMenuItems = () => wrapper.findAll(GlDropdownItem); + const findCopyLink = () => wrapper.findComponent({ ref: 'copyChartLink' }); + const findTimeChart = () => wrapper.findComponent({ ref: 'timeSeriesChart' }); + const findTitle = () => wrapper.findComponent({ ref: 'graphTitle' }); + const findCtxMenu = () => wrapper.findComponent({ ref: 'contextualMenu' }); + const findMenuItems = () => wrapper.findAllComponents(GlDropdownItem); const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text); const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => { @@ -72,8 +71,6 @@ describe('Dashboard Panel', () => { }; beforeEach(() => { - setTestTimeout(1000); - store = createStore(); state = store.state.monitoringDashboard; @@ -118,7 +115,7 @@ describe('Dashboard Panel', () => { }); it('renders no download csv link', () => { - expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false); }); it('does not contain graph widgets', () => { @@ -126,7 +123,7 @@ describe('Dashboard Panel', () => { }); it('The Empty Chart component is rendered and is a Vue instance', () => { - expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true); + expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true); }); }); @@ -146,7 +143,7 @@ describe('Dashboard Panel', () => { }); it('renders no download csv link', () => { - expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false); }); it('does not contain graph widgets', () => { @@ -154,7 +151,7 @@ describe('Dashboard Panel', () => { }); it('The Empty Chart component is rendered and is a Vue instance', () => { - expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true); + expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true); }); }); @@ -173,7 +170,7 @@ describe('Dashboard Panel', () => { it('contains graph widgets', () => { expect(findCtxMenu().exists()).toBe(true); - expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(true); }); it('sets no clipboard copy link on dropdown by default', () => { @@ -208,12 +205,12 @@ describe('Dashboard Panel', () => { it('empty chart is rendered for empty results', () => { createWrapper({ graphData: graphDataEmpty }); - expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true); + expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true); }); it('area chart is rendered by default', () => { createWrapper(); - expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true); + expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true); }); describe.each` @@ -234,8 +231,8 @@ describe('Dashboard Panel', () => { }); it(`renders the chart component and binds attributes`, () => { - expect(wrapper.find(component).exists()).toBe(true); - expect(wrapper.find(component).attributes()).toMatchObject(attrs); + expect(wrapper.findComponent(component).exists()).toBe(true); + expect(wrapper.findComponent(component).attributes()).toMatchObject(attrs); }); it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => { @@ -273,7 +270,7 @@ describe('Dashboard Panel', () => { }); describe('Edit custom metric dropdown item', () => { - const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' }); + const findEditCustomMetricLink = () => wrapper.findComponent({ ref: 'editMetricLink' }); const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit'; beforeEach(async () => { @@ -434,7 +431,7 @@ describe('Dashboard Panel', () => { }); it('it renders a time series chart with no errors', () => { - expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true); + expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true); }); }); @@ -446,7 +443,7 @@ describe('Dashboard Panel', () => { it('displays a heatmap in local timezone', () => { createWrapper({ graphData: heatmapGraphData() }); - expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('LOCAL'); + expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('LOCAL'); }); describe('when timezone is set to UTC', () => { @@ -461,13 +458,13 @@ describe('Dashboard Panel', () => { it('displays a heatmap with UTC', () => { createWrapper({ graphData: heatmapGraphData() }); - expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('UTC'); + expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('UTC'); }); }); }); describe('Expand to full screen', () => { - const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' }); + const findExpandBtn = () => wrapper.findComponent({ ref: 'expandBtn' }); describe('when there is no @expand listener', () => { it('does not show `View full screen` option', () => { @@ -495,7 +492,7 @@ describe('Dashboard Panel', () => { }); describe('When graphData contains links', () => { - const findManageLinksItem = () => wrapper.find({ ref: 'manageLinksItem' }); + const findManageLinksItem = () => wrapper.findComponent({ ref: 'manageLinksItem' }); const mockLinks = [ { url: 'https://example.com', diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 90171cfc65e..608404e5c5b 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -97,8 +97,10 @@ describe('Dashboard', () => { createShallowWrapper({ hasMetrics: true }); await nextTick(); - expect(wrapper.find(EmptyState).exists()).toBe(true); - expect(wrapper.find(EmptyState).props('selectedState')).toBe(dashboardEmptyStates.LOADING); + expect(wrapper.findComponent(EmptyState).exists()).toBe(true); + expect(wrapper.findComponent(EmptyState).props('selectedState')).toBe( + dashboardEmptyStates.LOADING, + ); }); it('hides the group panels when showPanels is false', async () => { @@ -126,7 +128,7 @@ describe('Dashboard', () => { describe('panel containers layout', () => { const findPanelLayoutWrapperAt = (index) => { return wrapper - .find(GraphGroup) + .findComponent(GraphGroup) .findAll('[data-testid="dashboard-panel-layout-wrapper"]') .at(index); }; @@ -366,7 +368,7 @@ describe('Dashboard', () => { }); describe('when all panels in the first group are loading', () => { - const findGroupAt = (i) => wrapper.findAll(GraphGroup).at(i); + const findGroupAt = (i) => wrapper.findAllComponents(GraphGroup).at(i); beforeEach(async () => { setupStoreWithDashboard(store); @@ -409,7 +411,7 @@ describe('Dashboard', () => { setupStoreWithData(store); await nextTick(); - wrapper.findAll(GraphGroup).wrappers.forEach((groupWrapper) => { + wrapper.findAllComponents(GraphGroup).wrappers.forEach((groupWrapper) => { expect(groupWrapper.props('isLoading')).toBe(false); }); }); @@ -443,7 +445,7 @@ describe('Dashboard', () => { }); describe('single panel expands to "full screen" mode', () => { - const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' }); + const findExpandedPanel = () => wrapper.findComponent({ ref: 'expandedPanel' }); describe('when the panel is not expanded', () => { beforeEach(async () => { @@ -457,7 +459,7 @@ describe('Dashboard', () => { }); it('can set a panel as expanded', () => { - const panel = wrapper.findAll(DashboardPanel).at(1); + const panel = wrapper.findAllComponents(DashboardPanel).at(1); jest.spyOn(store, 'dispatch'); @@ -503,7 +505,7 @@ describe('Dashboard', () => { }); it('displays a single panel and others are hidden', () => { - const panels = wrapper.findAll(MockPanel); + const panels = wrapper.findAllComponents(MockPanel); const visiblePanels = panels.filter((w) => w.isVisible()); expect(findExpandedPanel().isVisible()).toBe(true); @@ -523,7 +525,7 @@ describe('Dashboard', () => { }); it('restores full dashboard by clicking `back`', () => { - wrapper.find({ ref: 'goBackBtn' }).vm.$emit('click'); + wrapper.findComponent({ ref: 'goBackBtn' }).vm.$emit('click'); expect(store.dispatch).toHaveBeenCalledWith( 'monitoringDashboard/clearExpandedPanel', @@ -551,21 +553,21 @@ describe('Dashboard', () => { }); it('shows a group empty area', () => { - const emptyGroup = wrapper.findAll({ ref: 'empty-group' }); + const emptyGroup = wrapper.findAllComponents({ ref: 'empty-group' }); expect(emptyGroup).toHaveLength(1); expect(emptyGroup.is(GroupEmptyState)).toBe(true); }); it('group empty area displays a NO_DATA state', () => { - expect(wrapper.findAll({ ref: 'empty-group' }).at(0).props('selectedState')).toEqual( - metricStates.NO_DATA, - ); + expect( + wrapper.findAllComponents({ ref: 'empty-group' }).at(0).props('selectedState'), + ).toEqual(metricStates.NO_DATA); }); }); describe('drag and drop function', () => { - const findDraggables = () => wrapper.findAll(VueDraggable); + const findDraggables = () => wrapper.findAllComponents(VueDraggable); const findEnabledDraggables = () => findDraggables().filter((f) => !f.attributes('disabled')); const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel'); const findRearrangeButton = () => wrapper.find('.js-rearrange-button'); @@ -677,7 +679,7 @@ describe('Dashboard', () => { }); it('hides dashboard header by default', () => { - expect(wrapper.find({ ref: 'prometheusGraphsHeader' }).exists()).toEqual(false); + expect(wrapper.findComponent({ ref: 'prometheusGraphsHeader' }).exists()).toEqual(false); }); it('renders correctly', () => { @@ -742,7 +744,7 @@ describe('Dashboard', () => { const panelIndex = 1; // skip expanded panel const getClipboardTextFirstPanel = () => - wrapper.findAll(DashboardPanel).at(panelIndex).props('clipboardText'); + wrapper.findAllComponents(DashboardPanel).at(panelIndex).props('clipboardText'); beforeEach(async () => { setupStoreWithData(store); @@ -770,7 +772,7 @@ describe('Dashboard', () => { // While the recommendation in the documentation is to test // with a data-testid attribute, I want to make sure that // the dashboard panels have a ref attribute set. - const getDashboardPanel = () => wrapper.find({ ref: panelRef }); + const getDashboardPanel = () => wrapper.findComponent({ ref: panelRef }); beforeEach(async () => { setupStoreWithData(store); diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js index 64c48100b31..a327e234581 100644 --- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js +++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js @@ -35,7 +35,8 @@ describe('dashboard invalid url parameters', () => { }); }; - const findDateTimePicker = () => wrapper.find(DashboardHeader).find({ ref: 'dateTimePicker' }); + const findDateTimePicker = () => + wrapper.findComponent(DashboardHeader).findComponent({ ref: 'dateTimePicker' }); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js index f6d30384847..721992e710a 100644 --- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js +++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js @@ -33,11 +33,11 @@ describe('DashboardsDropdown', () => { }); } - const findItems = () => wrapper.findAll(GlDropdownItem); - const findItemAt = (i) => wrapper.findAll(GlDropdownItem).at(i); - const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' }); - const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' }); - const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' }); + const findItems = () => wrapper.findAllComponents(GlDropdownItem); + const findItemAt = (i) => wrapper.findAllComponents(GlDropdownItem).at(i); + const findSearchInput = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownSearch' }); + const findNoItemsMsg = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownMsg' }); + const findStarredListDivider = () => wrapper.findComponent({ ref: 'starredListDivider' }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax const setSearchTerm = (searchTerm) => wrapper.setData({ searchTerm }); @@ -127,7 +127,7 @@ describe('DashboardsDropdown', () => { }); it('displays a star icon', () => { - const star = findItemAt(0).find(GlIcon); + const star = findItemAt(0).findComponent(GlIcon); expect(star.exists()).toBe(true); expect(star.attributes('name')).toBe('star'); }); @@ -148,7 +148,7 @@ describe('DashboardsDropdown', () => { }); it('displays no star icon', () => { - const star = findItemAt(0).find(GlIcon); + const star = findItemAt(0).findComponent(GlIcon); expect(star.exists()).toBe(false); }); diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js index 0dd3afd7c83..755204dc721 100644 --- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js +++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js @@ -18,7 +18,7 @@ const createMountedWrapper = (props = {}) => { describe('DuplicateDashboardForm', () => { const defaultBranch = 'main'; - const findByRef = (ref) => wrapper.find({ ref }); + const findByRef = (ref) => wrapper.findComponent({ ref }); const setValue = (ref, val) => { findByRef(ref).setValue(val); }; diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js index 7e7a7a66d77..3032c236741 100644 --- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js +++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js @@ -44,9 +44,9 @@ describe('duplicate dashboard modal', () => { }); } - const findAlert = () => wrapper.find(GlAlert); - const findModal = () => wrapper.find(GlModal); - const findDuplicateDashboardForm = () => wrapper.find(DuplicateDashboardForm); + const findAlert = () => wrapper.findComponent(GlAlert); + const findModal = () => wrapper.findComponent(GlModal); + const findDuplicateDashboardForm = () => wrapper.findComponent(DuplicateDashboardForm); beforeEach(() => { mockDashboards = dashboardGitResponse; @@ -74,7 +74,7 @@ describe('duplicate dashboard modal', () => { expect(okEvent.preventDefault).toHaveBeenCalled(); expect(wrapper.emitted().dashboardDuplicated).toBeTruthy(); expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled(); expect(findAlert().exists()).toBe(false); }); @@ -92,7 +92,7 @@ describe('duplicate dashboard modal', () => { expect(findAlert().exists()).toBe(true); expect(findAlert().text()).toBe(errMsg); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled(); }); @@ -102,7 +102,7 @@ describe('duplicate dashboard modal', () => { commitMessage: 'A commit message', }; - findModal().find(DuplicateDashboardForm).vm.$emit('change', formVals); + findModal().findComponent(DuplicateDashboardForm).vm.$emit('change', formVals); // Binding's second argument contains the modal id expect(wrapper.vm.form).toEqual(formVals); diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js index 47366b345a8..6695353bdb5 100644 --- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js +++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js @@ -58,14 +58,14 @@ describe('Embed Group', () => { metricsWithDataGetter.mockReturnValue([]); mountComponent(); - expect(wrapper.find(GlCard).isVisible()).toBe(false); + expect(wrapper.findComponent(GlCard).isVisible()).toBe(false); }); it('shows the component when chart data is loaded', () => { metricsWithDataGetter.mockReturnValue([1]); mountComponent(); - expect(wrapper.find(GlCard).isVisible()).toBe(true); + expect(wrapper.findComponent(GlCard).isVisible()).toBe(true); }); it('is expanded by default', () => { @@ -79,7 +79,7 @@ describe('Embed Group', () => { metricsWithDataGetter.mockReturnValue([1]); mountComponent({ shallow: false, stubs: { MetricEmbed: true } }); - wrapper.find(GlButton).trigger('click'); + wrapper.findComponent(GlButton).trigger('click'); await nextTick(); expect(wrapper.find('.gl-card-body').classes()).toContain('d-none'); @@ -93,11 +93,11 @@ describe('Embed Group', () => { }); it('renders an Embed component', () => { - expect(wrapper.find(MetricEmbed).exists()).toBe(true); + expect(wrapper.findComponent(MetricEmbed).exists()).toBe(true); }); it('passes the correct props to the Embed component', () => { - expect(wrapper.find(MetricEmbed).props()).toEqual(singleEmbedProps()); + expect(wrapper.findComponent(MetricEmbed).props()).toEqual(singleEmbedProps()); }); it('adds the monitoring dashboard module', () => { @@ -112,7 +112,7 @@ describe('Embed Group', () => { }); it('passes the correct props to the dashboard Embed component', () => { - expect(wrapper.find(MetricEmbed).props()).toEqual(dashboardEmbedProps()); + expect(wrapper.findComponent(MetricEmbed).props()).toEqual(dashboardEmbedProps()); }); it('adds the monitoring dashboard module', () => { @@ -127,11 +127,11 @@ describe('Embed Group', () => { }); it('creates Embed components', () => { - expect(wrapper.findAll(MetricEmbed)).toHaveLength(2); + expect(wrapper.findAllComponents(MetricEmbed)).toHaveLength(2); }); it('passes the correct props to the Embed components', () => { - expect(wrapper.findAll(MetricEmbed).wrappers.map((item) => item.props())).toEqual( + expect(wrapper.findAllComponents(MetricEmbed).wrappers.map((item) => item.props())).toEqual( multipleEmbedProps(), ); }); @@ -147,14 +147,14 @@ describe('Embed Group', () => { metricsWithDataGetter.mockReturnValue([1]); mountComponent({ shallow: false, stubs: { MetricEmbed: true } }); - expect(wrapper.find(GlButton).text()).toBe('Hide chart'); + expect(wrapper.findComponent(GlButton).text()).toBe('Hide chart'); }); it('has a plural label when there are multiple embeds', () => { metricsWithDataGetter.mockReturnValue([2]); mountComponent({ shallow: false, stubs: { MetricEmbed: true } }); - expect(wrapper.find(GlButton).text()).toBe('Hide charts'); + expect(wrapper.findComponent(GlButton).text()).toBe('Hide charts'); }); }); }); diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js index f9f1be4f277..beff3da2baf 100644 --- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js +++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js @@ -64,7 +64,7 @@ describe('MetricEmbed', () => { it('shows an empty state when no metrics are present', () => { expect(wrapper.find('.metrics-embed').exists()).toBe(true); - expect(wrapper.find(DashboardPanel).exists()).toBe(false); + expect(wrapper.findComponent(DashboardPanel).exists()).toBe(false); }); }); @@ -92,12 +92,12 @@ describe('MetricEmbed', () => { it('shows a chart when metrics are present', () => { expect(wrapper.find('.metrics-embed').exists()).toBe(true); - expect(wrapper.find(DashboardPanel).exists()).toBe(true); - expect(wrapper.findAll(DashboardPanel).length).toBe(2); + expect(wrapper.findComponent(DashboardPanel).exists()).toBe(true); + expect(wrapper.findAllComponents(DashboardPanel).length).toBe(2); }); it('includes groupId with dashboardUrl', () => { - expect(wrapper.find(DashboardPanel).props('groupId')).toBe(TEST_HOST); + expect(wrapper.findComponent(DashboardPanel).props('groupId')).toBe(TEST_HOST); }); }); }); diff --git a/spec/frontend/monitoring/components/empty_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js index 1ecb101574b..ddefa8c5cd0 100644 --- a/spec/frontend/monitoring/components/empty_state_spec.js +++ b/spec/frontend/monitoring/components/empty_state_spec.js @@ -25,8 +25,8 @@ describe('EmptyState', () => { selectedState: dashboardEmptyStates.LOADING, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find(GlEmptyState).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false); }); it('shows gettingStarted state', () => { diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js index 31f52f6627b..104263e73e0 100644 --- a/spec/frontend/monitoring/components/graph_group_spec.js +++ b/spec/frontend/monitoring/components/graph_group_spec.js @@ -6,10 +6,10 @@ import GraphGroup from '~/monitoring/components/graph_group.vue'; describe('Graph group component', () => { let wrapper; - const findGroup = () => wrapper.find({ ref: 'graph-group' }); - const findContent = () => wrapper.find({ ref: 'graph-group-content' }); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findCaretIcon = () => wrapper.find(GlIcon); + const findGroup = () => wrapper.findComponent({ ref: 'graph-group' }); + const findContent = () => wrapper.findComponent({ ref: 'graph-group-content' }); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findCaretIcon = () => wrapper.findComponent(GlIcon); const findToggleButton = () => wrapper.find('[data-testid="group-toggle-button"]'); const createComponent = (propsData) => { diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js index 1dd2ed4e141..e3cd26b0e48 100644 --- a/spec/frontend/monitoring/components/group_empty_state_spec.js +++ b/spec/frontend/monitoring/components/group_empty_state_spec.js @@ -45,7 +45,7 @@ describe('GroupEmptyState', () => { }); it('passes the expected props to GlEmptyState', () => { - expect(wrapper.find(GlEmptyState).props()).toMatchSnapshot(); + expect(wrapper.findComponent(GlEmptyState).props()).toMatchSnapshot(); }); }); }); diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js index c9b5aeeecb8..94938e7f459 100644 --- a/spec/frontend/monitoring/components/links_section_spec.js +++ b/spec/frontend/monitoring/components/links_section_spec.js @@ -21,7 +21,7 @@ describe('Links Section component', () => { links, }; }; - const findLinks = () => wrapper.findAll(GlLink); + const findLinks = () => wrapper.findAllComponents(GlLink); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js index 0e45cc021c5..e00736954a9 100644 --- a/spec/frontend/monitoring/components/refresh_button_spec.js +++ b/spec/frontend/monitoring/components/refresh_button_spec.js @@ -15,9 +15,9 @@ describe('RefreshButton', () => { wrapper = shallowMount(RefreshButton, { store, ...options }); }; - const findRefreshBtn = () => wrapper.find(GlButton); - const findDropdown = () => wrapper.find(GlDropdown); - const findOptions = () => findDropdown().findAll(GlDropdownItem); + const findRefreshBtn = () => wrapper.findComponent(GlButton); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findOptions = () => findDropdown().findAllComponents(GlDropdownItem); const findOptionAt = (index) => findOptions().at(index); const expectFetchDataToHaveBeenCalledTimes = (times) => { diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js index 643bbb39f04..012e2e9c3e2 100644 --- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js +++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js @@ -27,8 +27,8 @@ describe('Custom variable component', () => { }); }; - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); it('renders dropdown element when all necessary props are passed', () => { createShallowWrapper(); diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js index 64b93bd3027..d6f8aac99aa 100644 --- a/spec/frontend/monitoring/components/variables_section_spec.js +++ b/spec/frontend/monitoring/components/variables_section_spec.js @@ -24,8 +24,8 @@ describe('Metrics dashboard/variables section component', () => { }); }; - const findTextInputs = () => wrapper.findAll(TextField); - const findCustomInputs = () => wrapper.findAll(DropdownField); + const findTextInputs = () => wrapper.findAllComponents(TextField); + const findCustomInputs = () => wrapper.findAllComponents(DropdownField); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js index c89cbc52bcb..fa112fca2db 100644 --- a/spec/frontend/monitoring/pages/panel_new_page_spec.js +++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js @@ -41,8 +41,8 @@ describe('monitoring/pages/panel_new_page', () => { }); }; - const findBackButton = () => wrapper.find(GlButtonStub); - const findPanelBuilder = () => wrapper.find(DashboardPanelBuilder); + const findBackButton = () => wrapper.findComponent(GlButtonStub); + const findPanelBuilder = () => wrapper.findComponent(DashboardPanelBuilder); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js index 7758dd351b7..368bd955fb3 100644 --- a/spec/frontend/monitoring/router_spec.js +++ b/spec/frontend/monitoring/router_spec.js @@ -61,8 +61,8 @@ describe('Monitoring router', () => { currentDashboard, }); - expect(wrapper.find(DashboardPage).exists()).toBe(true); - expect(wrapper.find(DashboardPage).find(Dashboard).exists()).toBe(true); + expect(wrapper.findComponent(DashboardPage).exists()).toBe(true); + expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true); }); }); @@ -84,8 +84,8 @@ describe('Monitoring router', () => { currentDashboard, }); - expect(wrapper.find(DashboardPage).exists()).toBe(true); - expect(wrapper.find(DashboardPage).find(Dashboard).exists()).toBe(true); + expect(wrapper.findComponent(DashboardPage).exists()).toBe(true); + expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true); }); }); @@ -100,7 +100,7 @@ describe('Monitoring router', () => { const wrapper = createWrapper(BASE_PATH, path); expect(wrapper.vm.$route.params.dashboard).toBe(currentDashboard); - expect(wrapper.find(PanelNewPage).exists()).toBe(true); + expect(wrapper.findComponent(PanelNewPage).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index c25de8caa95..54f9c59308e 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -511,10 +511,10 @@ describe('mapToDashboardViewModel', () => { describe('uniqMetricsId', () => { [ { input: { id: 1 }, expected: `${NOT_IN_DB_PREFIX}_1` }, - { input: { metric_id: 2 }, expected: '2_undefined' }, - { input: { metric_id: 2, id: 21 }, expected: '2_21' }, - { input: { metric_id: 22, id: 1 }, expected: '22_1' }, - { input: { metric_id: 'aaa', id: '_a' }, expected: 'aaa__a' }, + { input: { metricId: 2 }, expected: '2_undefined' }, + { input: { metricId: 2, id: 21 }, expected: '2_21' }, + { input: { metricId: 22, id: 1 }, expected: '22_1' }, + { input: { metricId: 'aaa', id: '_a' }, expected: 'aaa__a' }, ].forEach(({ input, expected }) => { it(`creates unique metric ID with ${JSON.stringify(input)}`, () => { expect(uniqMetricsId(input)).toEqual(expected); diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js index 70df05a2781..6cfbdb16111 100644 --- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js +++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js @@ -124,7 +124,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { }); it('clicked on link with view', () => { - expect(primaryLink.props('menuItem').view).toBeTruthy(); + expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace); }); it('changes active view', () => { diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js index d69c2c4adfa..6662492fd81 100644 --- a/spec/frontend/notes/components/comment_field_layout_spec.js +++ b/spec/frontend/notes/components/comment_field_layout_spec.js @@ -22,8 +22,8 @@ describe('Comment Field Layout Component', () => { confidential_issues_docs_path: CONFIDENTIAL_ISSUES_DOCS_PATH, }; - const findIssuableNoteWarning = () => wrapper.find(NoteableWarning); - const findEmailParticipantsWarning = () => wrapper.find(EmailParticipantsWarning); + const findIssuableNoteWarning = () => wrapper.findComponent(NoteableWarning); + const findEmailParticipantsWarning = () => wrapper.findComponent(EmailParticipantsWarning); const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container'); const createWrapper = (props = {}, slots = {}) => { diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js index 7878737fd31..5800f68b114 100644 --- a/spec/frontend/notes/components/diff_discussion_header_spec.js +++ b/spec/frontend/notes/components/diff_discussion_header_spec.js @@ -1,6 +1,7 @@ -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { GlAvatar, GlAvatarLink } from '@gitlab/ui'; import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue'; import createStore from '~/notes/stores'; @@ -15,7 +16,7 @@ describe('diff_discussion_header component', () => { window.mrTabs = {}; store = createStore(); - wrapper = mount(diffDiscussionHeader, { + wrapper = shallowMount(diffDiscussionHeader, { store, propsData: { discussion: discussionMock }, }); @@ -25,15 +26,25 @@ describe('diff_discussion_header component', () => { wrapper.destroy(); }); - it('should render user avatar', async () => { - const discussion = { ...discussionMock }; - discussion.diff_file = mockDiffFile; - discussion.diff_discussion = true; + describe('Avatar', () => { + const firstNoteAuthor = discussionMock.notes[0].author; + const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); + const findAvatar = () => wrapper.findComponent(GlAvatar); - wrapper.setProps({ discussion }); + it('should render user avatar and user avatar link', () => { + expect(findAvatar().exists()).toBe(true); + expect(findAvatarLink().exists()).toBe(true); + }); + + it('renders avatar of the first note author', () => { + const props = findAvatar().props(); - await nextTick(); - expect(wrapper.find('.user-avatar-link').exists()).toBe(true); + expect(props).toMatchObject({ + src: firstNoteAuthor.avatar_url, + alt: firstNoteAuthor.name, + size: { default: 24, md: 32 }, + }); + }); }); describe('action text', () => { diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index 925dbcc09ec..d16c13d6fd3 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -47,9 +47,9 @@ describe('DiscussionActions', () => { it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => { createComponent(); - expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true); - expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(true); - expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(true); + expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true); + expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(true); + expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(true); }); it('only renders reply placholder if disccusion is not resolvable', () => { @@ -57,15 +57,15 @@ describe('DiscussionActions', () => { discussion.resolvable = false; createComponent({ discussion }); - expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true); - expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(false); - expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false); + expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true); + expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(false); + expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(false); }); it('does not render resolve with issue button if resolveWithIssuePath is falsy', () => { createComponent({ resolveWithIssuePath: '' }); - expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false); + expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(false); }); describe.each` @@ -82,8 +82,8 @@ describe('DiscussionActions', () => { }); it(shouldRender ? 'renders resolve buttons' : 'does not render resolve buttons', () => { - expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(shouldRender); - expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(shouldRender); + expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(shouldRender); + expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(shouldRender); }); }); }); @@ -95,7 +95,7 @@ describe('DiscussionActions', () => { createComponent({}, { attachTo: document.body }); jest.spyOn(wrapper.vm, '$emit'); - wrapper.find(ReplyPlaceholder).find('textarea').trigger('focus'); + wrapper.findComponent(ReplyPlaceholder).find('textarea').trigger('focus'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm'); }); @@ -103,7 +103,7 @@ describe('DiscussionActions', () => { createComponent(); jest.spyOn(wrapper.vm, '$emit'); - wrapper.find(ResolveDiscussionButton).find('button').trigger('click'); + wrapper.findComponent(ResolveDiscussionButton).find('button').trigger('click'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve'); }); }); diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index f016cef18e6..a7e2f1efa09 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -47,7 +47,7 @@ describe('DiscussionCounter component', () => { it('does not render', () => { wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); - expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false); }); }); @@ -57,7 +57,7 @@ describe('DiscussionCounter component', () => { store.dispatch('updateResolvableDiscussionsCounts'); wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); - expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false); }); }); @@ -77,7 +77,7 @@ describe('DiscussionCounter component', () => { updateStore(); wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); - expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(true); }); it.each` @@ -103,7 +103,7 @@ describe('DiscussionCounter component', () => { updateStore({ resolvable: true, resolved }); wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); - expect(wrapper.findAll(GlButton)).toHaveLength(groupLength); + expect(wrapper.findAllComponents(GlButton)).toHaveLength(groupLength); }); }); diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js index ad9a2e898eb..48f5030aa1a 100644 --- a/spec/frontend/notes/components/discussion_filter_note_spec.js +++ b/spec/frontend/notes/components/discussion_filter_note_spec.js @@ -31,14 +31,14 @@ describe('DiscussionFilterNote component', () => { it('emits `dropdownSelect` event with 0 parameter on clicking Show all activity button', () => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - wrapper.findAll(GlButton).at(0).vm.$emit('click'); + wrapper.findAllComponents(GlButton).at(0).vm.$emit('click'); expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 0); }); it('emits `dropdownSelect` event with 1 parameter on clicking Show comments only button', () => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - wrapper.findAll(GlButton).at(1).vm.$emit('click'); + wrapper.findAllComponents(GlButton).at(1).vm.$emit('click'); expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); }); diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 3506b6ac9f3..1b8b6bec490 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -61,13 +61,13 @@ describe('DiscussionNotes', () => { it('renders an element for each note in the discussion', () => { createComponent(); const notesCount = discussionMock.notes.length; - const els = wrapper.findAll(NoteableNote); + const els = wrapper.findAllComponents(NoteableNote); expect(els.length).toBe(notesCount); }); it('renders one element if replies groupping is enabled', () => { createComponent({ shouldGroupReplies: true }); - const els = wrapper.findAll(NoteableNote); + const els = wrapper.findAllComponents(NoteableNote); expect(els.length).toBe(1); }); diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js index 3932f818c4e..971e3987929 100644 --- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js +++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js @@ -15,7 +15,7 @@ describe('ReplyPlaceholder', () => { }); }; - const findTextarea = () => wrapper.find({ ref: 'textarea' }); + const findTextarea = () => wrapper.findComponent({ ref: 'textarea' }); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js index ca0c0ca6de8..17c3523cf48 100644 --- a/spec/frontend/notes/components/discussion_resolve_button_spec.js +++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js @@ -28,7 +28,7 @@ describe('resolveDiscussionButton', () => { }); it('should emit a onClick event on button click', async () => { - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); button.vm.$emit('click'); @@ -39,7 +39,7 @@ describe('resolveDiscussionButton', () => { }); it('should contain the provided button title', () => { - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); expect(button.text()).toContain(buttonTitle); }); @@ -52,7 +52,7 @@ describe('resolveDiscussionButton', () => { }, }); - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); expect(button.props('loading')).toEqual(true); }); @@ -65,7 +65,7 @@ describe('resolveDiscussionButton', () => { }, }); - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); await nextTick(); expect(button.props('loading')).toEqual(false); diff --git a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js index 5bc6282db03..71406eeb7b4 100644 --- a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js +++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js @@ -20,7 +20,7 @@ describe('ResolveWithIssueButton', () => { }); it('it should have a link with the provided link property as href', () => { - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); expect(button.attributes().href).toBe(url); }); diff --git a/spec/frontend/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js index 4993ded365d..20b32b8c178 100644 --- a/spec/frontend/notes/components/note_actions/reply_button_spec.js +++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js @@ -15,7 +15,7 @@ describe('ReplyButton', () => { }); it('emits startReplying on click', () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); expect(wrapper.emitted('startReplying')).toEqual([[]]); }); diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index bf5a6b4966a..cbe11c20798 100644 --- a/spec/frontend/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js @@ -16,7 +16,7 @@ describe('noteActions', () => { let actions; let axiosMock; - const findUserAccessRoleBadge = (idx) => wrapper.findAll(UserAccessRoleBadge).at(idx); + const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx); const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim(); const mountNoteActions = (propsData, computed) => { @@ -159,7 +159,7 @@ describe('noteActions', () => { }); }); - describe('when a user has access to edit an issue', () => { + describe('when a user can set metadata of an issue', () => { const testButtonClickTriggersAction = () => { axiosMock.onPut(`${TEST_HOST}/api/v4/projects/group/project/issues/1`).reply(() => { expect(actions.updateAssignees).toHaveBeenCalled(); @@ -176,7 +176,7 @@ describe('noteActions', () => { }); store.state.noteableData = { current_user: { - can_update: true, + can_set_issue_metadata: true, }, }; store.state.userData = userDataMock; @@ -191,6 +191,31 @@ describe('noteActions', () => { it('should be possible to unassign the comment author', testButtonClickTriggersAction); }); + describe('when a user can update but not set metadata of an issue', () => { + beforeEach(() => { + wrapper = mountNoteActions(props, { + targetType: () => 'issue', + }); + store.state.noteableData = { + current_user: { + can_update: true, + can_set_issue_metadata: false, + }, + }; + store.state.userData = userDataMock; + }); + + afterEach(() => { + wrapper.destroy(); + axiosMock.restore(); + }); + + it('should not be possible to assign or unassign the comment author', () => { + const assignUserButton = wrapper.find('[data-testid="assign-user"]'); + expect(assignUserButton.exists()).toBe(false); + }); + }); + describe('when a user does not have access to edit an issue', () => { const testButtonDoesNotRender = () => { const assignUserButton = wrapper.find('[data-testid="assign-user"]'); @@ -241,7 +266,7 @@ describe('noteActions', () => { }); it('shows a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); + const replyButton = wrapper.findComponent({ ref: 'replyButton' }); expect(replyButton.exists()).toBe(true); }); @@ -256,7 +281,7 @@ describe('noteActions', () => { }); it('does not show a reply button', () => { - const replyButton = wrapper.find({ ref: 'replyButton' }); + const replyButton = wrapper.findComponent({ ref: 'replyButton' }); expect(replyButton.exists()).toBe(false); }); @@ -270,7 +295,7 @@ describe('noteActions', () => { }); it('should render the right resolve button title', () => { - const resolveButton = wrapper.find({ ref: 'resolveButton' }); + const resolveButton = wrapper.findComponent({ ref: 'resolveButton' }); expect(resolveButton.exists()).toBe(true); expect(resolveButton.attributes('title')).toBe('Thread stays unresolved'); diff --git a/spec/frontend/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js index d47c2beaaf8..24632f8e427 100644 --- a/spec/frontend/notes/components/note_attachment_spec.js +++ b/spec/frontend/notes/components/note_attachment_spec.js @@ -4,8 +4,8 @@ import NoteAttachment from '~/notes/components/note_attachment.vue'; describe('Issue note attachment', () => { let wrapper; - const findImage = () => wrapper.find({ ref: 'attachmentImage' }); - const findUrl = () => wrapper.find({ ref: 'attachmentUrl' }); + const findImage = () => wrapper.findComponent({ ref: 'attachmentImage' }); + const findUrl = () => wrapper.findComponent({ ref: 'attachmentUrl' }); const createComponent = (attachment) => { wrapper = shallowMount(NoteAttachment, { diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index 0f765a8da87..c2e56d3e7a7 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -7,7 +7,6 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue'; import NoteForm from '~/notes/components/note_form.vue'; import createStore from '~/notes/stores'; import notes from '~/notes/stores/modules/index'; -import { INTERNAL_NOTE_CLASSES } from '~/notes/constants'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; @@ -59,22 +58,10 @@ describe('issue_note_body component', () => { expect(wrapper.findComponent(NoteAwardsList).exists()).toBe(true); }); - it('should not have internal note classes', () => { - expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual( - INTERNAL_NOTE_CLASSES, - ); - }); - describe('isInternalNote', () => { beforeEach(() => { wrapper = createComponent({ props: { isInternalNote: true } }); }); - - it('should have internal note classes', () => { - expect(wrapper.findByTestId('note-internal-container').classes()).toEqual( - INTERNAL_NOTE_CLASSES, - ); - }); }); describe('isEditing', () => { @@ -110,12 +97,6 @@ describe('issue_note_body component', () => { beforeEach(() => { wrapper.setProps({ isInternalNote: true }); }); - - it('should not have internal note classes', () => { - expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual( - INTERNAL_NOTE_CLASSES, - ); - }); }); }); @@ -162,7 +143,7 @@ describe('issue_note_body component', () => { }); it('passes the correct default placeholder commit message for a suggestion to the suggestions component', () => { - const commitMessage = wrapper.find(Suggestions).attributes('defaultcommitmessage'); + const commitMessage = wrapper.findComponent(Suggestions).attributes('defaultcommitmessage'); expect(commitMessage).toBe('branch/pathnameuseruser usertonabc11'); }); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 252c24d1117..fad04e9063d 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -6,6 +6,7 @@ import { getDraft, updateDraft } from '~/lib/utils/autosave'; import NoteForm from '~/notes/components/note_form.vue'; import createStore from '~/notes/stores'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete'; import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data'; jest.mock('~/lib/utils/autosave'); @@ -91,7 +92,7 @@ describe('issue_note_form component', () => { expect(conflictWarning.exists()).toBe(true); expect(conflictWarning.text().replace(/\s+/g, ' ').trim()).toBe(message); - expect(conflictWarning.find(GlLink).attributes('href')).toBe('#note_545'); + expect(conflictWarning.findComponent(GlLink).attributes('href')).toBe('#note_545'); }); }); @@ -133,7 +134,7 @@ describe('issue_note_form component', () => { it('should link to markdown docs', () => { const { markdownDocsPath } = notesDataMock; - const markdownField = wrapper.find(MarkdownField); + const markdownField = wrapper.findComponent(MarkdownField); const markdownFieldProps = markdownField.props(); expect(markdownFieldProps.markdownDocsPath).toBe(markdownDocsPath); @@ -201,6 +202,21 @@ describe('issue_note_form component', () => { expect(wrapper.emitted().cancelForm).toHaveLength(1); }); + it('will not cancel form if there is an active at-who-active class', async () => { + wrapper.setProps({ + ...props, + }); + await nextTick(); + + const textareaEl = wrapper.vm.$refs.textarea; + const cancelButton = findCancelButton(); + textareaEl.classList.add(AT_WHO_ACTIVE_CLASS); + cancelButton.vm.$emit('click'); + await nextTick(); + + expect(wrapper.emitted().cancelForm).toBeUndefined(); + }); + it('should be possible to update the note', async () => { wrapper.setProps({ ...props, diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js index ad2cf1c5a35..43fbc5e26dc 100644 --- a/spec/frontend/notes/components/note_header_spec.js +++ b/spec/frontend/notes/components/note_header_spec.js @@ -15,15 +15,15 @@ const actions = { describe('NoteHeader component', () => { let wrapper; - const findActionsWrapper = () => wrapper.find({ ref: 'discussionActions' }); + const findActionsWrapper = () => wrapper.findComponent({ ref: 'discussionActions' }); const findToggleThreadButton = () => wrapper.findByTestId('thread-toggle'); - const findChevronIcon = () => wrapper.find({ ref: 'chevronIcon' }); - const findActionText = () => wrapper.find({ ref: 'actionText' }); - const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' }); - const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' }); + const findChevronIcon = () => wrapper.findComponent({ ref: 'chevronIcon' }); + const findActionText = () => wrapper.findComponent({ ref: 'actionText' }); + const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' }); + const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' }); const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator'); - const findSpinner = () => wrapper.find({ ref: 'spinner' }); - const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' }); + const findSpinner = () => wrapper.findComponent({ ref: 'spinner' }); + const findAuthorStatus = () => wrapper.findComponent({ ref: 'authorStatus' }); const statusHtml = '"<span class="user-status-emoji has-tooltip" title="foo bar" data-html="true" data-placement="top"><gl-emoji title="basketball and hoop" data-name="basketball" data-unicode-version="6.0">🏀</gl-emoji></span>"'; @@ -228,7 +228,7 @@ describe('NoteHeader component', () => { const dispatchEvent = jest.spyOn(wrapper.vm.$refs.authorNameLink, 'dispatchEvent'); - wrapper.find({ ref: 'authorUsernameLink' }).trigger('mouseenter'); + wrapper.findComponent({ ref: 'authorUsernameLink' }).trigger('mouseenter'); expect(dispatchEvent).toHaveBeenCalledWith(new Event('mouseenter')); }); @@ -238,7 +238,7 @@ describe('NoteHeader component', () => { const dispatchEvent = jest.spyOn(wrapper.vm.$refs.authorNameLink, 'dispatchEvent'); - wrapper.find({ ref: 'authorUsernameLink' }).trigger('mouseleave'); + wrapper.findComponent({ ref: 'authorUsernameLink' }).trigger('mouseleave'); expect(dispatchEvent).toHaveBeenCalledWith(new Event('mouseleave')); }); @@ -266,8 +266,8 @@ describe('NoteHeader component', () => { it('toggles hover specific CSS classes on author name link', async () => { createComponent({ author }); - const authorUsernameLink = wrapper.find({ ref: 'authorUsernameLink' }); - const authorNameLink = wrapper.find({ ref: 'authorNameLink' }); + const authorUsernameLink = wrapper.findComponent({ ref: 'authorUsernameLink' }); + const authorNameLink = wrapper.findComponent({ ref: 'authorNameLink' }); authorUsernameLink.trigger('mouseenter'); diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index 603db56a098..b34305688d9 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -73,13 +73,13 @@ describe('noteable_discussion component', () => { expect(wrapper.vm.isReplying).toEqual(false); - const replyPlaceholder = wrapper.find(ReplyPlaceholder); + const replyPlaceholder = wrapper.findComponent(ReplyPlaceholder); replyPlaceholder.vm.$emit('focus'); await nextTick(); expect(wrapper.vm.isReplying).toEqual(true); - const noteForm = wrapper.find(NoteForm); + const noteForm = wrapper.findComponent(NoteForm); expect(noteForm.exists()).toBe(true); @@ -100,11 +100,11 @@ describe('noteable_discussion component', () => { wrapper.setProps({ discussion: { ...discussionMock, confidential: isNoteInternal } }); await nextTick(); - const replyPlaceholder = wrapper.find(ReplyPlaceholder); + const replyPlaceholder = wrapper.findComponent(ReplyPlaceholder); replyPlaceholder.vm.$emit('focus'); await nextTick(); - expect(wrapper.find(NoteForm).props('saveButtonTitle')).toBe(saveButtonTitle); + expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(saveButtonTitle); }, ); @@ -116,7 +116,7 @@ describe('noteable_discussion component', () => { await nextTick(); - wrapper.find(DiscussionNotes).vm.$emit('startReplying'); + wrapper.findComponent(DiscussionNotes).vm.$emit('startReplying'); await nextTick(); @@ -139,7 +139,7 @@ describe('noteable_discussion component', () => { }); it('does not display a button to resolve with issue', () => { - const button = wrapper.find(ResolveWithIssueButton); + const button = wrapper.findComponent(ResolveWithIssueButton); expect(button.exists()).toBe(false); }); @@ -159,7 +159,7 @@ describe('noteable_discussion component', () => { }); it('displays a button to resolve with issue', () => { - const button = wrapper.find(ResolveWithIssueButton); + const button = wrapper.findComponent(ResolveWithIssueButton); expect(button.exists()).toBe(true); }); diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index 3350609bb90..e049c5bc0c8 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -285,11 +285,25 @@ describe('issue_note', () => { await waitForPromises(); expect(alertSpy).not.toHaveBeenCalled(); expect(wrapper.vm.note.note_html).toBe( - '<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"></p>\n', + '<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">', ); }); }); + describe('internal note', () => { + it('has internal note class for internal notes', () => { + createWrapper({ note: { ...note, confidential: true } }); + + expect(wrapper.classes()).toContain('internal-note'); + }); + + it('does not have internal note class for external notes', () => { + createWrapper({ note }); + + expect(wrapper.classes()).not.toContain('internal-note'); + }); + }); + describe('cancel edit', () => { beforeEach(() => { createWrapper(); @@ -357,7 +371,7 @@ describe('issue_note', () => { createWrapper(); updateActions(); wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params); - expect(wrapper.emitted('handleUpdateNote')).toBeTruthy(); + expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1); }); it('does not stringify empty position', () => { diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index 36a68118fa7..d4cb07d97dc 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -4,7 +4,6 @@ import $ from 'jquery'; import { nextTick } from 'vue'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import setWindowLocation from 'helpers/set_window_location_helper'; -import { setTestTimeout } from 'helpers/timeout'; import waitForPromises from 'helpers/wait_for_promises'; import DraftNote from '~/batch_comments/components/draft_note.vue'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; @@ -19,8 +18,6 @@ import '~/behaviors/markdown/render_gfm'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import * as mockData from '../mock_data'; -setTestTimeout(1000); - const TYPE_COMMENT_FORM = 'comment-form'; const TYPE_NOTES_LIST = 'notes-list'; @@ -359,7 +356,7 @@ describe('note_app', () => { }); it('should listen hashchange event', () => { - const notesApp = wrapper.find(NotesApp); + const notesApp = wrapper.findComponent(NotesApp); const hash = 'some dummy hash'; jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash); const setTargetNoteHash = jest.spyOn(notesApp.vm, 'setTargetNoteHash'); @@ -439,7 +436,7 @@ describe('note_app', () => { }); it('correctly finds only draft comments', () => { - const drafts = wrapper.findAll(DraftNote).wrappers; + const drafts = wrapper.findAllComponents(DraftNote).wrappers; expect(drafts.map((x) => x.props('draft'))).toEqual( mockData.draftComments.map(({ note }) => expect.objectContaining({ note })), diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js index bde27b7e5fc..8b6e05da3c0 100644 --- a/spec/frontend/notes/components/sort_discussion_spec.js +++ b/spec/frontend/notes/components/sort_discussion_spec.js @@ -21,7 +21,7 @@ describe('Sort Discussion component', () => { }); }; - const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/notes/components/timeline_toggle_spec.js b/spec/frontend/notes/components/timeline_toggle_spec.js index 84fa3008835..cf79416d300 100644 --- a/spec/frontend/notes/components/timeline_toggle_spec.js +++ b/spec/frontend/notes/components/timeline_toggle_spec.js @@ -27,7 +27,7 @@ describe('Timeline toggle', () => { }); }; - const findGlButton = () => wrapper.find(GlButton); + const findGlButton = () => wrapper.findComponent(GlButton); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js index 40b124b9029..d5e2a189afe 100644 --- a/spec/frontend/notes/deprecated_notes_spec.js +++ b/spec/frontend/notes/deprecated_notes_spec.js @@ -7,7 +7,6 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { createSpyObj } from 'helpers/jest_helpers'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; -import { setTestTimeoutOnce } from 'helpers/timeout'; import axios from '~/lib/utils/axios_utils'; import * as urlUtility from '~/lib/utils/url_utility'; @@ -48,7 +47,6 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { // random failures. // It seems that running tests in parallel increases failure rate. jest.setTimeout(4000); - setTestTimeoutOnce(4000); }); afterEach(async () => { @@ -510,7 +508,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { notes.putEditFormInPlace($el); - expect(notes.glForm.enableGFM).toBeTruthy(); + expect(notes.glForm.enableGFM).toBe(''); }); }); @@ -783,21 +781,21 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const hasQuickActions = notes.hasQuickActions(sampleComment); - expect(hasQuickActions).toBeTruthy(); + expect(hasQuickActions).toBe(true); }); it('should return false when comment does NOT begin with a quick action', () => { const sampleComment = 'Hey, /unassign Merging this'; const hasQuickActions = notes.hasQuickActions(sampleComment); - expect(hasQuickActions).toBeFalsy(); + expect(hasQuickActions).toBe(false); }); it('should return false when comment does NOT have any quick actions', () => { const sampleComment = 'Looking good, Awesome!'; const hasQuickActions = notes.hasQuickActions(sampleComment); - expect(hasQuickActions).toBeFalsy(); + expect(hasQuickActions).toBe(false); }); }); @@ -887,14 +885,14 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.attr('id')).toEqual(uniqueId); - expect($tempNote.hasClass('being-posted')).toBeTruthy(); - expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); + expect($tempNote.hasClass('being-posted')).toBe(true); + expect($tempNote.hasClass('fade-in-half')).toBe(true); $tempNote.find('.timeline-icon > a, .note-header-info > a').each((i, el) => { expect(el.getAttribute('href')).toEqual(`/${currentUsername}`); }); expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar); - expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBe(false); expect($tempNoteHeader.find('.d-none.d-sm-inline-block').text().trim()).toEqual( currentUserFullname, ); @@ -916,7 +914,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { }); expect($tempNote.prop('nodeName')).toEqual('LI'); - expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBe(true); }); it('should return a escaped user name', () => { @@ -954,8 +952,8 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.attr('id')).toEqual(uniqueId); - expect($tempNote.hasClass('being-posted')).toBeTruthy(); - expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); + expect($tempNote.hasClass('being-posted')).toBe(true); + expect($tempNote.hasClass('fade-in-half')).toBe(true); expect($tempNote.find('.timeline-content i').text().trim()).toEqual(sampleCommandDescription); }); }); diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js index c1fa1d24a82..21145466016 100644 --- a/spec/frontend/operation_settings/components/metrics_settings_spec.js +++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js @@ -105,7 +105,10 @@ describe('operation settings external dashboard component', () => { it('uses description text', () => { const description = formGroup.find('small'); - expect(description.text()).not.toBeFalsy(); + const expectedDescription = + "Choose whether to display dashboard metrics in UTC or the user's local timezone."; + + expect(description.text()).toBe(expectedDescription); }); }); @@ -138,7 +141,10 @@ describe('operation settings external dashboard component', () => { it('uses description text', () => { const description = formGroup.find('small'); - expect(description.text()).not.toBeFalsy(); + const expectedDescription = + 'Add a button to the metrics dashboard linking directly to your existing external dashboard.'; + + expect(description.text()).toBe(expectedDescription); }); }); @@ -151,7 +157,6 @@ describe('operation settings external dashboard component', () => { }); it('defaults to externalDashboardUrl', () => { - expect(input.attributes().value).toBeTruthy(); expect(input.attributes().value).toBe(externalDashboardUrl); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js index ef6c4a1fa32..b163557618e 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js @@ -4,7 +4,6 @@ import { GlEmptyState } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { stripTypenames } from 'helpers/graphql_helpers'; import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue'; import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue'; @@ -96,8 +95,8 @@ describe('Tags List', () => { it('binds the correct props', () => { expect(findRegistryList().props()).toMatchObject({ title: '2 tags', - pagination: stripTypenames(tagsPageInfo), - items: stripTypenames(tags), + pagination: tagsPageInfo, + items: tags, idProperty: 'name', }); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js index a5b2b1d7cf8..61503d0f3bf 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js @@ -90,18 +90,26 @@ describe('cleanup_status', () => { `( 'when the status is $status is $visible that the extra icon is visible', ({ status, visible }) => { - mountComponent({ status }); + mountComponent({ status, expirationPolicy: { next_run_at: '2063-04-08T01:44:03Z' } }); expect(findExtraInfoIcon().exists()).toBe(visible); }, ); + it(`when the status is ${UNFINISHED_STATUS} & expirationPolicy does not exist the extra icon is not visible`, () => { + mountComponent({ + status: UNFINISHED_STATUS, + }); + + expect(findExtraInfoIcon().exists()).toBe(false); + }); + it(`has a popover with a learn more link and a time frame for the next run`, () => { jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); mountComponent({ status: UNFINISHED_STATUS, - expirationPolicy: { next_run: '2063-04-08T01:44:03Z' }, + expirationPolicy: { next_run_at: '2063-04-08T01:44:03Z' }, }); expect(findPopover().exists()).toBe(true); @@ -113,7 +121,7 @@ describe('cleanup_status', () => { it('id matches popover target attribute', () => { mountComponent({ status: UNFINISHED_STATUS, - next_run_at: '2063-04-08T01:44:03Z', + expirationPolicy: { next_run_at: '2063-04-08T01:44:03Z' }, }); const id = findExtraInfoIcon().attributes('id'); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js index f9739509ef9..b11048cd7a2 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js @@ -13,7 +13,7 @@ export const imagesListResponse = [ expirationPolicyCleanupStatus: 'UNSCHEDULED', project: { id: 'gid://gitlab/Project/22', - path: 'gitlab-test', + path: 'GITLAB-TEST', }, }, { diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js index f2901148e17..fb50d623543 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js @@ -50,6 +50,7 @@ describe('DependencyProxyApp', () => { groupPath: 'gitlab-org', groupId: dummyGrouptId, noManifestsIllustration: 'noManifestsIllustration', + canClearCache: true, }; function createComponent({ provide = provideDefaults } = {}) { @@ -268,6 +269,23 @@ describe('DependencyProxyApp', () => { 'All items in the cache are scheduled for removal.', ); }); + + describe('when user has no permission to clear cache', () => { + beforeEach(() => { + createComponent({ + provide: { + groupPath: 'gitlab-org', + groupId: dummyGrouptId, + noManifestsIllustration: 'noManifestsIllustration', + canClearCache: false, + }, + }); + }); + + it('does not show the clear cache dropdown list', () => { + expect(findClearCacheDropdownList().exists()).toBe(false); + }); + }); }); }); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js index 79c1b18c9f9..721bdd34a4f 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js @@ -128,7 +128,7 @@ describe('packages_list_row', () => { findDeleteButton().vm.$emit('click'); await nextTick(); - expect(wrapper.emitted('packageToDelete')).toBeTruthy(); + expect(wrapper.emitted('packageToDelete')).toHaveLength(1); expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap index fdddc131412..61923233d2e 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap @@ -29,19 +29,25 @@ exports[`PackageTitle renders with tags 1`] = ` <div class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3" > - <span + <div + class="gl-display-flex gl-gap-3" data-testid="sub-header" > v 1.0.0 published <time-ago-tooltip-stub - class="gl-ml-2" cssclass="" time="2020-08-17T14:23:32Z" tooltipplacement="top" /> - </span> + + <package-tags-stub + hidelabel="true" + tagdisplaylimit="2" + tags="[object Object],[object Object],[object Object]" + /> + </div> </div> </div> </div> @@ -73,15 +79,6 @@ exports[`PackageTitle renders with tags 1`] = ` texttooltip="" /> </div> - <div - class="gl-display-flex gl-align-items-center gl-mr-5" - > - <package-tags-stub - hidelabel="true" - tagdisplaylimit="2" - tags="[object Object],[object Object],[object Object]" - /> - </div> </div> </div> @@ -121,19 +118,21 @@ exports[`PackageTitle renders without tags 1`] = ` <div class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3" > - <span + <div + class="gl-display-flex gl-gap-3" data-testid="sub-header" > v 1.0.0 published <time-ago-tooltip-stub - class="gl-ml-2" cssclass="" time="2020-08-17T14:23:32Z" tooltipplacement="top" /> - </span> + + <!----> + </div> </div> </div> </div> diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap index 06ae8645101..92c2cd90568 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap @@ -2,19 +2,161 @@ exports[`PypiInstallation renders all the messages 1`] = ` <div> - <installation-title-stub - options="[object Object]" - packagetype="pypi" - /> + <div + class="gl-display-flex gl-justify-content-space-between gl-align-items-center" + > + <h3 + class="gl-font-lg" + > + Installation + </h3> + + <div> + <div + class="dropdown b-dropdown gl-new-dropdown btn-group" + id="__BVID__27" + lazy="" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" + id="__BVID__27__BV_toggle_" + type="button" + > + <!----> + + <!----> + + <span + class="gl-new-dropdown-button-text" + > + Show PyPi commands + </span> + + <svg + aria-hidden="true" + class="gl-button-icon dropdown-chevron gl-icon s16" + data-testid="chevron-down-icon" + role="img" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + aria-labelledby="__BVID__27__BV_toggle_" + class="dropdown-menu" + role="menu" + tabindex="-1" + > + <!----> + </ul> + </div> + </div> + </div> - <code-instruction-stub - copytext="Copy Pip command" - data-testid="pip-command" - instruction="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple" - label="Pip Command" - trackingaction="copy_pip_install_command" - trackinglabel="code_instruction" - /> + <fieldset + aria-describedby="installation-pip-command-group__BV_description_" + class="form-group gl-form-group" + id="installation-pip-command-group" + > + <legend + class="bv-no-focus-ring col-form-label pt-0 col-form-label" + id="installation-pip-command-group__BV_label_" + tabindex="-1" + > + + + + <!----> + + <!----> + </legend> + <div + aria-labelledby="installation-pip-command-group__BV_label_" + class="bv-no-focus-ring" + role="group" + tabindex="-1" + > + <div + data-testid="pip-command" + id="installation-pip-command" + > + <label + for="instruction-input_5" + > + Pip Command + </label> + + <div + class="gl-mb-3" + > + <div + class="input-group gl-mb-3" + > + <input + class="form-control gl-font-monospace" + data-testid="instruction-input" + id="instruction-input_5" + readonly="readonly" + type="text" + /> + + <span + class="input-group-append" + data-testid="instruction-button" + > + <button + aria-label="Copy Pip command" + aria-live="polite" + class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" + data-clipboard-handle-tooltip="false" + data-clipboard-text="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple" + id="clipboard-button-6" + title="Copy Pip command" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="copy-to-clipboard-icon" + role="img" + > + <use + href="#copy-to-clipboard" + /> + </svg> + + <!----> + </button> + </span> + </div> + </div> + </div> + <!----> + <!----> + <small + class="form-text text-muted" + id="installation-pip-command-group__BV_description_" + tabindex="-1" + > + You will need a + <a + class="gl-link" + data-testid="access-token-link" + href="/help/user/profile/personal_access_tokens" + > + personal access token + </a> + . + </small> + </div> + </fieldset> <h3 class="gl-font-lg" @@ -30,25 +172,33 @@ exports[`PypiInstallation renders all the messages 1`] = ` file. </p> - <code-instruction-stub - copytext="Copy .pypirc content" + <div data-testid="pypi-setup-content" - instruction="[gitlab] + > + <!----> + + <div> + <pre + class="gl-font-monospace" + data-testid="multiline-instruction" + > + [gitlab] repository = http://gdk.test:3000/api/v4/projects/1/packages/pypi username = __token__ -password = <your personal access token>" - label="" - multiline="true" - trackingaction="copy_pypi_setup_command" - trackinglabel="code_instruction" - /> +password = <your personal access token> + </pre> + </div> + </div> For more information on the PyPi registry, - <gl-link-stub + <a + class="gl-link" + data-testid="pypi-docs-link" href="/help/user/packages/pypi_repository/index" + rel="noopener" target="_blank" > see the documentation - </gl-link-stub> + </a> . </div> `; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js index 0447ead0830..529a6a22ddf 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -1,6 +1,6 @@ -import { GlDropdown, GlButton } from '@gitlab/ui'; +import { GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui'; import { nextTick } from 'vue'; -import stubChildren from 'helpers/stub_children'; +import { stubComponent } from 'helpers/stub_component'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; @@ -11,6 +11,7 @@ describe('Package Files', () => { let wrapper; const findAllRows = () => wrapper.findAllByTestId('file-row'); + const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected'); const findFirstRow = () => extendedWrapper(findAllRows().at(0)); const findSecondRow = () => extendedWrapper(findAllRows().at(1)); const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link'); @@ -22,19 +23,27 @@ describe('Package Files', () => { const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file'); const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton); const findFirstRowShaComponent = (id) => wrapper.findByTestId(id); + const findCheckAllCheckbox = () => wrapper.findByTestId('package-files-checkbox-all'); + const findAllRowCheckboxes = () => wrapper.findAllByTestId('package-files-checkbox'); const files = packageFilesMock(); const [file] = files; - const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => { + const createComponent = ({ + packageFiles = [file], + isLoading = false, + canDelete = true, + stubs, + } = {}) => { wrapper = mountExtended(PackageFiles, { propsData: { canDelete, + isLoading, packageFiles, }, stubs: { - ...stubChildren(PackageFiles), - GlTableLite: false, + GlTable: false, + ...stubs, }, }); }; @@ -157,43 +166,170 @@ describe('Package Files', () => { expect(findSecondRowCommitLink().exists()).toBe(false); }); }); + }); - describe('action menu', () => { - describe('when the user can delete', () => { - it('exists', () => { - createComponent(); + describe('action menu', () => { + describe('when the user can delete', () => { + it('exists', () => { + createComponent(); - expect(findFirstActionMenu().exists()).toBe(true); - }); + expect(findFirstActionMenu().exists()).toBe(true); + expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v'); + expect(findFirstActionMenu().props('textSrOnly')).toBe(true); + expect(findFirstActionMenu().props('text')).toMatchInterpolatedText('More actions'); + }); - describe('menu items', () => { - describe('delete file', () => { - it('exists', () => { - createComponent(); + describe('menu items', () => { + describe('delete file', () => { + it('exists', () => { + createComponent(); - expect(findActionMenuDelete().exists()).toBe(true); - }); + expect(findActionMenuDelete().exists()).toBe(true); + }); - it('emits a delete event when clicked', () => { - createComponent(); + it('emits a delete event when clicked', async () => { + createComponent(); - findActionMenuDelete().vm.$emit('click'); + await findActionMenuDelete().trigger('click'); - const [[{ id }]] = wrapper.emitted('delete-file'); - expect(id).toBe(file.id); - }); + const [[items]] = wrapper.emitted('delete-files'); + const [{ id }] = items; + expect(id).toBe(file.id); }); }); }); + }); + + describe('when the user can not delete', () => { + const canDelete = false; + + it('does not exist', () => { + createComponent({ canDelete }); + + expect(findFirstActionMenu().exists()).toBe(false); + }); + }); + }); + + describe('multi select', () => { + describe('when user can delete', () => { + it('delete selected button exists & is disabled', () => { + createComponent(); + + expect(findDeleteSelectedButton().exists()).toBe(true); + expect(findDeleteSelectedButton().text()).toMatchInterpolatedText('Delete selected'); + expect(findDeleteSelectedButton().props('disabled')).toBe(true); + }); + + it('delete selected button exists & is disabled when isLoading prop is true', () => { + createComponent({ isLoading: true }); + + expect(findDeleteSelectedButton().props('disabled')).toBe(true); + }); + + it('checkboxes to select file are visible', () => { + createComponent({ packageFiles: files }); + + expect(findCheckAllCheckbox().exists()).toBe(true); + expect(findAllRowCheckboxes()).toHaveLength(2); + }); + + it('selecting a checkbox enables delete selected button', async () => { + createComponent(); + + const first = findAllRowCheckboxes().at(0); + + await first.setChecked(true); + + expect(findDeleteSelectedButton().props('disabled')).toBe(false); + }); + + describe('select all checkbox', () => { + it('will toggle between selecting all and deselecting all files', async () => { + const getChecked = () => findAllRowCheckboxes().filter((x) => x.element.checked === true); + + createComponent({ packageFiles: files }); + + expect(getChecked()).toHaveLength(0); + + await findCheckAllCheckbox().setChecked(true); - describe('when the user can not delete', () => { - const canDelete = false; + expect(getChecked()).toHaveLength(files.length); - it('does not exist', () => { - createComponent({ canDelete }); + await findCheckAllCheckbox().setChecked(false); - expect(findFirstActionMenu().exists()).toBe(false); + expect(getChecked()).toHaveLength(0); }); + + it('will toggle the indeterminate state when some but not all files are selected', async () => { + const expectIndeterminateState = (state) => + expect(findCheckAllCheckbox().props('indeterminate')).toBe(state); + + createComponent({ + packageFiles: files, + stubs: { GlFormCheckbox: stubComponent(GlFormCheckbox, { props: ['indeterminate'] }) }, + }); + + expectIndeterminateState(false); + + await findSecondRow().trigger('click'); + + expectIndeterminateState(true); + + await findSecondRow().trigger('click'); + + expectIndeterminateState(false); + + findCheckAllCheckbox().trigger('click'); + + expectIndeterminateState(false); + + await findSecondRow().trigger('click'); + + expectIndeterminateState(true); + }); + }); + + it('emits a delete event when selected', async () => { + createComponent(); + + const first = findAllRowCheckboxes().at(0); + + await first.setChecked(true); + + await findDeleteSelectedButton().trigger('click'); + + const [[items]] = wrapper.emitted('delete-files'); + const [{ id }] = items; + expect(id).toBe(file.id); + }); + + it('emits delete event with both items when all are selected', async () => { + createComponent({ packageFiles: files }); + + await findCheckAllCheckbox().setChecked(true); + + await findDeleteSelectedButton().trigger('click'); + + const [[items]] = wrapper.emitted('delete-files'); + expect(items).toHaveLength(2); + }); + }); + + describe('when user cannot delete', () => { + const canDelete = false; + + it('delete selected button does not exist', () => { + createComponent({ canDelete }); + + expect(findDeleteSelectedButton().exists()).toBe(false); + }); + + it('checkboxes to select file are not visible', () => { + createComponent({ packageFiles: files, canDelete }); + + expect(findCheckAllCheckbox().exists()).toBe(false); + expect(findAllRowCheckboxes()).toHaveLength(0); }); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js index f4e6d43812d..ec2e833552a 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js @@ -17,6 +17,12 @@ import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import waitForPromises from 'helpers/wait_for_promises'; import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql'; +import Tracking from '~/tracking'; +import { + TRACKING_ACTION_CLICK_PIPELINE_LINK, + TRACKING_ACTION_CLICK_COMMIT_LINK, + TRACKING_LABEL_PACKAGE_HISTORY, +} from '~/packages_and_registries/package_registry/constants'; Vue.use(VueApollo); @@ -181,7 +187,6 @@ describe('Package History', () => { it('link', () => { const linkElement = findElementLink(element); const exist = Boolean(link); - expect(linkElement.exists()).toBe(exist); if (exist) { expect(linkElement.attributes('href')).toBe(link); @@ -189,4 +194,29 @@ describe('Package History', () => { }); }, ); + describe('tracking', () => { + let eventSpy; + const category = 'UI::Packages'; + + beforeEach(() => { + mountComponent(); + eventSpy = jest.spyOn(Tracking, 'event'); + }); + + it('clicking pipeline link tracks the right action', () => { + wrapper.vm.trackPipelineClick(); + expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_PIPELINE_LINK, { + category, + label: TRACKING_LABEL_PACKAGE_HISTORY, + }); + }); + + it('clicking commit link tracks the right action', () => { + wrapper.vm.trackCommitClick(); + expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_COMMIT_LINK, { + category, + label: TRACKING_LABEL_PACKAGE_HISTORY, + }); + }); + }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js index d306f7834f0..37416dcd4e7 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js @@ -22,16 +22,21 @@ const packageWithTags = { packageFiles: { nodes: packageFiles() }, }; +const defaultProvide = { + isGroupPage: false, +}; + describe('PackageTitle', () => { let wrapper; - async function createComponent(packageEntity = packageWithTags) { + async function createComponent(packageEntity = packageWithTags, provide = defaultProvide) { wrapper = shallowMountExtended(PackageTitle, { propsData: { packageEntity }, stubs: { TitleArea, GlSprintf, }, + provide, directives: { GlResizeObserver: createMockDirective(), }, @@ -199,11 +204,22 @@ describe('PackageTitle', () => { expect(findPipelineProject().exists()).toBe(false); }); - it('correctly shows the pipeline project if there is one', async () => { + it('does not display the pipeline project on project page even if it exists', async () => { await createComponent({ ...packageData(), pipelines: { nodes: packagePipelines() }, }); + expect(findPipelineProject().exists()).toBe(false); + }); + + it('correctly shows the pipeline project on group page if there is one', async () => { + await createComponent( + { + ...packageData(), + pipelines: { nodes: packagePipelines() }, + }, + { isGroupPage: true }, + ); expect(findPipelineProject().props()).toMatchObject({ text: packagePipelines()[0].project.name, icon: 'review-list', diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js index f2fef6436a6..20acb0872e5 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js @@ -1,9 +1,10 @@ -import { GlLink, GlSprintf } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { GlSprintf } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { packageData } from 'jest/packages_and_registries/package_registry/mock_data'; import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; import PypiInstallation from '~/packages_and_registries/package_registry/components/details/pypi_installation.vue'; import { + PERSONAL_ACCESS_TOKEN_HELP_URL, PACKAGE_TYPE_PYPI, TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND, TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND, @@ -24,11 +25,12 @@ password = <your personal access token>`; const pipCommand = () => wrapper.findByTestId('pip-command'); const setupInstruction = () => wrapper.findByTestId('pypi-setup-content'); + const findAccessTokenLink = () => wrapper.findByTestId('access-token-link'); const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); - const findSetupDocsLink = () => wrapper.findComponent(GlLink); + const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link'); function createComponent() { - wrapper = shallowMountExtended(PypiInstallation, { + wrapper = mountExtended(PypiInstallation, { propsData: { packageEntity, }, @@ -78,6 +80,12 @@ password = <your personal access token>`; }); }); + it('has a link to personal access token docs', () => { + expect(findAccessTokenLink().attributes()).toMatchObject({ + href: PERSONAL_ACCESS_TOKEN_HELP_URL, + }); + }); + it('has a link to the docs', () => { expect(findSetupDocsLink().attributes()).toMatchObject({ href: PYPI_HELP_PATH, diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js index c16c09b5326..eb1e76377ff 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js @@ -123,7 +123,7 @@ describe('packages_list_row', () => { findDeleteDropdown().vm.$emit('click'); await nextTick(); - expect(wrapper.emitted('packageToDelete')).toBeTruthy(); + expect(wrapper.emitted('packageToDelete')).toHaveLength(1); expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index d40feee582f..22236424e6a 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -141,6 +141,7 @@ export const packageData = (extend) => ({ }); export const conanMetadata = () => ({ + __typename: 'ConanMetadata', id: 'conan-1', packageChannel: 'stable', packageUsername: 'gitlab-org+gitlab-test', @@ -148,9 +149,8 @@ export const conanMetadata = () => ({ recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable', }); -const conanMetadataQuery = () => ({ ...conanMetadata(), __typename: 'ConanMetadata' }); - export const composerMetadata = () => ({ + __typename: 'ComposerMetadata', targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', composerJson: { license: 'MIT', @@ -158,19 +158,14 @@ export const composerMetadata = () => ({ }, }); -const composerMetadataQuery = () => ({ - ...composerMetadata(), - __typename: 'ComposerMetadata', -}); - export const pypiMetadata = () => ({ + __typename: 'PypiMetadata', id: 'pypi-1', requiredPython: '1.0.0', }); -const pypiMetadataQuery = () => ({ ...pypiMetadata(), __typename: 'PypiMetadata' }); - export const mavenMetadata = () => ({ + __typename: 'MavenMetadata', id: 'maven-1', appName: 'appName', appGroup: 'appGroup', @@ -178,23 +173,20 @@ export const mavenMetadata = () => ({ path: 'path', }); -const mavenMetadataQuery = () => ({ ...mavenMetadata(), __typename: 'MavenMetadata' }); - export const nugetMetadata = () => ({ + __typename: 'NugetMetadata', id: 'nuget-1', iconUrl: 'iconUrl', licenseUrl: 'licenseUrl', projectUrl: 'projectUrl', }); -const nugetMetadataQuery = () => ({ ...nugetMetadata(), __typename: 'NugetMetadata' }); - const packageTypeMetadataQueryMapping = { - CONAN: conanMetadataQuery, - COMPOSER: composerMetadataQuery, - PYPI: pypiMetadataQuery, - MAVEN: mavenMetadataQuery, - NUGET: nugetMetadataQuery, + CONAN: conanMetadata, + COMPOSER: composerMetadata, + PYPI: pypiMetadata, + MAVEN: mavenMetadata, + NUGET: nugetMetadata, }; export const pagination = (extend) => ({ @@ -221,6 +213,7 @@ export const packageDetailsQuery = (extendPackage) => ({ id: '1', path: 'projectPath', name: 'gitlab-test', + fullPath: 'gitlab-test', }, tags: { nodes: packageTags(), @@ -231,6 +224,9 @@ export const packageDetailsQuery = (extendPackage) => ({ __typename: 'PipelineConnection', }, packageFiles: { + pageInfo: { + hasNextPage: true, + }, nodes: packageFiles(), __typename: 'PackageFileConnection', }, @@ -310,16 +306,16 @@ export const packageDestroyMutationError = () => ({ ], }); -export const packageDestroyFileMutation = () => ({ +export const packageDestroyFilesMutation = () => ({ data: { - destroyPackageFile: { + destroyPackageFiles: { errors: [], }, }, }); -export const packageDestroyFileMutationError = () => ({ +export const packageDestroyFilesMutationError = () => ({ data: { - destroyPackageFile: null, + destroyPackageFiles: null, }, errors: [ { @@ -331,7 +327,7 @@ export const packageDestroyFileMutationError = () => ({ column: 3, }, ], - path: ['destroyPackageFile'], + path: ['destroyPackageFiles'], }, ], }); diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index 3cadb001c58..de78e6bb87b 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -22,6 +22,8 @@ import { PACKAGE_TYPE_COMPOSER, DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILES_ERROR_MESSAGE, PACKAGE_TYPE_NUGET, PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_CONAN, @@ -29,7 +31,7 @@ import { PACKAGE_TYPE_NPM, } from '~/packages_and_registries/package_registry/constants'; -import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; +import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import { packageDetailsQuery, @@ -38,8 +40,8 @@ import { dependencyLinks, emptyPackageDetailsQuery, packageFiles, - packageDestroyFileMutation, - packageDestroyFileMutationError, + packageDestroyFilesMutation, + packageDestroyFilesMutationError, } from '../mock_data'; jest.mock('~/flash'); @@ -58,6 +60,7 @@ describe('PackagesApp', () => { emptyListIllustration: 'svgPath', projectListUrl: 'projectListUrl', groupListUrl: 'groupListUrl', + isGroupPage: false, breadCrumbState, }; @@ -65,14 +68,14 @@ describe('PackagesApp', () => { function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), - fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()), + filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()), routeId = '1', } = {}) { Vue.use(VueApollo); const requestHandlers = [ [getPackageDetails, resolver], - [destroyPackageFileMutation, fileDeleteMutationResolver], + [destroyPackageFilesMutation, filesDeleteMutationResolver], ]; apolloProvider = createMockApollo(requestHandlers); @@ -110,6 +113,7 @@ describe('PackagesApp', () => { const findDeleteButton = () => wrapper.findByTestId('delete-package'); const findPackageFiles = () => wrapper.findComponent(PackageFiles); const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal'); + const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal'); const findVersionRows = () => wrapper.findAllComponents(VersionRow); const noVersionsMessage = () => wrapper.findByTestId('no-versions-message'); const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge); @@ -288,6 +292,7 @@ describe('PackagesApp', () => { expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile); expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy); + expect(findPackageFiles().props('isLoading')).toEqual(false); }); it('does not render the package files table when the package is composer', async () => { @@ -305,24 +310,69 @@ describe('PackagesApp', () => { describe('deleting a file', () => { const [fileToDelete] = packageFiles(); - const doDeleteFile = () => { - findPackageFiles().vm.$emit('delete-file', fileToDelete); + const doDeleteFile = async () => { + findPackageFiles().vm.$emit('delete-files', [fileToDelete]); findDeleteFileModal().vm.$emit('primary'); return waitForPromises(); }; - it('opens a confirmation modal', async () => { + it('opens delete file confirmation modal', async () => { createComponent(); await waitForPromises(); - findPackageFiles().vm.$emit('delete-file', fileToDelete); + const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show'); + const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show'); + + findPackageFiles().vm.$emit('delete-files', [fileToDelete]); + + expect(showDeletePackageSpy).not.toBeCalled(); + expect(showDeleteFileSpy).toBeCalled(); + }); + + it('when its the only file opens delete package confirmation modal', async () => { + const [packageFile] = packageFiles(); + const resolver = jest.fn().mockResolvedValue( + packageDetailsQuery({ + packageFiles: { + pageInfo: { + hasNextPage: false, + }, + nodes: [packageFile], + __typename: 'PackageFileConnection', + }, + }), + ); + + createComponent({ + resolver, + }); + + await waitForPromises(); + + const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show'); + const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show'); + + findPackageFiles().vm.$emit('delete-files', [fileToDelete]); + + expect(showDeletePackageSpy).toBeCalled(); + expect(showDeleteFileSpy).not.toBeCalled(); + }); + + it('confirming on the modal sets the loading state', async () => { + createComponent(); + + await waitForPromises(); + + findPackageFiles().vm.$emit('delete-files', [fileToDelete]); + + findDeleteFileModal().vm.$emit('primary'); await nextTick(); - expect(findDeleteFileModal().exists()).toBe(true); + expect(findPackageFiles().props('isLoading')).toEqual(true); }); it('confirming on the modal deletes the file and shows a success message', async () => { @@ -344,7 +394,7 @@ describe('PackagesApp', () => { describe('errors', () => { it('shows an error when the mutation request fails', async () => { - createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() }); + createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() }); await waitForPromises(); await doDeleteFile(); @@ -358,9 +408,9 @@ describe('PackagesApp', () => { it('shows an error when the mutation request returns an error payload', async () => { createComponent({ - fileDeleteMutationResolver: jest + filesDeleteMutationResolver: jest .fn() - .mockResolvedValue(packageDestroyFileMutationError()), + .mockResolvedValue(packageDestroyFilesMutationError()), }); await waitForPromises(); @@ -374,6 +424,117 @@ describe('PackagesApp', () => { }); }); }); + + describe('deleting multiple files', () => { + const doDeleteFiles = async () => { + findPackageFiles().vm.$emit('delete-files', packageFiles()); + + findDeleteFilesModal().vm.$emit('primary'); + + return waitForPromises(); + }; + + it('opens delete files confirmation modal', async () => { + createComponent(); + + await waitForPromises(); + + const showDeleteFilesSpy = jest.spyOn(wrapper.vm.$refs.deleteFilesModal, 'show'); + + findPackageFiles().vm.$emit('delete-files', packageFiles()); + + expect(showDeleteFilesSpy).toBeCalled(); + }); + + it('confirming on the modal sets the loading state', async () => { + createComponent(); + + await waitForPromises(); + + findPackageFiles().vm.$emit('delete-files', packageFiles()); + + findDeleteFilesModal().vm.$emit('primary'); + + await nextTick(); + + expect(findPackageFiles().props('isLoading')).toEqual(true); + }); + + it('confirming on the modal deletes the file and shows a success message', async () => { + const resolver = jest.fn().mockResolvedValue(packageDetailsQuery()); + createComponent({ resolver }); + + await waitForPromises(); + + await doDeleteFiles(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, + }), + ); + // we are re-fetching the package details, so we expect the resolver to have been called twice + expect(resolver).toHaveBeenCalledTimes(2); + }); + + describe('errors', () => { + it('shows an error when the mutation request fails', async () => { + createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() }); + await waitForPromises(); + + await doDeleteFiles(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILES_ERROR_MESSAGE, + }), + ); + }); + + it('shows an error when the mutation request returns an error payload', async () => { + createComponent({ + filesDeleteMutationResolver: jest + .fn() + .mockResolvedValue(packageDestroyFilesMutationError()), + }); + await waitForPromises(); + + await doDeleteFiles(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILES_ERROR_MESSAGE, + }), + ); + }); + }); + }); + + describe('deleting all files', () => { + it('opens the delete package confirmation modal', async () => { + const resolver = jest.fn().mockResolvedValue( + packageDetailsQuery({ + packageFiles: { + pageInfo: { + hasNextPage: false, + }, + nodes: packageFiles(), + }, + }), + ); + createComponent({ + resolver, + }); + + await waitForPromises(); + + const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show'); + + findPackageFiles().vm.$emit('delete-files', packageFiles()); + + expect(showDeletePackageSpy).toBeCalled(); + }); + }); }); describe('versions', () => { diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap index 108d9478788..5d08574234c 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap @@ -5,6 +5,7 @@ exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`] class="gl-mr-7 gl-mb-0!" data-testid="cadence-dropdown" description="" + dropdownclass="" formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]" label="Run cleanup:" name="cadence" @@ -24,6 +25,7 @@ exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] = <expiration-dropdown-stub data-testid="keep-n-dropdown" description="" + dropdownclass="" formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" label="Keep the most recent:" name="keep-n" @@ -47,6 +49,7 @@ exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1` <expiration-dropdown-stub data-testid="older-than-dropdown" description="" + dropdownclass="" formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" label="Remove tags older than:" name="older-than" diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js index d4b6c66ddeb..0696144215c 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js @@ -1,4 +1,5 @@ export const containerExpirationPolicyData = () => ({ + __typename: 'ContainerExpirationPolicy', cadence: 'EVERY_DAY', enabled: true, keepN: 'TEN_TAGS', @@ -13,7 +14,6 @@ export const expirationPolicyPayload = (override) => ({ project: { id: '1', containerExpirationPolicy: { - __typename: 'ContainerExpirationPolicy', ...containerExpirationPolicyData(), ...override, }, @@ -42,6 +42,7 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) }); export const packagesCleanupPolicyData = { + __typename: 'PackagesCleanupPolicy', keepNDuplicatedPackageFiles: 'ALL_PACKAGE_FILES', nextRunAt: '2020-11-19T07:37:03.941Z', }; @@ -51,7 +52,6 @@ export const packagesCleanupPolicyPayload = (override) => ({ project: { id: '1', packagesCleanupPolicy: { - __typename: 'PackagesCleanupPolicy', ...packagesCleanupPolicyData, ...override, }, diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js index 542eb2f3ab8..85ed94b748d 100644 --- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js +++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js @@ -23,17 +23,17 @@ describe('AccountAndLimits', () => { describe('Changing of userInternalRegex when userDefaultExternal', () => { it('is unchecked', () => { - expect($userDefaultExternal.prop('checked')).toBeFalsy(); + expect($userDefaultExternal.prop('checked')).toBe(false); expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE); - expect($userInternalRegex.readOnly).toBeTruthy(); + expect($userInternalRegex.readOnly).toBe(true); }); it('is checked', () => { if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click(); - expect($userDefaultExternal.prop('checked')).toBeTruthy(); + expect($userDefaultExternal.prop('checked')).toBe(true); expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE); - expect($userInternalRegex.readOnly).toBeFalsy(); + expect($userInternalRegex.readOnly).toBe(false); }); }); }); diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js new file mode 100644 index 00000000000..ab483316086 --- /dev/null +++ b/spec/frontend/pages/groups/new/components/app_spec.js @@ -0,0 +1,39 @@ +import { shallowMount } from '@vue/test-utils'; +import App from '~/pages/groups/new/components/app.vue'; +import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; + +describe('App component', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(App, { propsData }); + }; + + const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage); + + const findCreateGroupPanel = () => + findNewNamespacePage() + .props('panels') + .find((panel) => panel.name === 'create-group-pane'); + + afterEach(() => { + wrapper.destroy(); + }); + + it('creates correct component for group creation', () => { + createComponent(); + + expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('New group'); + expect(findCreateGroupPanel().title).toBe('Create group'); + }); + + it('creates correct component for subgroup creation', () => { + const props = { parentGroupName: 'parent', importExistingGroupPath: '/path' }; + + createComponent(props); + + expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('parent'); + expect(findCreateGroupPanel().title).toBe('Create subgroup'); + expect(findCreateGroupPanel().detailProps).toEqual(props); + }); +}); diff --git a/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js new file mode 100644 index 00000000000..56a1fd03f71 --- /dev/null +++ b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf, GlLink } from '@gitlab/ui'; +import CreateGroupDescriptionDetails from '~/pages/groups/new/components/create_group_description_details.vue'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +describe('CreateGroupDescriptionDetails component', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(CreateGroupDescriptionDetails, { + propsData, + stubs: { GlSprintf, GlLink }, + }); + }; + + const findLinkHref = (at) => wrapper.findAllComponents(GlLink).at(at); + + afterEach(() => { + wrapper.destroy(); + }); + + it('creates correct component for group creation', () => { + createComponent(); + + const groupsLink = findLinkHref(0); + expect(groupsLink.attributes('href')).toBe(helpPagePath('user/group/index')); + expect(groupsLink.text()).toBe('Groups'); + + const subgroupsLink = findLinkHref(1); + expect(subgroupsLink.text()).toBe('subgroups'); + expect(subgroupsLink.attributes('href')).toBe(helpPagePath('user/group/subgroups/index')); + + expect(wrapper.text()).toBe( + 'Groups allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups.', + ); + }); + + it('creates correct component for subgroup creation', () => { + createComponent({ parentGroupName: 'parent', importExistingGroupPath: '/path' }); + + const groupsLink = findLinkHref(0); + expect(groupsLink.attributes('href')).toBe(helpPagePath('user/group/index')); + expect(groupsLink.text()).toBe('Groups'); + + const subgroupsLink = findLinkHref(1); + expect(subgroupsLink.text()).toBe('subgroups'); + expect(subgroupsLink.attributes('href')).toBe(helpPagePath('user/group/subgroups/index')); + + const importGroupLink = findLinkHref(2); + expect(importGroupLink.text()).toBe('import an existing group'); + expect(importGroupLink.attributes('href')).toBe('/path'); + + expect(wrapper.text()).toBe( + 'Groups and subgroups allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. You can also import an existing group.', + ); + }); +}); diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap index 43361bb6f24..21a38f066d9 100644 --- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap +++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap @@ -3,6 +3,33 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = ` <div> <div + class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-t gl-pt-4 gl-mb-3" + > + <h4 + class="gl-m-0" + sub-header="" + > + <gl-sprintf-stub + message="Code coverage statistics for %{ref} %{start_date} - %{end_date}" + /> + </h4> + + <gl-button-stub + buttontextclasses="" + category="primary" + data-testid="download-button" + href="url/" + icon="" + size="small" + variant="default" + > + + Download raw data (.csv) + + </gl-button-stub> + </div> + + <div class="gl-mt-3 gl-mb-3" > <!----> @@ -79,6 +106,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`] legendmaxtext="Max" legendmintext="Min" option="[object Object]" + responsive="" thresholds="" /> </div> diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js index 0f763e3220a..f272891919d 100644 --- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js +++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js @@ -15,17 +15,26 @@ describe('Code Coverage', () => { let mockAxios; const graphEndpoint = '/graph'; + const graphStartDate = '13 February'; + const graphEndDate = '12 May'; + const graphRef = 'master'; + const graphCsvPath = 'url/'; const findAlert = () => wrapper.find(GlAlert); const findAreaChart = () => wrapper.find(GlAreaChart); const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); const findFirstDropdownItem = () => findAllDropdownItems().at(0); const findSecondDropdownItem = () => findAllDropdownItems().at(1); + const findDownloadButton = () => wrapper.find('[data-testid="download-button"]'); const createComponent = () => { wrapper = shallowMount(CodeCoverage, { propsData: { graphEndpoint, + graphStartDate, + graphEndDate, + graphRef, + graphCsvPath, }, }); }; @@ -64,6 +73,10 @@ describe('Code Coverage', () => { it('shows no error messages', () => { expect(findAlert().exists()).toBe(false); }); + + it('does not render download button', () => { + expect(findDownloadButton().exists()).toBe(true); + }); }); describe('when fetching data fails', () => { @@ -112,6 +125,10 @@ describe('Code Coverage', () => { it('still renders an empty graph', () => { expect(findAreaChart().exists()).toBe(true); }); + + it('does not render download button', () => { + expect(findDownloadButton().exists()).toBe(false); + }); }); describe('dropdown options', () => { @@ -146,8 +163,8 @@ describe('Code Coverage', () => { await nextTick(); - expect(findFirstDropdownItem().attributes('ischecked')).toBeFalsy(); - expect(findSecondDropdownItem().attributes('ischecked')).toBeTruthy(); + expect(findFirstDropdownItem().attributes('ischecked')).toBe(undefined); + expect(findSecondDropdownItem().attributes('ischecked')).toBe('true'); }); it('updates the graph data when selecting a different option in dropdown', async () => { diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js index 42eeff89bf4..5b9c48f0d9b 100644 --- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js +++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js @@ -226,7 +226,6 @@ describe('Timezone Dropdown', () => { it('returns the correct object if the identifier exists', () => { const res = findTimezoneByIdentifier(tzList, identifier); - expect(res).toBeTruthy(); expect(res).toBe(tzList[2]); }); diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 85660d09baa..f908508c4b5 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -127,6 +127,7 @@ describe('Settings Panel', () => { const findOperationsVisibilityInput = () => findOperationsSettings().findComponent(ProjectFeatureSetting); const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger); + const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' }); afterEach(() => { wrapper.destroy(); @@ -786,4 +787,23 @@ describe('Settings Panel', () => { expect(findOperationsSettings().exists()).toBe(true); }); }); + + describe('Environments', () => { + describe('with feature flag', () => { + it('should show the environments toggle', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + }); + + expect(findEnvironmentsSettings().exists()).toBe(true); + }); + }); + describe('without feature flag', () => { + it('should not show the environments toggle', () => { + wrapper = mountComponent({}); + + expect(findEnvironmentsSettings().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js index a5db10d106d..204c48f8de1 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -1,5 +1,5 @@ import { nextTick } from 'vue'; -import { GlAlert, GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui'; +import { GlAlert, GlButton, GlFormInput, GlFormGroup, GlSegmentedControl } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; @@ -106,6 +106,7 @@ describe('WikiForm', () => { MarkdownField, GlAlert, GlButton, + GlSegmentedControl, LocalStorageSync: stubComponent(LocalStorageSync), GlFormInput, GlFormGroup, @@ -317,20 +318,20 @@ describe('WikiForm', () => { }); describe('when content editor is not active', () => { - it('displays "Edit rich text" label in the toggle editing mode button', () => { - expect(findToggleEditingModeButton().text()).toBe('Edit rich text'); + it('displays "Source" label in the toggle editing mode button', () => { + expect(findToggleEditingModeButton().props().checked).toBe('source'); }); describe('when clicking the toggle editing mode button', () => { beforeEach(async () => { - await findToggleEditingModeButton().trigger('click'); + await findToggleEditingModeButton().vm.$emit('input', 'richText'); }); it('hides the classic editor', () => { expect(findClassicEditor().exists()).toBe(false); }); - it('hides the content editor', () => { + it('shows the content editor', () => { expect(findContentEditor().exists()).toBe(true); }); }); @@ -342,7 +343,7 @@ describe('WikiForm', () => { expect(findContentEditor().exists()).toBe(false); // enable content editor - await findLocalStorageSync().vm.$emit('input', true); + await findLocalStorageSync().vm.$emit('input', 'richText'); expect(findContentEditor().exists()).toBe(true); expect(findClassicEditor().exists()).toBe(false); @@ -352,17 +353,18 @@ describe('WikiForm', () => { describe('when content editor is active', () => { let mockContentEditor; - beforeEach(async () => { + beforeEach(() => { + createWrapper(); mockContentEditor = { getSerializedContent: jest.fn(), setSerializedContent: jest.fn(), }; - await findToggleEditingModeButton().trigger('click'); + findToggleEditingModeButton().vm.$emit('input', 'richText'); }); - it('displays "Edit source" label in the toggle editing mode button', () => { - expect(findToggleEditingModeButton().text()).toBe('Edit source'); + it('displays "Edit Rich" label in the toggle editing mode button', () => { + expect(findToggleEditingModeButton().props().checked).toBe('richText'); }); describe('when clicking the toggle editing mode button', () => { @@ -374,7 +376,8 @@ describe('WikiForm', () => { ); findContentEditor().vm.$emit('initialized', mockContentEditor); - await findToggleEditingModeButton().trigger('click'); + await findToggleEditingModeButton().vm.$emit('input', 'source'); + await nextTick(); }); it('hides the content editor', () => { @@ -389,6 +392,38 @@ describe('WikiForm', () => { expect(findContent().element.value).toBe(contentEditorFakeSerializedContent); }); }); + + describe('when content editor is loading', () => { + beforeEach(async () => { + findContentEditor().vm.$emit('loading'); + + await nextTick(); + }); + + it('disables toggle editing mode button', () => { + expect(findToggleEditingModeButton().attributes().disabled).toBe('true'); + }); + + describe('when content editor loads successfully', () => { + it('enables toggle editing mode button', async () => { + findContentEditor().vm.$emit('loadingSuccess'); + + await nextTick(); + + expect(findToggleEditingModeButton().attributes().disabled).not.toBeDefined(); + }); + }); + + describe('when content editor fails to load', () => { + it('enables toggle editing mode button', async () => { + findContentEditor().vm.$emit('loadingError'); + + await nextTick(); + + expect(findToggleEditingModeButton().attributes().disabled).not.toBeDefined(); + }); + }); + }); }); }); @@ -398,7 +433,7 @@ describe('WikiForm', () => { createWrapper({ mountFn: mount }); mock.onPost(/preview-markdown/).reply(400); - await findToggleEditingModeButton().trigger('click'); + await findToggleEditingModeButton().vm.$emit('input', 'richText'); // try waiting for content editor to load (but it will never actually load) await waitForPromises(); @@ -410,7 +445,7 @@ describe('WikiForm', () => { describe('toggling editing modes to the classic editor', () => { beforeEach(() => { - return findToggleEditingModeButton().trigger('click'); + return findToggleEditingModeButton().vm.$emit('input', 'source'); }); it('switches to classic editor', () => { @@ -426,7 +461,7 @@ describe('WikiForm', () => { mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' }); - await findToggleEditingModeButton().trigger('click'); + await findToggleEditingModeButton().vm.$emit('input', 'richText'); await waitForPromises(); }); @@ -463,7 +498,6 @@ describe('WikiForm', () => { it('triggers tracking events on form submit', async () => { await triggerFormSubmit(); - expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, { label: WIKI_CONTENT_EDITOR_TRACKING_LABEL, }); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js index bf5d15516c2..7e1e5004d91 100644 --- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js +++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js @@ -8,16 +8,8 @@ describe('First pipeline card', () => { let wrapper; let trackingSpy; - const defaultProvide = { - runnerHelpPagePath: '/help/runners', - }; - const createComponent = () => { - wrapper = mount(FirstPipelineCard, { - provide: { - ...defaultProvide, - }, - }); + wrapper = mount(FirstPipelineCard); }; const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }); @@ -43,7 +35,7 @@ describe('First pipeline card', () => { }); it('renders the link', () => { - expect(findRunnersLink().href).toContain(defaultProvide.runnerHelpPagePath); + expect(findRunnersLink().href).toBe(wrapper.vm.$options.RUNNER_HELP_URL); }); describe('tracking', () => { diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js deleted file mode 100644 index 238942a34ff..00000000000 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import { GlAlert, GlLink } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; -import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data'; - -describe('~/pipeline_editor/components/lint/ci_lint.vue', () => { - let wrapper; - - const createComponent = ({ props, mountFn = shallowMount } = {}) => { - wrapper = mountFn(CiLint, { - provide: { - lintHelpPagePath: mockLintHelpPagePath, - }, - propsData: { - ciConfig: mergeUnwrappedCiConfig(), - ...props, - }, - }); - }; - - const findAllByTestId = (selector) => wrapper.findAll(`[data-testid="${selector}"]`); - const findAlert = () => wrapper.find(GlAlert); - const findLintParameters = () => findAllByTestId('ci-lint-parameter'); - const findLintParameterAt = (i) => findLintParameters().at(i); - const findLintValueAt = (i) => findAllByTestId('ci-lint-value').at(i); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('Valid Results', () => { - beforeEach(() => { - createComponent({ props: { isValid: true }, mountFn: mount }); - }); - - it('displays valid results', () => { - expect(findAlert().text()).toMatch('Status: Syntax is correct.'); - }); - - it('displays link to the right help page', () => { - expect(findAlert().find(GlLink).attributes('href')).toBe(mockLintHelpPagePath); - }); - - it('displays jobs', () => { - expect(findLintParameters()).toHaveLength(3); - - expect(findLintParameterAt(0).text()).toBe('Test Job - job_test_1'); - expect(findLintParameterAt(1).text()).toBe('Test Job - job_test_2'); - expect(findLintParameterAt(2).text()).toBe('Build Job - job_build'); - }); - - it('displays jobs details', () => { - expect(findLintParameters()).toHaveLength(3); - - expect(findLintValueAt(0).text()).toMatchInterpolatedText( - 'echo "test 1" Only policy: branches, tags When: on_success', - ); - expect(findLintValueAt(1).text()).toMatchInterpolatedText( - 'echo "test 2" Only policy: branches, tags When: on_success', - ); - expect(findLintValueAt(2).text()).toMatchInterpolatedText( - 'echo "build" Only policy: branches, tags When: on_success', - ); - }); - - it('displays invalid results', () => { - createComponent({ props: { isValid: false }, mountFn: mount }); - - expect(findAlert().text()).toMatch('Status: Syntax is incorrect.'); - }); - }); -}); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 87a7f07f7d4..2f3e1b49b37 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -1,11 +1,12 @@ +// TODO + import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui'; -import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import Vue, { nextTick } from 'vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; -import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import CiValidate from '~/pipeline_editor/components/validate/ci_validate.vue'; import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; @@ -30,8 +31,7 @@ import { mockLintResponseWithoutMerged, } from '../mock_data'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); Vue.config.ignoredElements = ['gl-emoji']; @@ -64,7 +64,12 @@ describe('Pipeline editor tabs component', () => { }; }, provide: { + ciConfigPath: '/path/to/ci-config', ciLintPath: mockCiLintPath, + currentBranch: 'main', + projectFullPath: '/path/to/project', + simulatePipelineHelpPagePath: 'path/to/help/page', + validateTabIllustrationPath: 'path/to/svg', ...provide, }, stubs: { @@ -88,21 +93,18 @@ describe('Pipeline editor tabs component', () => { provide, mountFn, options: { - localVue, apolloProvider: mockApollo, }, }); }; const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]'); - const findLintTab = () => wrapper.find('[data-testid="lint-tab"]'); const findMergedTab = () => wrapper.find('[data-testid="merged-tab"]'); const findValidateTab = () => wrapper.find('[data-testid="validate-tab"]'); const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); const findAlert = () => wrapper.findComponent(GlAlert); const findBadge = () => wrapper.findComponent(GlBadge); - const findCiLint = () => wrapper.findComponent(CiLint); const findCiValidate = () => wrapper.findComponent(CiValidate); const findGlTabs = () => wrapper.findComponent(GlTabs); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); @@ -121,7 +123,8 @@ describe('Pipeline editor tabs component', () => { describe('editor tab', () => { it('displays editor only after the tab is mounted', async () => { - createComponent({ mountFn: mount }); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + createComponentWithApollo({ mountFn: mount }); expect(findTextEditor().exists()).toBe(false); @@ -156,138 +159,57 @@ describe('Pipeline editor tabs component', () => { }); describe('validate tab', () => { - describe('with simulatePipeline feature flag ON', () => { - describe('after loading', () => { - beforeEach(() => { - createComponent({ - provide: { glFeatures: { simulatePipeline: true } }, - }); - }); - - it('displays the tab and the validate component', () => { - expect(findValidateTab().exists()).toBe(true); - expect(findCiValidate().exists()).toBe(true); - }); + describe('after loading', () => { + beforeEach(() => { + createComponent(); }); - describe('NEW badge', () => { - describe('default', () => { - beforeEach(() => { - mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - createComponentWithApollo({ - mountFn: mount, - props: { - currentTab: VALIDATE_TAB, - }, - provide: { - glFeatures: { simulatePipeline: true }, - ciConfigPath: '/path/to/ci-config', - currentBranch: 'main', - projectFullPath: '/path/to/project', - simulatePipelineHelpPagePath: 'path/to/help/page', - validateTabIllustrationPath: 'path/to/svg', - }, - }); - }); - - it('renders badge by default', () => { - expect(findBadge().exists()).toBe(true); - expect(findBadge().text()).toBe(wrapper.vm.$options.i18n.new); - }); - - it('hides badge when moving away from the validate tab', async () => { - expect(findBadge().exists()).toBe(true); - - await findEditorTab().vm.$emit('click'); - - expect(findBadge().exists()).toBe(false); - }); - }); - - describe('if badge has been dismissed before', () => { - beforeEach(() => { - localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true'); - mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - createComponentWithApollo({ - mountFn: mount, - provide: { - glFeatures: { simulatePipeline: true }, - ciConfigPath: '/path/to/ci-config', - currentBranch: 'main', - projectFullPath: '/path/to/project', - simulatePipelineHelpPagePath: 'path/to/help/page', - validateTabIllustrationPath: 'path/to/svg', - }, - }); - }); - - it('does not render badge if it has been dismissed before', () => { - expect(findBadge().exists()).toBe(false); - }); - }); + it('displays the tab and the validate component', () => { + expect(findValidateTab().exists()).toBe(true); + expect(findCiValidate().exists()).toBe(true); }); }); - describe('with simulatePipeline feature flag OFF', () => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures: { - simulatePipeline: false, + describe('NEW badge', () => { + describe('default', () => { + beforeEach(() => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + createComponentWithApollo({ + mountFn: mount, + props: { + currentTab: VALIDATE_TAB, }, - }, + }); }); - }); - it('does not render the tab and the validate component', () => { - expect(findValidateTab().exists()).toBe(false); - expect(findCiValidate().exists()).toBe(false); - }); - }); - }); + it('renders badge by default', () => { + expect(findBadge().exists()).toBe(true); + expect(findBadge().text()).toBe(wrapper.vm.$options.i18n.new); + }); - describe('lint tab', () => { - describe('while loading', () => { - beforeEach(() => { - createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); - }); + it('hides badge when moving away from the validate tab', async () => { + expect(findBadge().exists()).toBe(true); - it('displays a loading icon if the lint query is loading', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); + await findEditorTab().vm.$emit('click'); - it('does not display the lint component', () => { - expect(findCiLint().exists()).toBe(false); - }); - }); - describe('after loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('display the tab and the lint component', () => { - expect(findLintTab().exists()).toBe(true); - expect(findCiLint().exists()).toBe(true); + expect(findBadge().exists()).toBe(false); + }); }); - }); - describe('with simulatePipeline feature flag ON', () => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures: { - simulatePipeline: true, - }, - }, + describe('if badge has been dismissed before', () => { + beforeEach(() => { + localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true'); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + createComponentWithApollo({ mountFn: mount }); }); - }); - it('does not render the tab and the lint component', () => { - expect(findLintTab().exists()).toBe(false); - expect(findCiLint().exists()).toBe(false); + it('does not render badge if it has been dismissed before', () => { + expect(findBadge().exists()).toBe(false); + }); }); }); }); + describe('merged tab', () => { describe('while loading', () => { beforeEach(() => { @@ -328,19 +250,19 @@ describe('Pipeline editor tabs component', () => { describe('show tab content based on status', () => { it.each` - appStatus | editor | viz | lint | merged + appStatus | editor | viz | validate | merged ${undefined} | ${true} | ${true} | ${true} | ${true} - ${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${false} | ${false} + ${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${true} | ${false} ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false} ${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true} `( - 'when status is $appStatus, we show - editor:$editor | viz:$viz | lint:$lint | merged:$merged ', - ({ appStatus, editor, viz, lint, merged }) => { + 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged ', + ({ appStatus, editor, viz, validate, merged }) => { createComponent({ appStatus }); expect(findTextEditor().exists()).toBe(editor); expect(findPipelineGraph().exists()).toBe(viz); - expect(findCiLint().exists()).toBe(lint); + expect(findValidateTab().exists()).toBe(validate); expect(findMergedPreview().exists()).toBe(merged); }, ); @@ -386,11 +308,8 @@ describe('Pipeline editor tabs component', () => { describe('pipeline editor walkthrough', () => { describe('when isNewCiConfigFile prop is true (default)', () => { - beforeEach(async () => { - createComponent({ - mountFn: mount, - }); - await nextTick(); + beforeEach(() => { + createComponent(); }); it('shows walkthrough popover', async () => { @@ -400,8 +319,7 @@ describe('Pipeline editor tabs component', () => { describe('when isNewCiConfigFile prop is false', () => { it('does not show walkthrough popover', async () => { - createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount }); - await nextTick(); + createComponent({ props: { isNewCiConfigFile: false } }); expect(findWalkthroughPopover().exists()).toBe(false); }); }); @@ -411,7 +329,6 @@ describe('Pipeline editor tabs component', () => { const handler = jest.fn(); createComponent({ - mountFn: mount, listeners: { event: handler, }, diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js index f5f01b675b2..09d4f9736ad 100644 --- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js +++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js @@ -2,6 +2,7 @@ import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/u import { nextTick } from 'vue'; import { createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue'; @@ -9,6 +10,7 @@ import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_valid import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue'; import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql'; import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql'; +import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants'; import { mockBlobContentQueryResponse, mockCiLintPath, @@ -24,6 +26,7 @@ describe('Pipeline Editor Validate Tab', () => { let wrapper; let mockApollo; let mockBlobContentData; + let trackingSpy; const createComponent = ({ props, @@ -140,9 +143,24 @@ describe('Pipeline Editor Validate Tab', () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); await createComponentWithApollo(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); }); + afterEach(() => { + unmockTracking(); + }); + + it('tracks the simulation event', () => { + const { + label, + actions: { simulatePipeline }, + } = pipelineEditorTrackingOptions; + findCta().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, simulatePipeline, { label }); + }); + it('renders loading state while simulation is ongoing', async () => { findCta().vm.$emit('click'); await nextTick(); @@ -159,7 +177,7 @@ describe('Pipeline Editor Validate Tab', () => { expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: lintCIMutation, variables: { - dry_run: true, + dry: true, content: mockCiYml, endpoint: mockCiLintPath, }, @@ -224,10 +242,27 @@ describe('Pipeline Editor Validate Tab', () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); await createComponentWithApollo(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); await findCta().vm.$emit('click'); }); + afterEach(() => { + unmockTracking(); + }); + + it('tracks the second simulation event', async () => { + const { + label, + actions: { resimulatePipeline }, + } = pipelineEditorTrackingOptions; + + await wrapper.setProps({ ciFileContent: 'new yaml content' }); + findResultsCta().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, resimulatePipeline, { label }); + }); + it('renders content change status', async () => { await wrapper.setProps({ ciFileContent: 'new yaml content' }); @@ -243,7 +278,7 @@ describe('Pipeline Editor Validate Tab', () => { expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: lintCIMutation, variables: { - dry_run: true, + dry: true, content: 'new yaml content', endpoint: mockCiLintPath, }, diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js index c6964f190b4..0cb7155c8c0 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -14,7 +14,7 @@ import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tab import { CREATE_TAB, FILE_TREE_DISPLAY_KEY, - LINT_TAB, + VALIDATE_TAB, MERGED_TAB, TABS_INDEX, VISUALIZE_TAB, @@ -138,7 +138,7 @@ describe('Pipeline editor home wrapper', () => { tab | shouldShow ${MERGED_TAB} | ${false} ${VISUALIZE_TAB} | ${false} - ${LINT_TAB} | ${false} + ${VALIDATE_TAB} | ${false} ${CREATE_TAB} | ${true} `( 'when the active tab is $tab the commit form is shown: $shouldShow', @@ -170,7 +170,7 @@ describe('Pipeline editor home wrapper', () => { tab | shouldShow ${MERGED_TAB} | ${false} ${VISUALIZE_TAB} | ${false} - ${LINT_TAB} | ${false} + ${VALIDATE_TAB} | ${false} ${CREATE_TAB} | ${true} `( 'when the tab query param is $tab the commit form is shown: $shouldShow', diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js index eec55091efa..18dbd1ce9d6 100644 --- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -39,6 +39,7 @@ describe('Pipeline New Form', () => { const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]'); const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); + const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]'); const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]'); const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]'); const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]'); @@ -102,6 +103,8 @@ describe('Pipeline New Form', () => { }); it('displays the correct values for the provided query params', async () => { + expect(findDropdowns().at(0).props('text')).toBe('Variable'); + expect(findDropdowns().at(1).props('text')).toBe('File'); expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' }); expect(findVariableRows()).toHaveLength(3); }); @@ -114,6 +117,7 @@ describe('Pipeline New Form', () => { it('displays an empty variable for the user to fill out', async () => { expect(findKeyInputs().at(2).element.value).toBe(''); expect(findValueInputs().at(2).element.value).toBe(''); + expect(findDropdowns().at(2).props('text')).toBe('Variable'); }); it('does not display remove icon for last row', () => { diff --git a/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js new file mode 100644 index 00000000000..d787611fe8f --- /dev/null +++ b/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js @@ -0,0 +1,54 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import TakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue'; + +describe('Take ownership modal', () => { + let wrapper; + const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(TakeOwnershipModal, { + propsData: { + ownershipUrl: url, + ...props, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('has a primary action set to a url and a post data-method', () => { + const actionPrimary = findModal().props('actionPrimary'); + + expect(actionPrimary.attributes).toEqual( + expect.objectContaining([ + { + category: 'primary', + variant: 'confirm', + href: url, + 'data-method': 'post', + }, + ]), + ); + }); + + it('shows a take ownership message', () => { + expect(findModal().text()).toBe( + 'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?', + ); + }); + + it('emits the cancel event when clicking on cancel', async () => { + findModal().vm.$emit('cancel'); + + expect(findModal().emitted('cancel')).toHaveLength(1); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js index 446412a4f02..540a08d2c7f 100644 --- a/spec/frontend/pipeline_wizard/components/editor_spec.js +++ b/spec/frontend/pipeline_wizard/components/editor_spec.js @@ -42,7 +42,7 @@ describe('Pages Yaml Editor wrapper', () => { it('does not cause the touch event to be emitted', () => { wrapper.setProps({ doc }); - expect(wrapper.emitted('touch')).not.toBeTruthy(); + expect(wrapper.emitted('touch')).toBeUndefined(); }); }); @@ -63,7 +63,7 @@ describe('Pages Yaml Editor wrapper', () => { it('emits touch if content is changed in editor', async () => { await wrapper.vm.editor.setValue('foo: boo'); - expect(wrapper.emitted('touch')).toBeTruthy(); + expect(wrapper.emitted('touch')).toEqual([expect.any(Array)]); }); }); }); diff --git a/spec/frontend/pipeline_wizard/components/step_spec.js b/spec/frontend/pipeline_wizard/components/step_spec.js index aa87b1d0b04..00b57f95ccc 100644 --- a/spec/frontend/pipeline_wizard/components/step_spec.js +++ b/spec/frontend/pipeline_wizard/components/step_spec.js @@ -139,7 +139,7 @@ describe('Pipeline Wizard - Step Page', () => { await mockPrevClick(); await nextTick(); - expect(wrapper.emitted().back).toBeTruthy(); + expect(wrapper.emitted().back).toEqual(expect.arrayContaining([])); }); it('lets "next" event bubble upwards', async () => { @@ -148,7 +148,7 @@ describe('Pipeline Wizard - Step Page', () => { await mockNextClick(); await nextTick(); - expect(wrapper.emitted().next).toBeTruthy(); + expect(wrapper.emitted().next).toEqual(expect.arrayContaining([])); }); }); diff --git a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js index 43719595c5c..b8e194015b0 100644 --- a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js +++ b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js @@ -1,4 +1,4 @@ -import { GlFormCheckbox, GlFormCheckboxGroup } from '@gitlab/ui'; +import { GlFormCheckbox, GlFormGroup, GlFormCheckboxGroup } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ChecklistWidget from '~/pipeline_wizard/components/widgets/checklist.vue'; @@ -21,6 +21,7 @@ describe('Pipeline Wizard - Checklist Widget', () => { return eventArray[eventArray.length - 1]; }; const findItem = (atIndex = 0) => wrapper.findAllComponents(GlFormCheckbox).at(atIndex); + const getGlFormGroup = () => wrapper.getComponent(GlFormGroup); const getGlFormCheckboxGroup = () => wrapper.getComponent(GlFormCheckboxGroup); // The item.ids *can* be passed inside props.items, but are usually @@ -57,6 +58,16 @@ describe('Pipeline Wizard - Checklist Widget', () => { expect(findItem().text()).toBe(props.items[0]); }); + it('assigns the same non-null value to label-for and form id', () => { + createComponent(); + const formGroupLabelFor = getGlFormGroup().attributes('label-for'); + const formCheckboxGroupId = getGlFormCheckboxGroup().attributes('id'); + + expect(formGroupLabelFor).not.toBeNull(); + expect(formCheckboxGroupId).not.toBeNull(); + expect(formGroupLabelFor).toBe(formCheckboxGroupId); + }); + it('displays an item with a help text', () => { createComponent(); const { text, help } = props.items[1]; diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js index e0210307823..3680d9d62c7 100644 --- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js +++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js @@ -24,12 +24,14 @@ describe('The Pipeline Tabs', () => { const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter'); const findJobsBadge = () => wrapper.findByTestId('builds-counter'); + const findTestsBadge = () => wrapper.findByTestId('tests-counter'); const defaultProvide = { defaultTabValue: '', failedJobsCount: 1, failedJobsSummary: [], totalJobCount: 10, + testsCount: 123, }; const createComponent = (provide = {}) => { @@ -41,7 +43,6 @@ describe('The Pipeline Tabs', () => { }, stubs: { GlTab, - TestReports: { template: '<div id="tests" />' }, }, }), ); @@ -82,6 +83,7 @@ describe('The Pipeline Tabs', () => { tabName | badgeComponent | badgeText ${'Jobs'} | ${findJobsBadge} | ${String(defaultProvide.totalJobCount)} ${'Failed Jobs'} | ${findFailedJobsBadge} | ${String(defaultProvide.failedJobsCount)} + ${'Tests'} | ${findTestsBadge} | ${String(defaultProvide.testsCount)} `('shows badge for $tabName with the correct text', ({ badgeComponent, badgeText }) => { createComponent(); diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js index 6c743f92116..f958f12acd4 100644 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js @@ -102,7 +102,7 @@ describe('Pipelines filtered search', () => { it('emits filterPipelines on submit with correct filter', () => { findFilteredSearch().vm.$emit('submit', mockSearch); - expect(wrapper.emitted('filterPipelines')).toBeTruthy(); + expect(wrapper.emitted('filterPipelines')).toHaveLength(1); expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]); }); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js index 1ff32b03344..e712cdeaea2 100644 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js @@ -1,4 +1,5 @@ import { GlDropdown } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -61,11 +62,10 @@ describe('Pipelines stage component', () => { const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]'); - const openStageDropdown = () => { - findDropdownToggle().trigger('click'); - return new Promise((resolve) => { - wrapper.vm.$root.$on('bv::dropdown::show', resolve); - }); + const openStageDropdown = async () => { + await findDropdownToggle().trigger('click'); + await waitForPromises(); + await nextTick(); }; describe('loading state', () => { @@ -77,7 +77,10 @@ describe('Pipelines stage component', () => { await openStageDropdown(); }); - it('displays loading state while jobs are being fetched', () => { + it('displays loading state while jobs are being fetched', async () => { + jest.runOnlyPendingTimers(); + await nextTick(); + expect(findLoadingState().exists()).toBe(true); expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText); }); @@ -98,46 +101,41 @@ describe('Pipelines stage component', () => { expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true); }); - it('should render a dropdown with the status icon', () => { + it('renders a dropdown with the status icon', () => { expect(findDropdown().exists()).toBe(true); expect(findDropdownToggle().exists()).toBe(true); expect(findCiIcon().exists()).toBe(true); }); - it('should render a borderless ci-icon', () => { + it('renders a borderless ci-icon', () => { expect(findCiIcon().exists()).toBe(true); expect(findCiIcon().props('isBorderless')).toBe(true); expect(findCiIcon().classes('borderless')).toBe(true); }); - it('should render a ci-icon with a custom border class', () => { + it('renders a ci-icon with a custom border class', () => { expect(findCiIcon().exists()).toBe(true); expect(findCiIcon().classes('gl-border')).toBe(true); }); }); - describe('when update dropdown is changed', () => { - beforeEach(() => { - createComponent(); - }); - }); - describe('when user opens dropdown and stage request is successful', () => { beforeEach(async () => { mock.onGet(dropdownPath).reply(200, stageReply); createComponent(); await openStageDropdown(); + await jest.runAllTimers(); await axios.waitForAll(); }); - it('should render the received data and emit `clickedDropdown` event', async () => { + it('renders the received data and emit `clickedDropdown` event', async () => { expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); expect(findDropdownMenuTitle().text()).toContain(stageReply.name); expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); }); - it('should refresh when updateDropdown is set to true', async () => { + it('refreshes when updateDropdown is set to true', async () => { expect(mock.history.get).toHaveLength(1); wrapper.setProps({ updateDropdown: true }); @@ -148,15 +146,14 @@ describe('Pipelines stage component', () => { }); describe('when user opens dropdown and stage request fails', () => { - beforeEach(async () => { + it('should close the dropdown', async () => { mock.onGet(dropdownPath).reply(500); createComponent(); await openStageDropdown(); await axios.waitForAll(); - }); + await waitForPromises(); - it('should close the dropdown', () => { expect(findDropdown().classes('show')).toBe(false); }); }); @@ -181,26 +178,29 @@ describe('Pipelines stage component', () => { it('should update the stage to request the new endpoint provided', async () => { await openStageDropdown(); - await axios.waitForAll(); + jest.runOnlyPendingTimers(); + await waitForPromises(); expect(findDropdownMenu().text()).toContain('this is the updated content'); }); }); describe('pipelineActionRequestComplete', () => { - beforeEach(() => { + beforeEach(async () => { mock.onGet(dropdownPath).reply(200, stageReply); mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); createComponent(); + await waitForPromises(); + await nextTick(); }); const clickCiAction = async () => { await openStageDropdown(); - await axios.waitForAll(); + jest.runOnlyPendingTimers(); + await waitForPromises(); - findCiActionBtn().trigger('click'); - await axios.waitForAll(); + await findCiActionBtn().trigger('click'); }; it('closes dropdown when job item action is clicked', async () => { @@ -211,29 +211,30 @@ describe('Pipelines stage component', () => { expect(hidden).toHaveBeenCalledTimes(0); await clickCiAction(); + await waitForPromises(); expect(hidden).toHaveBeenCalledTimes(1); }); it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { await clickCiAction(); + await waitForPromises(); expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); }); }); describe('With merge trains enabled', () => { - beforeEach(async () => { + it('shows a warning on the dropdown', async () => { mock.onGet(dropdownPath).reply(200, stageReply); createComponent({ isMergeTrain: true, }); await openStageDropdown(); - await axios.waitForAll(); - }); + jest.runOnlyPendingTimers(); + await waitForPromises(); - it('shows a warning on the dropdown', () => { const warning = findMergeTrainWarning(); expect(warning.text()).toBe('Merge train pipeline jobs can not be retried'); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index cdeaa0db61d..7d1e4774a24 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -426,7 +426,7 @@ describe('Linked pipeline', () => { jest.spyOn(wrapper.vm, '$emit'); findButton().trigger('click'); - expect(wrapper.emitted().pipelineClicked).toBeTruthy(); + expect(wrapper.emitted().pipelineClicked).toHaveLength(1); }); it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => { diff --git a/spec/frontend/pipelines/performance_insights_modal_spec.js b/spec/frontend/pipelines/performance_insights_modal_spec.js index b745eb1d78e..8c802be7718 100644 --- a/spec/frontend/pipelines/performance_insights_modal_spec.js +++ b/spec/frontend/pipelines/performance_insights_modal_spec.js @@ -20,6 +20,7 @@ describe('Performance insights modal', () => { const findModal = () => wrapper.findComponent(GlModal); const findAlert = () => wrapper.findComponent(GlAlert); const findLink = () => wrapper.findComponent(GlLink); + const findLimitText = () => wrapper.findByTestId('limit-alert-text'); const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data'); const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link'); const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data'); @@ -62,8 +63,19 @@ describe('Performance insights modal', () => { expect(findModal().exists()).toBe(true); }); - it('does not dispaly alert', () => { - expect(findAlert().exists()).toBe(false); + it('displays alert', () => { + expect(findAlert().exists()).toBe(true); + }); + + it('displays feedback issue link', () => { + expect(findLink().text()).toBe('Feedback issue'); + expect(findLink().attributes('href')).toBe( + 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902', + ); + }); + + it('does not display limit text', () => { + expect(findLimitText().exists()).toBe(false); }); describe('queued duration card', () => { @@ -107,16 +119,13 @@ describe('Performance insights modal', () => { }); }); - describe('limit alert', () => { - it('displays limit alert when there is a next page', async () => { + describe('with next page', () => { + it('displays limit text when there is a next page', async () => { createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]); await waitForPromises(); - expect(findAlert().exists()).toBe(true); - expect(findLink().attributes('href')).toBe( - 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902', - ); + expect(findLimitText().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js index e24d2e51f08..f554166da33 100644 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js @@ -84,13 +84,22 @@ describe('Pipeline Multi Actions Dropdown', () => { expect(wrapper.vm.artifacts).toEqual(artifacts); }); - it('should render all the provided artifacts', () => { - createComponent({ mockData: { artifacts } }); + it('should render all the provided artifacts when search query is empty', () => { + const searchQuery = ''; + createComponent({ mockData: { searchQuery, artifacts } }); expect(findAllArtifactItems()).toHaveLength(artifacts.length); expect(findEmptyMessage().exists()).toBe(false); }); + it('should render filtered artifacts when search query is not empty', () => { + const searchQuery = 'job-2'; + createComponent({ mockData: { searchQuery, artifacts } }); + + expect(findAllArtifactItems()).toHaveLength(1); + expect(findEmptyMessage().exists()).toBe(false); + }); + it('should render the correct artifact name and path', () => { createComponent({ mockData: { artifacts } }); diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index c6104a13216..25a97ecf49d 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -61,14 +61,14 @@ describe('Pipeline Url Component', () => { describe('commit user avatar', () => { it('renders when commit author exists', () => { const pipelineBranch = mockPipelineBranch(); - const { avatar_url, name, path } = pipelineBranch.pipeline.commit.author; + const { avatar_url: imgSrc, name, path } = pipelineBranch.pipeline.commit.author; createComponent(pipelineBranch); const component = wrapper.findComponent(UserAvatarLink); expect(component.exists()).toBe(true); expect(component.props()).toMatchObject({ imgSize: 16, - imgSrc: avatar_url, + imgSrc, imgAlt: name, linkHref: path, tooltipText: name, diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index ad6d650670a..0bed24e588e 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -45,6 +45,7 @@ describe('Pipelines', () => { ciLintPath: '/ci/lint', resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`, newPipelinePath: `${mockProjectPath}/pipelines/new`, + ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`, }; @@ -654,7 +655,12 @@ describe('Pipelines', () => { // Mock init a polling cycle wrapper.vm.poll.options.notificationCallback(true); - findStagesDropdownToggle().trigger('click'); + await findStagesDropdownToggle().trigger('click'); + jest.runOnlyPendingTimers(); + + // cancelMock is getting overwritten in pipelines_service.js#L29 + // so we have to spy on it again here + cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); await waitForPromises(); @@ -664,7 +670,8 @@ describe('Pipelines', () => { }); it('stops polling & restarts polling', async () => { - findStagesDropdownToggle().trigger('click'); + await findStagesDropdownToggle().trigger('click'); + jest.runOnlyPendingTimers(); await waitForPromises(); expect(cancelMock).not.toHaveBeenCalled(); diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js index 3c3143b1865..9b9ee4172f9 100644 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js @@ -94,8 +94,8 @@ describe('Test reports app', () => { beforeEach(() => createComponent()); it('sets testReports and shows tests', () => { - expect(wrapper.vm.testReports).toBeTruthy(); - expect(wrapper.vm.showTests).toBeTruthy(); + expect(wrapper.vm.testReports).toEqual(expect.any(Object)); + expect(wrapper.vm.showTests).toBe(true); }); it('shows tests details', () => { diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js index d11090cba8a..57e5ef0ed1d 100644 --- a/spec/frontend/projects/commits/components/author_select_spec.js +++ b/spec/frontend/projects/commits/components/author_select_spec.js @@ -71,7 +71,7 @@ describe('Author Select', () => { wrapper.setData({ hasSearchParam: true }); await nextTick(); - expect(findDropdownContainer().attributes('disabled')).toBeFalsy(); + expect(findDropdownContainer().attributes('disabled')).toBeUndefined(); }); it('has correct tooltip message', async () => { @@ -91,13 +91,13 @@ describe('Author Select', () => { wrapper.setData({ hasSearchParam: false }); await nextTick(); - expect(findDropdown().attributes('disabled')).toBeFalsy(); + expect(findDropdown().attributes('disabled')).toBeUndefined(); }); it('hasSearchParam if user types a truthy string', () => { wrapper.vm.setSearchParam('false'); - expect(wrapper.vm.hasSearchParam).toBeTruthy(); + expect(wrapper.vm.hasSearchParam).toBe(true); }); }); @@ -153,9 +153,9 @@ describe('Author Select', () => { }); it('has the correct props', async () => { - const [{ avatar_url, username }] = authors; + const [{ avatar_url: avatarUrl, username }] = authors; const result = { - avatarUrl: avatar_url, + avatarUrl, secondaryText: username, isChecked: true, }; diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js index 18e7f2e0f6e..c9ffdf20c32 100644 --- a/spec/frontend/projects/compare/components/app_spec.js +++ b/spec/frontend/projects/compare/components/app_spec.js @@ -34,7 +34,8 @@ describe('CompareApp component', () => { expect(wrapper.props()).toEqual( expect.objectContaining({ projectCompareIndexPath: defaultProps.projectCompareIndexPath, - refsProjectPath: defaultProps.refsProjectPath, + sourceProjectRefsPath: defaultProps.sourceProjectRefsPath, + targetProjectRefsPath: defaultProps.targetProjectRefsPath, paramsFrom: defaultProps.paramsFrom, paramsTo: defaultProps.paramsTo, }), diff --git a/spec/frontend/projects/compare/components/mock_data.js b/spec/frontend/projects/compare/components/mock_data.js index 61309928c26..81d64469a2a 100644 --- a/spec/frontend/projects/compare/components/mock_data.js +++ b/spec/frontend/projects/compare/components/mock_data.js @@ -1,7 +1,12 @@ -const refsProjectPath = 'some/refs/path'; +const sourceProjectRefsPath = 'some/refs/path'; +const targetProjectRefsPath = 'some/refs/path'; const paramsName = 'to'; const paramsBranch = 'main'; -const defaultProject = { +const sourceProject = { + name: 'some-to-name', + id: '2', +}; +const targetProject = { name: 'some-to-name', id: '1', }; @@ -9,29 +14,31 @@ const defaultProject = { export const appDefaultProps = { projectCompareIndexPath: 'some/path', projectMergeRequestPath: '', - projects: [defaultProject], + projects: [sourceProject], paramsFrom: 'main', paramsTo: 'target/branch', createMrPath: '', - refsProjectPath, - defaultProject, + sourceProjectRefsPath, + targetProjectRefsPath, + sourceProject, + targetProject, }; export const revisionCardDefaultProps = { - selectedProject: defaultProject, + selectedProject: targetProject, paramsBranch, revisionText: 'Source', - refsProjectPath, + refsProjectPath: sourceProjectRefsPath, paramsName, }; export const repoDropdownDefaultProps = { - selectedProject: defaultProject, + selectedProject: targetProject, paramsName, }; export const revisionDropdownDefaultProps = { - refsProjectPath, + refsProjectPath: sourceProjectRefsPath, paramsBranch, paramsName, }; diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js index 3034037fb1d..4fcecc3a307 100644 --- a/spec/frontend/projects/project_new_spec.js +++ b/spec/frontend/projects/project_new_spec.js @@ -1,6 +1,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import projectNew from '~/projects/project_new'; +import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper'; describe('New Project', () => { let $projectImportUrl; @@ -12,21 +13,27 @@ describe('New Project', () => { beforeEach(() => { setHTMLFixture(` - <div class='toggle-import-form'> - <div class='import-url-data'> - <div class="form-group"> - <input id="project_import_url" /> - </div> - <div id="import-url-auth-method"> - <div class="form-group"> - <input id="project-import-url-user" /> + <div class="tab-pane active"> + <div class='toggle-import-form'> + <form id="new_project"> + <div class='import-url-data'> + <div class="form-group"> + <input id="project_import_url" /> + </div> + <div id="import-url-auth-method"> + <div class="form-group"> + <input id="project-import-url-user" /> + </div> + <div class="form-group"> + <input id="project_import_url_password" /> + </div> + </div> + <input id="project_name" /> + <input id="project_path" /> </div> - <div class="form-group"> - <input id="project_import_url_password" /> - </div> - </div> - <input id="project_name" /> - <input id="project_path" /> + <div class="js-user-readme-repo"></div> + <button class="js-create-project-button"/> + </form> </div> </div> `); @@ -45,6 +52,38 @@ describe('New Project', () => { el.value = value; }; + describe('tracks manual path input', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking('_category_', undefined, jest.spyOn); + projectNew.bindEvents(); + $projectPath.oldInputValue = '_old_value_'; + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks the event', () => { + $projectPath.value = '_new_value_'; + + triggerEvent($projectPath, 'blur'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'user_input_path_slug', { + label: 'new_project_form', + }); + }); + + it('does not track the event when there has been no change', () => { + $projectPath.value = '_old_value_'; + + triggerEvent($projectPath, 'blur'); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + describe('deriveProjectPathFromUrl', () => { const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`; diff --git a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js index 5997c2a083c..79bce5a4b3f 100644 --- a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js +++ b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js @@ -1,6 +1,6 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; +import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BranchDropdown, { i18n, @@ -36,15 +36,20 @@ describe('Branch dropdown', () => { await waitForPromises(); }; - const findGlDropdown = () => wrapper.find(GlDropdown); - const findAllBranches = () => wrapper.findAll(GlDropdownItem); + const findGlDropdown = () => wrapper.findComponent(GlDropdown); + const findAllBranches = () => wrapper.findAllComponents(GlDropdownItem); const findNoDataMsg = () => wrapper.findByTestId('no-data'); - const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType); + const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); const findWildcardButton = () => wrapper.findByTestId('create-wildcard-button'); + const findHelpText = () => wrapper.findComponent(GlSprintf); const setSearchTerm = (searchTerm) => findGlSearchBoxByType().vm.$emit('input', searchTerm); beforeEach(() => createComponent()); + afterEach(() => { + wrapper.destroy(); + }); + it('renders a GlDropdown component with the correct props', () => { expect(findGlDropdown().props()).toMatchObject({ text: value }); }); @@ -85,6 +90,10 @@ describe('Branch dropdown', () => { findWildcardButton().vm.$emit('click'); expect(wrapper.emitted('createWildcard')).toEqual([[searchTerm]]); }); + + it('renders help text', () => { + expect(findHelpText().attributes('message')).toBe(i18n.branchHelpText); + }); }); it('displays an error message if fetch failed', async () => { diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js new file mode 100644 index 00000000000..3592fa50622 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js @@ -0,0 +1,57 @@ +import { nextTick } from 'vue'; +import { GlLink } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import Protections, { + i18n, +} from '~/projects/settings/branch_rules/components/protections/index.vue'; +import PushProtections from '~/projects/settings/branch_rules/components/protections/push_protections.vue'; +import MergeProtections from '~/projects/settings/branch_rules/components/protections/merge_protections.vue'; +import { protections } from '../../mock_data'; + +describe('Branch Protections', () => { + let wrapper; + + const createComponent = async () => { + wrapper = mountExtended(Protections, { + propsData: { protections }, + }); + await nextTick(); + }; + + const findHeading = () => wrapper.find('h4'); + const findHelpText = () => wrapper.findByTestId('protections-help-text'); + const findHelpLink = () => wrapper.findComponent(GlLink); + const findPushProtections = () => wrapper.findComponent(PushProtections); + const findMergeProtections = () => wrapper.findComponent(MergeProtections); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a heading', () => { + expect(findHeading().text()).toBe(i18n.protections); + }); + + it('renders help text', () => { + expect(findHelpText().text()).toMatchInterpolatedText(i18n.protectionsHelpText); + expect(findHelpLink().attributes('href')).toBe('/help/user/project/protected_branches'); + }); + + it('renders a PushProtections component with correct props', () => { + expect(findPushProtections().props('membersAllowedToPush')).toStrictEqual( + protections.membersAllowedToPush, + ); + expect(findPushProtections().props('allowForcePush')).toBe(protections.allowForcePush); + }); + + it('renders a MergeProtections component with correct props', () => { + expect(findMergeProtections().props('membersAllowedToMerge')).toStrictEqual( + protections.membersAllowedToMerge, + ); + expect(findMergeProtections().props('requireCodeOwnersApproval')).toBe( + protections.requireCodeOwnersApproval, + ); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js new file mode 100644 index 00000000000..0e168a2ad78 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js @@ -0,0 +1,53 @@ +import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import MergeProtections, { + i18n, +} from '~/projects/settings/branch_rules/components/protections/merge_protections.vue'; +import { membersAllowedToMerge, requireCodeOwnersApproval } from '../../mock_data'; + +describe('Merge Protections', () => { + let wrapper; + + const propsData = { + membersAllowedToMerge, + requireCodeOwnersApproval, + }; + + const createComponent = () => { + wrapper = mountExtended(MergeProtections, { + propsData, + }); + }; + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findCodeOwnersApprovalCheckbox = () => wrapper.findComponent(GlFormCheckbox); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a form group with the correct label', () => { + expect(findFormGroup().text()).toContain(i18n.allowedToMerge); + }); + + describe('Require code owners approval checkbox', () => { + it('renders a checkbox with the correct props', () => { + expect(findCodeOwnersApprovalCheckbox().vm.$attrs.checked).toBe( + propsData.requireCodeOwnersApproval, + ); + }); + + it('renders help text', () => { + expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalTitle); + expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalHelpText); + }); + + it('emits a change-allow-force-push event when changed', () => { + findCodeOwnersApprovalCheckbox().vm.$emit('change', false); + + expect(wrapper.emitted('change-require-code-owners-approval')[0]).toEqual([false]); + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js new file mode 100644 index 00000000000..d54dad08338 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js @@ -0,0 +1,50 @@ +import { GlFormGroup, GlSprintf, GlFormCheckbox } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import PushProtections, { + i18n, +} from '~/projects/settings/branch_rules/components/protections/push_protections.vue'; +import { membersAllowedToPush, allowForcePush } from '../../mock_data'; + +describe('Push Protections', () => { + let wrapper; + const propsData = { + membersAllowedToPush, + allowForcePush, + }; + + const createComponent = () => { + wrapper = shallowMountExtended(PushProtections, { + propsData, + }); + }; + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findAllowForcePushCheckbox = () => wrapper.findComponent(GlFormCheckbox); + const findHelpText = () => wrapper.findComponent(GlSprintf); + + beforeEach(() => createComponent()); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a form group with the correct label', () => { + expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush); + }); + + describe('Allow force push checkbox', () => { + it('renders a checkbox with the correct props', () => { + expect(findAllowForcePushCheckbox().vm.$attrs.checked).toBe(propsData.allowForcePush); + }); + + it('renders help text', () => { + expect(findHelpText().attributes('message')).toBe(i18n.forcePushTitle); + }); + + it('emits a change-allow-force-push event when changed', () => { + findAllowForcePushCheckbox().vm.$emit('change', false); + + expect(wrapper.emitted('change-allow-force-push')[0]).toEqual([false]); + }); + }); +}); diff --git a/spec/frontend/projects/settings/branch_rules/mock_data.js b/spec/frontend/projects/settings/branch_rules/mock_data.js new file mode 100644 index 00000000000..32cca027d19 --- /dev/null +++ b/spec/frontend/projects/settings/branch_rules/mock_data.js @@ -0,0 +1,10 @@ +export const membersAllowedToPush = ['Maintainers', 'Developers']; +export const allowForcePush = false; +export const membersAllowedToMerge = ['Maintainers']; +export const requireCodeOwnersApproval = false; +export const protections = { + membersAllowedToPush, + allowForcePush, + membersAllowedToMerge, + requireCodeOwnersApproval, +}; diff --git a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js index 66ae6ddc02d..b0b2b9191d4 100644 --- a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js +++ b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js @@ -3,9 +3,12 @@ import { getParameterByName } from '~/lib/utils/url_utility'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RuleEdit from '~/projects/settings/branch_rules/components/rule_edit.vue'; import BranchDropdown from '~/projects/settings/branch_rules/components/branch_dropdown.vue'; +import Protections from '~/projects/settings/branch_rules/components/protections/index.vue'; jest.mock('~/lib/utils/url_utility', () => ({ getParameterByName: jest.fn().mockImplementation(() => 'main'), + joinPaths: jest.fn(), + setUrlFragment: jest.fn(), })); describe('Edit branch rule', () => { @@ -16,10 +19,15 @@ describe('Edit branch rule', () => { wrapper = shallowMountExtended(RuleEdit, { propsData: { projectPath } }); }; - const findBranchDropdown = () => wrapper.find(BranchDropdown); + const findBranchDropdown = () => wrapper.findComponent(BranchDropdown); + const findProtections = () => wrapper.findComponent(Protections); beforeEach(() => createComponent()); + afterEach(() => { + wrapper.destroy(); + }); + it('gets the branch param from url', () => { expect(getParameterByName).toHaveBeenCalledWith('branch'); }); @@ -46,4 +54,55 @@ describe('Edit branch rule', () => { expect(findBranchDropdown().props('value')).toBe(wildcard); }); }); + + describe('Protections', () => { + it('renders a Protections component with the correct props', () => { + expect(findProtections().props('protections')).toMatchObject({ + membersAllowedToPush: [], + allowForcePush: false, + membersAllowedToMerge: [], + requireCodeOwnersApproval: false, + }); + }); + + it('updates protections when change-allowed-to-push-members is emitted', async () => { + const membersAllowedToPush = ['test']; + findProtections().vm.$emit('change-allowed-to-push-members', membersAllowedToPush); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ membersAllowedToPush }), + ); + }); + + it('updates protections when change-allow-force-push is emitted', async () => { + const allowForcePush = true; + findProtections().vm.$emit('change-allow-force-push', allowForcePush); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ allowForcePush }), + ); + }); + + it('updates protections when change-allowed-to-merge-members is emitted', async () => { + const membersAllowedToMerge = ['test']; + findProtections().vm.$emit('change-allowed-to-merge-members', membersAllowedToMerge); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ membersAllowedToMerge }), + ); + }); + + it('updates protections when change-require-code-owners-approval is emitted', async () => { + const requireCodeOwnersApproval = true; + findProtections().vm.$emit('change-require-code-owners-approval', requireCodeOwnersApproval); + await nextTick(); + + expect(findProtections().props('protections')).toEqual( + expect.objectContaining({ requireCodeOwnersApproval }), + ); + }); + }); }); diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js index 85b09ced024..bde7148078d 100644 --- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js +++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js @@ -1,11 +1,19 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_1.query.graphql.json'; +import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_2.query.graphql.json'; import { groupNamespaces, userNamespaces, } from 'jest/vue_shared/components/namespace_select/mock_data'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue'; import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; +import searchNamespacesWhereUserCanTransferProjectsQuery from '~/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql'; +import waitForPromises from 'helpers/wait_for_promises'; describe('Transfer project form', () => { let wrapper; @@ -13,36 +21,50 @@ describe('Transfer project form', () => { const confirmButtonText = 'Confirm'; const confirmationPhrase = 'You must construct additional pylons!'; - const createComponent = () => - shallowMountExtended(TransferProjectForm, { + const runDebounce = () => jest.runAllTimers(); + + Vue.use(VueApollo); + + const defaultQueryHandler = jest + .fn() + .mockResolvedValue(searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1); + + const createComponent = ({ + requestHandlers = [[searchNamespacesWhereUserCanTransferProjectsQuery, defaultQueryHandler]], + } = {}) => { + wrapper = shallowMountExtended(TransferProjectForm, { propsData: { userNamespaces, groupNamespaces, confirmButtonText, confirmationPhrase, }, + apolloProvider: createMockApollo(requestHandlers), }); + }; const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect); const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger); - beforeEach(() => { - wrapper = createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); it('renders the namespace selector', () => { + createComponent(); + expect(findNamespaceSelect().exists()).toBe(true); }); it('renders the confirm button', () => { + createComponent(); + expect(findConfirmDanger().exists()).toBe(true); }); it('disables the confirm button by default', () => { + createComponent(); + expect(findConfirmDanger().attributes('disabled')).toBe('true'); }); @@ -50,6 +72,8 @@ describe('Transfer project form', () => { const [selectedItem] = groupNamespaces; beforeEach(() => { + createComponent(); + findNamespaceSelect().vm.$emit('select', selectedItem); }); @@ -69,4 +93,132 @@ describe('Transfer project form', () => { expect(wrapper.emitted('confirm')).toBeDefined(); }); }); + + it('passes correct props to `NamespaceSelect` component', async () => { + createComponent(); + + runDebounce(); + await waitForPromises(); + + const { + namespace, + groups, + } = searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser; + + expect(findNamespaceSelect().props()).toMatchObject({ + userNamespaces: [ + { + id: getIdFromGraphQLId(namespace.id), + humanName: namespace.fullName, + }, + ], + groupNamespaces: groups.nodes.map((node) => ({ + id: getIdFromGraphQLId(node.id), + humanName: node.fullName, + })), + hasNextPageOfGroups: true, + isLoadingMoreGroups: false, + isSearchLoading: false, + shouldFilterNamespaces: false, + }); + }); + + describe('when `search` event is fired', () => { + const arrange = async () => { + createComponent(); + + findNamespaceSelect().vm.$emit('search', 'foo'); + + await nextTick(); + }; + + it('sets `isSearchLoading` prop to `true`', async () => { + await arrange(); + + expect(findNamespaceSelect().props('isSearchLoading')).toBe(true); + }); + + it('passes `search` variable to query', async () => { + await arrange(); + + runDebounce(); + await waitForPromises(); + + expect(defaultQueryHandler).toHaveBeenCalledWith(expect.objectContaining({ search: 'foo' })); + }); + }); + + describe('when `load-more-groups` event is fired', () => { + let queryHandler; + + const arrange = async () => { + queryHandler = jest.fn(); + queryHandler.mockResolvedValueOnce( + searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1, + ); + queryHandler.mockResolvedValueOnce( + searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2, + ); + + createComponent({ + requestHandlers: [[searchNamespacesWhereUserCanTransferProjectsQuery, queryHandler]], + }); + + runDebounce(); + await waitForPromises(); + + findNamespaceSelect().vm.$emit('load-more-groups'); + await nextTick(); + }; + + it('sets `isLoadingMoreGroups` prop to `true`', async () => { + await arrange(); + + expect(findNamespaceSelect().props('isLoadingMoreGroups')).toBe(true); + }); + + it('passes `after` and `first` variables to query', async () => { + await arrange(); + + runDebounce(); + await waitForPromises(); + + expect(queryHandler).toHaveBeenCalledWith( + expect.objectContaining({ + first: 25, + after: + searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups + .pageInfo.endCursor, + }), + ); + }); + + it('updates `groupNamespaces` prop with new groups', async () => { + await arrange(); + + runDebounce(); + await waitForPromises(); + + expect(findNamespaceSelect().props('groupNamespaces')).toEqual( + [ + ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups + .nodes, + ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2.data.currentUser.groups + .nodes, + ].map((node) => ({ + id: getIdFromGraphQLId(node.id), + humanName: node.fullName, + })), + ); + }); + + it('updates `hasNextPageOfGroups` prop', async () => { + await arrange(); + + runDebounce(); + await waitForPromises(); + + expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false); + }); + }); }); diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js index fc906194059..a079b0b97fd 100644 --- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js +++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js @@ -50,39 +50,33 @@ describe('PrometheusMetrics', () => { customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LOADING); expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toEqual(false); - expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy(); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); - - expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); + + expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); }); it('should show metrics list when called with `list`', () => { customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true); expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); }); it('should show empty state when called with `empty`', () => { customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toEqual(false); - expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy(); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false); expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toEqual(false); @@ -94,14 +88,12 @@ describe('PrometheusMetrics', () => { const $metricsListLi = customMetrics.$monitoredCustomMetricsList.find('li'); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); expect($metricsListLi.length).toEqual(metrics.length); }); @@ -114,10 +106,10 @@ describe('PrometheusMetrics', () => { false, ); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true); + expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); }); }); }); diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js index 0df2aad5882..a65cbe1a47a 100644 --- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js @@ -54,25 +54,25 @@ describe('PrometheusMetrics', () => { it('should show loading state when called with `loading`', () => { prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true); }); it('should show metrics list when called with `list`', () => { prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false); }); it('should show empty state when called with `empty`', () => { prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true); }); }); @@ -88,8 +88,8 @@ describe('PrometheusMetrics', () => { const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li'); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false); expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual( '3 exporters with 12 metrics were found', @@ -102,8 +102,8 @@ describe('PrometheusMetrics', () => { it('should show missing environment variables list', () => { prometheusMetrics.populateActiveMetrics(missingVarMetrics); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBe(false); expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2'); expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2); @@ -143,12 +143,12 @@ describe('PrometheusMetrics', () => { prometheusMetrics.loadActiveMetrics(); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false); expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); await waitForPromises(); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); }); it('should show empty state if response failed to load', async () => { @@ -158,8 +158,8 @@ describe('PrometheusMetrics', () => { await waitForPromises(); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false); }); it('should populate metrics list once response is loaded', async () => { diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap index 90a33152877..55e3dda60a0 100644 --- a/spec/frontend/releases/__snapshots__/util_spec.js.snap +++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap @@ -55,6 +55,7 @@ Object { "commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0", "descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>", "evidences": Array [], + "historicalRelease": false, "milestones": Array [], "name": "The second release", "releasedAt": 2019-01-10T00:00:00.000Z, @@ -159,6 +160,7 @@ Object { "sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", }, ], + "historicalRelease": false, "milestones": Array [ Object { "__typename": "Milestone", @@ -208,6 +210,7 @@ exports[`releases/util.js convertOneReleaseForEditingGraphQLResponse matches sna Object { "data": Object { "_links": Object { + "__typename": "ReleaseLinks", "self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1", "selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1", }, @@ -215,6 +218,7 @@ Object { "count": undefined, "links": Array [ Object { + "__typename": "ReleaseAssetLink", "directAssetPath": "/binaries/awesome-app-3", "id": "gid://gitlab/Releases::Link/13", "linkType": "image", @@ -222,6 +226,7 @@ Object { "url": "https://example.com/image", }, Object { + "__typename": "ReleaseAssetLink", "directAssetPath": "/binaries/awesome-app-2", "id": "gid://gitlab/Releases::Link/12", "linkType": "package", @@ -229,6 +234,7 @@ Object { "url": "https://example.com/package", }, Object { + "__typename": "ReleaseAssetLink", "directAssetPath": "/binaries/awesome-app-1", "id": "gid://gitlab/Releases::Link/11", "linkType": "runbook", @@ -236,6 +242,7 @@ Object { "url": "http://localhost/releases-namespace/releases-project/runbook", }, Object { + "__typename": "ReleaseAssetLink", "directAssetPath": "/binaries/linux-amd64", "id": "gid://gitlab/Releases::Link/10", "linkType": "other", @@ -250,6 +257,7 @@ Object { "evidences": Array [], "milestones": Array [ Object { + "__typename": "Milestone", "id": "gid://gitlab/Milestone/123", "issueStats": Object {}, "stats": undefined, @@ -258,6 +266,7 @@ Object { "webUrl": undefined, }, Object { + "__typename": "Milestone", "id": "gid://gitlab/Milestone/124", "issueStats": Object {}, "stats": undefined, @@ -373,6 +382,7 @@ Object { "sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", }, ], + "historicalRelease": false, "milestones": Array [ Object { "__typename": "Milestone", diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js index 167ae4f32a2..c9921185bad 100644 --- a/spec/frontend/releases/components/release_block_header_spec.js +++ b/spec/frontend/releases/components/release_block_header_spec.js @@ -1,8 +1,9 @@ -import { GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlLink, GlBadge } from '@gitlab/ui'; import { merge } from 'lodash'; import originalRelease from 'test_fixtures/api/releases/release.json'; import setWindowLocation from 'helpers/set_window_location_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { __ } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import ReleaseBlockHeader from '~/releases/components/release_block_header.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; @@ -12,10 +13,11 @@ describe('Release block header', () => { let release; const factory = (releaseUpdates = {}) => { - wrapper = shallowMount(ReleaseBlockHeader, { + wrapper = shallowMountExtended(ReleaseBlockHeader, { propsData: { release: merge({}, release, releaseUpdates), }, + stubs: { GlBadge }, }); }; @@ -30,6 +32,7 @@ describe('Release block header', () => { const findHeader = () => wrapper.find('h2'); const findHeaderLink = () => findHeader().find(GlLink); const findEditButton = () => wrapper.find('.js-edit-button'); + const findBadge = () => wrapper.findComponent(GlBadge); describe('when _links.self is provided', () => { beforeEach(() => { @@ -84,4 +87,34 @@ describe('Release block header', () => { expect(findEditButton().exists()).toBe(false); }); }); + + describe('upcoming release', () => { + beforeEach(() => { + factory({ upcomingRelease: true, historicalRelease: false }); + }); + + it('shows a badge that the release is upcoming', () => { + const badge = findBadge(); + + expect(badge.text()).toBe(__('Upcoming Release')); + expect(badge.props('variant')).toBe('warning'); + }); + }); + + describe('historical release', () => { + beforeEach(() => { + factory({ upcomingRelease: false, historicalRelease: true }); + }); + + it('shows a badge that the release is historical', () => { + const badge = findBadge(); + + expect(badge.text()).toBe(__('Historical release')); + expect(badge.attributes('title')).toBe( + __( + 'This release was created with a date in the past. Evidence collection at the moment of the release is unavailable.', + ), + ); + }); + }); }); diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js index 888b49f3e0c..bdfba8d6878 100644 --- a/spec/frontend/reports/components/report_section_spec.js +++ b/spec/frontend/reports/components/report_section_spec.js @@ -1,16 +1,15 @@ -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { GlButton } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import reportSection from '~/reports/components/report_section.vue'; +import ReportItem from '~/reports/components/report_item.vue'; +import ReportSection from '~/reports/components/report_section.vue'; -describe('Report section', () => { - let vm; +describe('ReportSection component', () => { let wrapper; - const ReportSection = Vue.extend(reportSection); - const findCollapseButton = () => wrapper.findByTestId('report-section-expand-button'); + + const findButton = () => wrapper.findComponent(GlButton); const findPopover = () => wrapper.findComponent(HelpPopover); + const findReportSection = () => wrapper.find('.js-report-section-container'); const resolvedIssues = [ { @@ -33,34 +32,24 @@ describe('Report section', () => { alwaysOpen: false, }; - const createComponent = (props) => { - wrapper = extendedWrapper( - mount(reportSection, { - propsData: { - ...defaultProps, - ...props, - }, - }), - ); - return wrapper; + const createComponent = ({ props = {}, data = {}, slots = {} } = {}) => { + wrapper = mountExtended(ReportSection, { + propsData: { + ...defaultProps, + ...props, + }, + data() { + return data; + }, + slots, + }); }; afterEach(() => { - if (vm) { - vm.$destroy(); - vm = null; - } - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } + wrapper.destroy(); }); describe('computed', () => { - beforeEach(() => { - vm = mountComponent(ReportSection, defaultProps); - }); - describe('isCollapsible', () => { const testMatrix = [ { hasIssues: false, alwaysOpen: false, isCollapsible: false }, @@ -73,12 +62,10 @@ describe('Report section', () => { const issues = hasIssues ? 'has issues' : 'has no issues'; const open = alwaysOpen ? 'is always open' : 'is not always open'; - it(`is ${isCollapsible}, if the report ${issues} and ${open}`, async () => { - vm.hasIssues = hasIssues; - vm.alwaysOpen = alwaysOpen; + it(`is ${isCollapsible}, if the report ${issues} and ${open}`, () => { + createComponent({ props: { hasIssues, alwaysOpen } }); - await nextTick(); - expect(vm.isCollapsible).toBe(isCollapsible); + expect(wrapper.vm.isCollapsible).toBe(isCollapsible); }); }); }); @@ -95,12 +82,10 @@ describe('Report section', () => { const issues = isCollapsed ? 'is collapsed' : 'is not collapsed'; const open = alwaysOpen ? 'is always open' : 'is not always open'; - it(`is ${isExpanded}, if the report ${issues} and ${open}`, async () => { - vm.isCollapsed = isCollapsed; - vm.alwaysOpen = alwaysOpen; + it(`is ${isExpanded}, if the report ${issues} and ${open}`, () => { + createComponent({ props: { alwaysOpen }, data: { isCollapsed } }); - await nextTick(); - expect(vm.isExpanded).toBe(isExpanded); + expect(wrapper.vm.isExpanded).toBe(isExpanded); }); }); }); @@ -108,110 +93,105 @@ describe('Report section', () => { describe('when it is loading', () => { it('should render loading indicator', () => { - vm = mountComponent(ReportSection, { - component: '', - status: 'LOADING', - loadingText: 'Loading Code Quality report', - errorText: 'foo', - successText: 'Code quality improved on 1 point and degraded on 1 point', - hasIssues: false, + createComponent({ + props: { + component: '', + status: 'LOADING', + loadingText: 'Loading Code Quality report', + errorText: 'foo', + successText: 'Code quality improved on 1 point and degraded on 1 point', + hasIssues: false, + }, }); - expect(vm.$el.textContent.trim()).toEqual('Loading Code Quality report'); + expect(wrapper.text()).toBe('Loading Code Quality report'); }); }); describe('with success status', () => { - beforeEach(() => { - vm = mountComponent(ReportSection, { - ...defaultProps, - hasIssues: true, - }); - }); - it('should render provided data', () => { - expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( - 'Code quality improved on 1 point and degraded on 1 point', - ); + createComponent({ props: { hasIssues: true } }); - expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual( - resolvedIssues.length, + expect(wrapper.find('.js-code-text').text()).toBe( + 'Code quality improved on 1 point and degraded on 1 point', ); + expect(wrapper.findAllComponents(ReportItem)).toHaveLength(resolvedIssues.length); }); describe('toggleCollapsed', () => { - const hiddenCss = { display: 'none' }; - it('toggles issues', async () => { - vm.$el.querySelector('button').click(); + createComponent({ props: { hasIssues: true } }); + + await findButton().trigger('click'); - await nextTick(); - expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss); - expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Collapse'); + expect(findReportSection().isVisible()).toBe(true); + expect(findButton().text()).toBe('Collapse'); - vm.$el.querySelector('button').click(); + await findButton().trigger('click'); - await nextTick(); - expect(vm.$el.querySelector('.js-report-section-container')).toHaveCss(hiddenCss); - expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Expand'); + expect(findReportSection().isVisible()).toBe(false); + expect(findButton().text()).toBe('Expand'); }); - it('is always expanded, if always-open is set to true', async () => { - vm.alwaysOpen = true; - await nextTick(); - expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss); - expect(vm.$el.querySelector('button')).toBeNull(); + it('is always expanded, if always-open is set to true', () => { + createComponent({ props: { hasIssues: true, alwaysOpen: true } }); + + expect(findReportSection().isVisible()).toBe(true); + expect(findButton().exists()).toBe(false); }); }); }); describe('snowplow events', () => { - it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', async () => { - createComponent({ hasIssues: true, shouldEmitToggleEvent: true }); + it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', () => { + createComponent({ props: { hasIssues: true, shouldEmitToggleEvent: true } }); - expect(wrapper.emitted().toggleEvent).toBeUndefined(); + expect(wrapper.emitted('toggleEvent')).toBeUndefined(); - findCollapseButton().trigger('click'); - await nextTick(); - expect(wrapper.emitted().toggleEvent).toHaveLength(1); + findButton().trigger('click'); + + expect(wrapper.emitted('toggleEvent')).toEqual([[]]); }); - it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', async () => { - createComponent({ hasIssues: true }); + it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', () => { + createComponent({ props: { hasIssues: true } }); + + expect(wrapper.emitted('toggleEvent')).toBeUndefined(); - expect(wrapper.emitted().toggleEvent).toBeUndefined(); + findButton().trigger('click'); - findCollapseButton().trigger('click'); - await nextTick(); - expect(wrapper.emitted().toggleEvent).toBeUndefined(); + expect(wrapper.emitted('toggleEvent')).toBeUndefined(); }); - it('does not emit an event if always-open is set to true', async () => { - createComponent({ alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true }); + it('does not emit an event if always-open is set to true', () => { + createComponent({ + props: { alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true }, + }); - await nextTick(); - expect(wrapper.emitted().toggleEvent).toBeUndefined(); + expect(wrapper.emitted('toggleEvent')).toBeUndefined(); }); }); describe('with failed request', () => { it('should render error indicator', () => { - vm = mountComponent(ReportSection, { - component: '', - status: 'ERROR', - loadingText: 'Loading Code Quality report', - errorText: 'Failed to load Code Quality report', - successText: 'Code quality improved on 1 point and degraded on 1 point', - hasIssues: false, + createComponent({ + props: { + component: '', + status: 'ERROR', + loadingText: 'Loading Code Quality report', + errorText: 'Failed to load Code Quality report', + successText: 'Code quality improved on 1 point and degraded on 1 point', + hasIssues: false, + }, }); - expect(vm.$el.textContent.trim()).toEqual('Failed to load Code Quality report'); + expect(wrapper.text()).toBe('Failed to load Code Quality report'); }); }); describe('with action buttons passed to the slot', () => { beforeEach(() => { - vm = mountComponentWithSlots(ReportSection, { + createComponent({ props: { status: 'SUCCESS', successText: 'success', @@ -224,17 +204,17 @@ describe('Report section', () => { }); it('should render the passed button', () => { - expect(vm.$el.textContent.trim()).toContain('Action!'); + expect(wrapper.text()).toContain('Action!'); }); it('should still render the expand/collapse button', () => { - expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand'); + expect(findButton().text()).toBe('Expand'); }); }); describe('Success and Error slots', () => { const createComponentWithSlots = (status) => { - vm = mountComponentWithSlots(ReportSection, { + createComponent({ props: { status, hasIssues: true, @@ -250,25 +230,25 @@ describe('Report section', () => { it('only renders success slot when status is "SUCCESS"', () => { createComponentWithSlots('SUCCESS'); - expect(vm.$el.textContent.trim()).toContain('This is a success'); - expect(vm.$el.textContent.trim()).not.toContain('This is an error'); - expect(vm.$el.textContent.trim()).not.toContain('This is loading'); + expect(wrapper.text()).toContain('This is a success'); + expect(wrapper.text()).not.toContain('This is an error'); + expect(wrapper.text()).not.toContain('This is loading'); }); it('only renders error slot when status is "ERROR"', () => { createComponentWithSlots('ERROR'); - expect(vm.$el.textContent.trim()).toContain('This is an error'); - expect(vm.$el.textContent.trim()).not.toContain('This is a success'); - expect(vm.$el.textContent.trim()).not.toContain('This is loading'); + expect(wrapper.text()).toContain('This is an error'); + expect(wrapper.text()).not.toContain('This is a success'); + expect(wrapper.text()).not.toContain('This is loading'); }); it('only renders loading slot when status is "LOADING"', () => { createComponentWithSlots('LOADING'); - expect(vm.$el.textContent.trim()).toContain('This is loading'); - expect(vm.$el.textContent.trim()).not.toContain('This is an error'); - expect(vm.$el.textContent.trim()).not.toContain('This is a success'); + expect(wrapper.text()).toContain('This is loading'); + expect(wrapper.text()).not.toContain('This is an error'); + expect(wrapper.text()).not.toContain('This is a success'); }); }); @@ -280,9 +260,7 @@ describe('Report section', () => { }; beforeEach(() => { - createComponent({ - popoverOptions: options, - }); + createComponent({ props: { popoverOptions: options } }); }); it('popover is shown with options', () => { @@ -292,7 +270,7 @@ describe('Report section', () => { describe('when popover options are not defined', () => { beforeEach(() => { - createComponent({ popoverOptions: {} }); + createComponent({ props: { popoverOptions: {} } }); }); it('popover is not shown', () => { diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index 2b70cb84c67..0f7cf4e61b2 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -21,12 +21,13 @@ import blobInfoQuery from '~/repository/queries/blob_info.query.graphql'; import userInfoQuery from '~/repository/queries/user_info.query.graphql'; import applicationInfoQuery from '~/repository/queries/application_info.query.graphql'; import CodeIntelligence from '~/code_navigation/components/app.vue'; -import { redirectTo } from '~/lib/utils/url_utility'; +import * as urlUtility from '~/lib/utils/url_utility'; import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import httpStatusCodes from '~/lib/utils/http_status'; import LineHighlighter from '~/blob/line_highlighter'; import { LEGACY_FILE_TYPES } from '~/repository/constants'; +import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; import { simpleViewerMock, richViewerMock, @@ -53,7 +54,12 @@ const mockAxios = new MockAdapter(axios); const createMockStore = () => new Vuex.Store({ actions: { fetchData: jest.fn, setInitialData: jest.fn() } }); -const createComponent = async (mockData = {}, mountFn = shallowMount) => { +const mockRouterPush = jest.fn(); +const mockRouter = { + push: mockRouterPush, +}; + +const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute = {}) => { Vue.use(VueApollo); const { @@ -106,6 +112,10 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => { apolloProvider: fakeApollo, propsData: propsMock, mixins: [{ data: () => ({ ref: refMock }) }], + mocks: { + $route: mockRoute, + $router: mockRouter, + }, provide: { targetBranch: 'test', originalBranch: 'default-ref', @@ -158,10 +168,11 @@ describe('Blob content viewer component', () => { it('renders a BlobHeader component', async () => { await createComponent(); - expect(findBlobHeader().props('activeViewerType')).toEqual('simple'); + expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER); expect(findBlobHeader().props('hasRenderError')).toEqual(false); expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true); expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock); + expect(mockRouterPush).not.toHaveBeenCalled(); }); it('copies blob text to clipboard', async () => { @@ -179,7 +190,7 @@ describe('Blob content viewer component', () => { expect(findBlobContent().props('activeViewer')).toEqual({ fileType: 'text', tooLarge: false, - type: 'simple', + type: SIMPLE_BLOB_VIEWER, renderError: null, }); }); @@ -229,6 +240,12 @@ describe('Blob content viewer component', () => { expect(LineHighlighter).toHaveBeenCalled(); }); + it('does not load the LineHighlighter for RichViewers', async () => { + mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test'); + await createComponent({ blob: { ...richViewerMock, fileType, highlightJs } }); + expect(LineHighlighter).not.toHaveBeenCalled(); + }); + it('scrolls to the hash', async () => { mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test'); await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } }); @@ -241,10 +258,11 @@ describe('Blob content viewer component', () => { it('renders a BlobHeader component', async () => { await createComponent({ blob: richViewerMock }); - expect(findBlobHeader().props('activeViewerType')).toEqual('rich'); + expect(findBlobHeader().props('activeViewerType')).toEqual(RICH_BLOB_VIEWER); expect(findBlobHeader().props('hasRenderError')).toEqual(false); expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false); expect(findBlobHeader().props('blob')).toEqual(richViewerMock); + expect(mockRouterPush).not.toHaveBeenCalled(); }); it('renders a BlobContent component', async () => { @@ -254,30 +272,49 @@ describe('Blob content viewer component', () => { expect(findBlobContent().props('activeViewer')).toEqual({ fileType: 'markup', tooLarge: false, - type: 'rich', + type: RICH_BLOB_VIEWER, renderError: null, }); }); - it('updates viewer type when viewer changed is clicked', async () => { + it('changes to simple viewer when URL has code line hash', async () => { + jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce('L5'); + await createComponent({ blob: richViewerMock }); expect(findBlobContent().props('activeViewer')).toEqual( expect.objectContaining({ - type: 'rich', + type: SIMPLE_BLOB_VIEWER, + }), + ); + expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER); + }); + + it('updates viewer type when viewer changed is clicked', async () => { + await createComponent({ blob: richViewerMock }, shallowMount, { path: '/mock_path' }); + + expect(findBlobContent().props('activeViewer')).toEqual( + expect.objectContaining({ + type: RICH_BLOB_VIEWER, }), ); - expect(findBlobHeader().props('activeViewerType')).toEqual('rich'); + expect(findBlobHeader().props('activeViewerType')).toEqual(RICH_BLOB_VIEWER); - findBlobHeader().vm.$emit('viewer-changed', 'simple'); + findBlobHeader().vm.$emit('viewer-changed', SIMPLE_BLOB_VIEWER); await nextTick(); - expect(findBlobHeader().props('activeViewerType')).toEqual('simple'); + expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER); expect(findBlobContent().props('activeViewer')).toEqual( expect.objectContaining({ - type: 'simple', + type: SIMPLE_BLOB_VIEWER, }), ); + expect(mockRouterPush).toHaveBeenCalledWith({ + path: '/mock_path', + query: { + plain: '1', + }, + }); }); }); @@ -497,12 +534,12 @@ describe('Blob content viewer component', () => { it('simple edit redirects to the simple editor', () => { findWebIdeLink().vm.$emit('edit', 'simple'); - expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath); + expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath); }); it('IDE edit redirects to the IDE editor', () => { findWebIdeLink().vm.$emit('edit', 'ide'); - expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath); + expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath); }); it.each` @@ -537,4 +574,32 @@ describe('Blob content viewer component', () => { }, ); }); + + describe('active viewer based on plain attribute', () => { + it.each` + hasRichViewer | plain | activeViewerType + ${true} | ${'0'} | ${RICH_BLOB_VIEWER} + ${true} | ${'1'} | ${SIMPLE_BLOB_VIEWER} + ${false} | ${'0'} | ${SIMPLE_BLOB_VIEWER} + ${false} | ${'1'} | ${SIMPLE_BLOB_VIEWER} + `( + 'activeViewerType is `$activeViewerType` when hasRichViewer is $hasRichViewer and plain is set to $plain', + async ({ hasRichViewer, plain, activeViewerType }) => { + await createComponent( + { blob: hasRichViewer ? richViewerMock : simpleViewerMock }, + shallowMount, + { query: { plain } }, + ); + + await nextTick(); + + expect(findBlobContent().props('activeViewer')).toEqual( + expect.objectContaining({ + type: activeViewerType, + }), + ); + expect(findBlobHeader().props('activeViewerType')).toEqual(activeViewerType); + }, + ); + }); }); diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index 0a5766a25f9..4db295fe0b7 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -8,6 +8,7 @@ export const simpleViewerMock = { language: 'javascript', path: 'some_file.js', webPath: 'some_file.js', + blamePath: 'blame/file.js', editBlobPath: 'some_file.js/edit', gitpodBlobUrl: 'https://gitpod.io#path/to/blob.js', ideEditPath: 'some_file.js/ide/edit', diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js index 5847842f5a6..3b220ba8351 100644 --- a/spec/frontend/right_sidebar_spec.js +++ b/spec/frontend/right_sidebar_spec.js @@ -70,7 +70,7 @@ describe('RightSidebar', () => { it('should not hide collapsed icons', () => { [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => { - expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy(); + expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBe(false); }); }); }); diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js index 8a34cb14d8b..ffe3599ac64 100644 --- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js +++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js @@ -87,10 +87,10 @@ describe('AdminRunnerEditApp', () => { await createComponentWithApollo(); expect(findRunnerUpdateForm().props()).toMatchObject({ - runner: mockRunner, loading: false, runnerPath: mockRunnerPath, }); + expect(findRunnerUpdateForm().props('runner')).toEqual(mockRunner); }); describe('When there is an error', () => { diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js index 433be5d5027..509681c5a77 100644 --- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -14,6 +14,7 @@ import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; import RunnersJobs from '~/runner/components/runner_jobs.vue'; + import runnerQuery from '~/runner/graphql/show/runner.query.graphql'; import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue'; import { captureException } from '~/runner/sentry_utils'; @@ -94,10 +95,10 @@ describe('AdminRunnerShowApp', () => { }); it('shows basic runner details', async () => { - const expected = `Description Instance runner + const expected = `Description My Runner Last contact Never contacted Version 1.0.0 - IP Address 127.0.0.1 + IP Address None Executor None Architecture None Platform darwin @@ -182,17 +183,19 @@ describe('AdminRunnerShowApp', () => { }); describe('When loading', () => { - beforeEach(() => { + it('does not show runner details', () => { mockRunnerQueryResult(); createComponent(); - }); - it('does not show runner details', () => { expect(findRunnerDetails().exists()).toBe(false); }); it('does not show runner jobs', () => { + mockRunnerQueryResult(); + + createComponent(); + expect(findRunnersJobs().exists()).toBe(false); }); }); diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index aa1aa723491..97341be7d5d 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { GlToast, GlLink } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -19,10 +19,11 @@ import { createLocalState } from '~/runner/graphql/list/local_state'; import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; +import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; +import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue'; import RunnerList from '~/runner/components/runner_list.vue'; import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; -import RunnerCount from '~/runner/components/stat/runner_count.vue'; import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -37,12 +38,10 @@ import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, - STATUS_OFFLINE, - STATUS_STALE, RUNNER_PAGE_SIZE, } from '~/runner/constants'; import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql'; -import allRunnersCountQuery from '~/runner/graphql/list/all_runners_count.query.graphql'; +import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql'; import { captureException } from '~/runner/sentry_utils'; import { @@ -51,6 +50,7 @@ import { allRunnersDataPaginated, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyPageInfo, emptyStateSvgPath, emptyStateFilteredSvgPath, } from '../mock_data'; @@ -72,19 +72,24 @@ jest.mock('~/lib/utils/url_utility', () => ({ Vue.use(VueApollo); Vue.use(GlToast); +const COUNT_QUERIES = 7; // 4 tabs + 3 status queries + describe('AdminRunnersApp', () => { let wrapper; let cacheConfig; let localMutations; + let showToast; const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs); + const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete); + const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox); const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); - const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page'); + const findRunnerPaginationNext = () => findRunnerPagination().findByText(s__('Pagination|Next')); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); const createComponent = ({ @@ -117,6 +122,8 @@ describe('AdminRunnersApp', () => { ...options, }); + showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); + return waitForPromises(); }; @@ -128,17 +135,10 @@ describe('AdminRunnersApp', () => { afterEach(() => { mockRunnersHandler.mockReset(); mockRunnersCountHandler.mockReset(); + showToast.mockReset(); wrapper.destroy(); }); - it('shows the runner tabs with a runner count for each type', async () => { - await createComponent({ mountFn: mountExtended }); - - expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( - `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`, - ); - }); - it('shows the runner setup instructions', () => { createComponent(); @@ -146,27 +146,38 @@ describe('AdminRunnersApp', () => { expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE); }); - it('shows total runner counts', async () => { - await createComponent({ mountFn: mountExtended }); + describe('shows total runner counts', () => { + beforeEach(async () => { + await createComponent({ mountFn: mountExtended }); + }); + + it('fetches counts', () => { + expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES); + }); + + it('shows the runner tabs', () => { + expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( + `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`, + ); + }); - expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_ONLINE }); - expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_OFFLINE }); - expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_STALE }); - - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Online runners')} ${mockRunnersCount}`, - ); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Offline runners')} ${mockRunnersCount}`, - ); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Stale runners')} ${mockRunnersCount}`, - ); + it('shows the total', () => { + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Online runners')} ${mockRunnersCount}`, + ); + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Offline runners')} ${mockRunnersCount}`, + ); + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Stale runners')} ${mockRunnersCount}`, + ); + }); }); it('shows the runners list', async () => { await createComponent(); + expect(mockRunnersHandler).toHaveBeenCalledTimes(1); expect(findRunnerList().props('runners')).toEqual(mockRunners); }); @@ -226,18 +237,13 @@ describe('AdminRunnersApp', () => { }); describe('Single runner row', () => { - let showToast; - const { id: graphqlId, shortSha } = mockRunners[0]; const id = getIdFromGraphQLId(graphqlId); - const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners - const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs beforeEach(async () => { mockRunnersCountHandler.mockClear(); await createComponent({ mountFn: mountExtended }); - showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); }); it('Links to the runner page', async () => { @@ -252,7 +258,7 @@ describe('AdminRunnersApp', () => { findRunnerActionsCell().vm.$emit('toggledPaused'); - expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES); + expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2); expect(showToast).toHaveBeenCalledTimes(0); }); @@ -266,25 +272,20 @@ describe('AdminRunnersApp', () => { describe('when a filter is preselected', () => { beforeEach(async () => { - setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`); + setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&paused[]=true`); - await createComponent({ - stubs: { - RunnerStats, - RunnerCount, - }, - }); + await createComponent({ mountFn: mountExtended }); }); it('sets the filters in the search bar', () => { expect(findRunnerFilteredSearchBar().props('value')).toEqual({ runnerType: INSTANCE_TYPE, filters: [ - { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }, - { type: 'tag', value: { data: 'tag1', operator: '=' } }, + { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, + { type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } }, ], sort: 'CREATED_DESC', - pagination: { page: 1 }, + pagination: {}, }); }); @@ -292,7 +293,7 @@ describe('AdminRunnersApp', () => { expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, type: INSTANCE_TYPE, - tagList: ['tag1'], + paused: true, sort: DEFAULT_SORT, first: RUNNER_PAGE_SIZE, }); @@ -302,41 +303,34 @@ describe('AdminRunnersApp', () => { expect(mockRunnersCountHandler).toHaveBeenCalledWith({ type: INSTANCE_TYPE, status: STATUS_ONLINE, - tagList: ['tag1'], + paused: true, }); }); }); describe('when a filter is selected by the user', () => { - beforeEach(() => { - createComponent({ - stubs: { - RunnerStats, - RunnerCount, - }, - }); + beforeEach(async () => { + await createComponent({ mountFn: mountExtended }); findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, - filters: [ - { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, - { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } }, - ], + filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], sort: CREATED_ASC, }); + + await nextTick(); }); it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: expect.stringContaining('?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC'), + url: expect.stringContaining('?status[]=ONLINE&sort=CREATED_ASC'), }); }); it('requests the runners with filters', () => { expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, - tagList: ['tag1'], sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); @@ -344,7 +338,6 @@ describe('AdminRunnersApp', () => { it('fetches count results for requested status', () => { expect(mockRunnersCountHandler).toHaveBeenCalledWith({ - tagList: ['tag1'], status: STATUS_ONLINE, }); }); @@ -353,39 +346,79 @@ describe('AdminRunnersApp', () => { it('when runners have not loaded, shows a loading state', () => { createComponent(); expect(findRunnerList().props('loading')).toBe(true); + expect(findRunnerPagination().attributes('disabled')).toBe('true'); }); describe('when bulk delete is enabled', () => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures: { adminRunnersBulkDelete: true }, - }, + describe('Before runners are deleted', () => { + beforeEach(async () => { + await createComponent({ + mountFn: mountExtended, + provide: { + glFeatures: { adminRunnersBulkDelete: true }, + }, + }); }); - }); - it('runner list is checkable', () => { - expect(findRunnerList().props('checkable')).toBe(true); + it('runner bulk delete is available', () => { + expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners); + }); + + it('runner bulk delete checkbox is available', () => { + expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners); + }); + + it('runner list is checkable', () => { + expect(findRunnerList().props('checkable')).toBe(true); + }); + + it('responds to checked items by updating the local cache', () => { + const setRunnerCheckedMock = jest + .spyOn(localMutations, 'setRunnerChecked') + .mockImplementation(() => {}); + + const runner = mockRunners[0]; + + expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0); + + findRunnerList().vm.$emit('checked', { + runner, + isChecked: true, + }); + + expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1); + expect(setRunnerCheckedMock).toHaveBeenCalledWith({ + runner, + isChecked: true, + }); + }); }); - it('responds to checked items by updating the local cache', () => { - const setRunnerCheckedMock = jest - .spyOn(localMutations, 'setRunnerChecked') - .mockImplementation(() => {}); + describe('When runners are deleted', () => { + beforeEach(async () => { + await createComponent({ + mountFn: mountExtended, + provide: { + glFeatures: { adminRunnersBulkDelete: true }, + }, + }); + }); - const runner = mockRunners[0]; + it('count data is refetched', async () => { + expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES); - expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0); + findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' }); - findRunnerList().vm.$emit('checked', { - runner, - isChecked: true, + expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2); }); - expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1); - expect(setRunnerCheckedMock).toHaveBeenCalledWith({ - runner, - isChecked: true, + it('toast is shown', async () => { + expect(showToast).toHaveBeenCalledTimes(0); + + findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' }); + + expect(showToast).toHaveBeenCalledTimes(1); + expect(showToast).toHaveBeenCalledWith('Runners deleted'); }); }); }); @@ -394,13 +427,20 @@ describe('AdminRunnersApp', () => { beforeEach(async () => { mockRunnersHandler.mockResolvedValue({ data: { - runners: { nodes: [] }, + runners: { + nodes: [], + pageInfo: emptyPageInfo, + }, }, }); await createComponent(); }); + it('shows no errors', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); + it('shows an empty state', () => { expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false); }); @@ -440,19 +480,25 @@ describe('AdminRunnersApp', () => { }); describe('Pagination', () => { + const { pageInfo } = allRunnersDataPaginated.data.runners; + beforeEach(async () => { mockRunnersHandler.mockResolvedValue(allRunnersDataPaginated); await createComponent({ mountFn: mountExtended }); }); + it('passes the page info', () => { + expect(findRunnerPagination().props('pageInfo')).toEqual(pageInfo); + }); + it('navigates to the next page', async () => { await findRunnerPaginationNext().trigger('click'); expect(mockRunnersHandler).toHaveBeenLastCalledWith({ sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, - after: allRunnersDataPaginated.data.runners.pageInfo.endCursor, + after: pageInfo.endCursor, }); }); }); diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js index b2e8c5a3ad9..b06ab652212 100644 --- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js @@ -1,3 +1,4 @@ +import { __ } from '~/locale'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue'; import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; @@ -61,8 +62,16 @@ describe('RunnerTypeCell', () => { expect(wrapper.text()).toContain(mockDescription); }); - it('Displays the runner ip address', () => { - expect(wrapper.text()).toContain(mockIpAddress); + it('Displays ip address', () => { + expect(wrapper.text()).toContain(`${__('IP Address')} ${mockIpAddress}`); + }); + + it('Displays no ip address', () => { + createComponent({ + ipAddress: null, + }); + + expect(wrapper.text()).not.toContain(__('IP Address')); }); it('Displays a custom slot', () => { diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js index ed1a698d36f..19344a68f79 100644 --- a/spec/frontend/runner/components/registration/registration_token_spec.js +++ b/spec/frontend/runner/components/registration/registration_token_spec.js @@ -1,5 +1,5 @@ import { GlToast } from '@gitlab/ui'; -import { createLocalVue } from '@vue/test-utils'; +import Vue from 'vue'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RegistrationToken from '~/runner/components/registration/registration_token.vue'; import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; @@ -11,28 +11,17 @@ describe('RegistrationToken', () => { let wrapper; let showToast; - const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility); - - const vueWithGlToast = () => { - const localVue = createLocalVue(); - localVue.use(GlToast); - return localVue; - }; + Vue.use(GlToast); - const createComponent = ({ - props = {}, - withGlToast = true, - mountFn = shallowMountExtended, - } = {}) => { - const localVue = withGlToast ? vueWithGlToast() : undefined; + const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility); + const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RegistrationToken, { propsData: { value: mockToken, inputId: 'token-value', ...props, }, - localVue, }); showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; @@ -69,13 +58,5 @@ describe('RegistrationToken', () => { expect(showToast).toHaveBeenCalledTimes(1); expect(showToast).toHaveBeenCalledWith('Registration token copied!'); }); - - it('does not fail when toast is not defined', () => { - createComponent({ withGlToast: false }); - findInputCopyToggleVisibility().vm.$emit('copy'); - - // This block also tests for unhandled errors - expect(showToast).toBeNull(); - }); }); }); diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js index 1ff6983fbe7..cc09046c000 100644 --- a/spec/frontend/runner/components/runner_assigned_item_spec.js +++ b/spec/frontend/runner/components/runner_assigned_item_spec.js @@ -1,10 +1,12 @@ -import { GlAvatar } from '@gitlab/ui'; +import { GlAvatar, GlBadge } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue'; import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; const mockHref = '/group/project'; const mockName = 'Project'; +const mockDescription = 'Project description'; const mockFullName = 'Group / Project'; const mockAvatarUrl = '/avatar.png'; @@ -12,6 +14,7 @@ describe('RunnerAssignedItem', () => { let wrapper; const findAvatar = () => wrapper.findByTestId('item-avatar'); + const findBadge = () => wrapper.findComponent(GlBadge); const createComponent = ({ props = {} } = {}) => { wrapper = shallowMountExtended(RunnerAssignedItem, { @@ -20,6 +23,7 @@ describe('RunnerAssignedItem', () => { name: mockName, fullName: mockFullName, avatarUrl: mockAvatarUrl, + description: mockDescription, ...props, }, }); @@ -51,4 +55,14 @@ describe('RunnerAssignedItem', () => { expect(groupFullName.attributes('href')).toBe(mockHref); }); + + it('Shows description', () => { + expect(wrapper.text()).toContain(mockDescription); + }); + + it('Shows owner badge', () => { + createComponent({ props: { isOwner: true } }); + + expect(findBadge().text()).toBe(s__('Runner|Owner')); + }); }); diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js new file mode 100644 index 00000000000..0ac89e82314 --- /dev/null +++ b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js @@ -0,0 +1,101 @@ +import Vue from 'vue'; +import { GlFormCheckbox } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createLocalState } from '~/runner/graphql/list/local_state'; +import { allRunnersData } from '../mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/flash'); + +describe('RunnerBulkDeleteCheckbox', () => { + let wrapper; + let mockState; + let mockCheckedRunnerIds; + + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); + + const mockRunners = allRunnersData.data.runners.nodes; + const mockIds = allRunnersData.data.runners.nodes.map(({ id }) => id); + const mockId = mockIds[0]; + const mockIdAnotherPage = 'RUNNER_IN_ANOTHER_PAGE_ID'; + + const createComponent = ({ props = {} } = {}) => { + const { cacheConfig, localMutations } = mockState; + const apolloProvider = createMockApollo(undefined, undefined, cacheConfig); + + wrapper = shallowMountExtended(RunnerBulkDeleteCheckbox, { + apolloProvider, + provide: { + localMutations, + }, + propsData: { + runners: mockRunners, + ...props, + }, + }); + }; + + beforeEach(() => { + mockState = createLocalState(); + + jest + .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds') + .mockImplementation(() => mockCheckedRunnerIds); + + jest.spyOn(mockState.localMutations, 'setRunnersChecked'); + }); + + describe.each` + case | is | checkedRunnerIds | disabled | checked | indeterminate + ${'no runners'} | ${'unchecked'} | ${[]} | ${undefined} | ${undefined} | ${undefined} + ${'no runners in this page'} | ${'unchecked'} | ${[mockIdAnotherPage]} | ${undefined} | ${undefined} | ${undefined} + ${'all runners'} | ${'checked'} | ${mockIds} | ${undefined} | ${'true'} | ${undefined} + ${'some runners'} | ${'indeterminate'} | ${[mockId]} | ${undefined} | ${undefined} | ${'true'} + ${'all plus other runners'} | ${'checked'} | ${[...mockIds, mockIdAnotherPage]} | ${undefined} | ${'true'} | ${undefined} + `('When $case are checked', ({ is, checkedRunnerIds, disabled, checked, indeterminate }) => { + beforeEach(async () => { + mockCheckedRunnerIds = checkedRunnerIds; + + createComponent(); + }); + + it(`is ${is}`, () => { + expect(findCheckbox().attributes('disabled')).toBe(disabled); + expect(findCheckbox().attributes('checked')).toBe(checked); + expect(findCheckbox().attributes('indeterminate')).toBe(indeterminate); + }); + }); + + describe('When user selects', () => { + beforeEach(() => { + mockCheckedRunnerIds = mockIds; + createComponent(); + }); + + it.each([[true], [false]])('sets checked to %s', (checked) => { + findCheckbox().vm.$emit('change', checked); + + expect(mockState.localMutations.setRunnersChecked).toHaveBeenCalledTimes(1); + expect(mockState.localMutations.setRunnersChecked).toHaveBeenCalledWith({ + isChecked: checked, + runners: mockRunners, + }); + }); + }); + + describe('When runners are loading', () => { + beforeEach(() => { + createComponent({ props: { runners: [] } }); + }); + + it(`is disabled`, () => { + expect(findCheckbox().attributes('disabled')).toBe('true'); + expect(findCheckbox().attributes('checked')).toBe(undefined); + expect(findCheckbox().attributes('indeterminate')).toBe(undefined); + }); + }); +}); diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js index f5b56396cf1..6df918c684f 100644 --- a/spec/frontend/runner/components/runner_bulk_delete_spec.js +++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js @@ -1,37 +1,65 @@ import Vue from 'vue'; -import { GlSprintf } from '@gitlab/ui'; +import { GlModal, GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; +import { createAlert } from '~/flash'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import { s__ } from '~/locale'; import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; +import BulkRunnerDeleteMutation from '~/runner/graphql/list/bulk_runner_delete.mutation.graphql'; import { createLocalState } from '~/runner/graphql/list/local_state'; import waitForPromises from 'helpers/wait_for_promises'; +import { allRunnersData } from '../mock_data'; Vue.use(VueApollo); -jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); +jest.mock('~/flash'); describe('RunnerBulkDelete', () => { let wrapper; + let apolloCache; let mockState; let mockCheckedRunnerIds; - const findClearBtn = () => wrapper.findByTestId('clear-btn'); - const findDeleteBtn = () => wrapper.findByTestId('delete-btn'); + const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection')); + const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected')); + const findModal = () => wrapper.findComponent(GlModal); + + const mockRunners = allRunnersData.data.runners.nodes; + const mockId1 = allRunnersData.data.runners.nodes[0].id; + const mockId2 = allRunnersData.data.runners.nodes[1].id; + + const bulkRunnerDeleteHandler = jest.fn(); const createComponent = () => { const { cacheConfig, localMutations } = mockState; + const apolloProvider = createMockApollo( + [[BulkRunnerDeleteMutation, bulkRunnerDeleteHandler]], + undefined, + cacheConfig, + ); wrapper = shallowMountExtended(RunnerBulkDelete, { - apolloProvider: createMockApollo(undefined, undefined, cacheConfig), + apolloProvider, provide: { localMutations, }, + propsData: { + runners: mockRunners, + }, + directives: { + GlTooltip: createMockDirective(), + }, stubs: { GlSprintf, + GlModal, }, }); + + apolloCache = apolloProvider.defaultClient.cache; + jest.spyOn(apolloCache, 'evict'); + jest.spyOn(apolloCache, 'gc'); }; beforeEach(() => { @@ -43,6 +71,7 @@ describe('RunnerBulkDelete', () => { }); afterEach(() => { + bulkRunnerDeleteHandler.mockReset(); wrapper.destroy(); }); @@ -61,10 +90,10 @@ describe('RunnerBulkDelete', () => { }); describe.each` - count | ids | text - ${1} | ${['gid:Runner/1']} | ${'1 runner'} - ${2} | ${['gid:Runner/1', 'gid:Runner/2']} | ${'2 runners'} - `('When $count runner(s) are checked', ({ count, ids, text }) => { + count | ids | text + ${1} | ${[mockId1]} | ${'1 runner'} + ${2} | ${[mockId1, mockId2]} | ${'2 runners'} + `('When $count runner(s) are checked', ({ ids, text }) => { beforeEach(() => { mockCheckedRunnerIds = ids; @@ -86,18 +115,129 @@ describe('RunnerBulkDelete', () => { }); it('shows confirmation modal', () => { - expect(confirmAction).toHaveBeenCalledTimes(0); + const modalId = getBinding(findDeleteBtn().element, 'gl-modal'); + + expect(findModal().props('modal-id')).toBe(modalId); + expect(findModal().text()).toContain(text); + }); + }); + + describe('when runners are deleted', () => { + let evt; + let mockHideModal; + + beforeEach(() => { + mockCheckedRunnerIds = [mockId1, mockId2]; + + createComponent(); + + jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {}); + mockHideModal = jest.spyOn(findModal().vm, 'hide'); + }); + + describe('when deletion is successful', () => { + beforeEach(() => { + bulkRunnerDeleteHandler.mockResolvedValue({ + data: { + bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] }, + }, + }); + + evt = { + preventDefault: jest.fn(), + }; + findModal().vm.$emit('primary', evt); + }); + + it('has loading state', async () => { + expect(findModal().props('actionPrimary').attributes.loading).toBe(true); + expect(findModal().props('actionCancel').attributes.loading).toBe(true); + + await waitForPromises(); + + expect(findModal().props('actionPrimary').attributes.loading).toBe(false); + expect(findModal().props('actionCancel').attributes.loading).toBe(false); + }); + + it('modal is not prevented from closing', () => { + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('mutation is called', async () => { + expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({ + input: { ids: mockCheckedRunnerIds }, + }); + }); + + it('user interface is updated', async () => { + const { evict, gc } = apolloCache; + + expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length); + expect(evict).toHaveBeenCalledWith({ + id: expect.stringContaining(mockCheckedRunnerIds[0]), + }); + expect(evict).toHaveBeenCalledWith({ + id: expect.stringContaining(mockCheckedRunnerIds[1]), + }); + + expect(gc).toHaveBeenCalledTimes(1); + }); + + it('modal is hidden', () => { + expect(mockHideModal).toHaveBeenCalledTimes(1); + }); + }); + + describe('when deletion fails', () => { + beforeEach(() => { + bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!')); + + evt = { + preventDefault: jest.fn(), + }; + findModal().vm.$emit('primary', evt); + }); + + it('has loading state', async () => { + expect(findModal().props('actionPrimary').attributes.loading).toBe(true); + expect(findModal().props('actionCancel').attributes.loading).toBe(true); + + await waitForPromises(); + + expect(findModal().props('actionPrimary').attributes.loading).toBe(false); + expect(findModal().props('actionCancel').attributes.loading).toBe(false); + }); + + it('modal is not prevented from closing', () => { + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('mutation is called', () => { + expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({ + input: { ids: mockCheckedRunnerIds }, + }); + }); + + it('user interface is not updated', async () => { + await waitForPromises(); - findDeleteBtn().vm.$emit('click'); + const { evict, gc } = apolloCache; - expect(confirmAction).toHaveBeenCalledTimes(1); + expect(evict).not.toHaveBeenCalled(); + expect(gc).not.toHaveBeenCalled(); + expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled(); + }); - const [, confirmOptions] = confirmAction.mock.calls[0]; - const { title, modalHtmlMessage, primaryBtnText } = confirmOptions; + it('alert is called', async () => { + await waitForPromises(); - expect(title).toMatch(text); - expect(primaryBtnText).toMatch(text); - expect(modalHtmlMessage).toMatch(`<strong>${count}</strong>`); + expect(createAlert).toHaveBeenCalled(); + expect(createAlert).toHaveBeenCalledWith({ + message: expect.any(String), + captureError: true, + error: expect.any(Error), + }); + }); }); }); }); diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js index 83fb1764c6d..e35bec3aa38 100644 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js @@ -143,7 +143,7 @@ describe('RunnerList', () => { runnerType: INSTANCE_TYPE, filters: mockFilters, sort: mockOtherSort, - pagination: { page: 1 }, + pagination: {}, }); }); }); @@ -156,7 +156,7 @@ describe('RunnerList', () => { runnerType: null, filters: mockFilters, sort: mockDefaultSort, - pagination: { page: 1 }, + pagination: {}, }); }); @@ -167,7 +167,7 @@ describe('RunnerList', () => { runnerType: null, filters: [], sort: mockOtherSort, - pagination: { page: 1 }, + pagination: {}, }); }); }); diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js index 20582aaaf40..4d38afb25ee 100644 --- a/spec/frontend/runner/components/runner_jobs_spec.js +++ b/spec/frontend/runner/components/runner_jobs_spec.js @@ -73,8 +73,7 @@ describe('RunnerJobs', () => { it('Shows jobs', () => { const jobs = findRunnerJobsTable().props('jobs'); - expect(jobs).toHaveLength(mockJobs.length); - expect(jobs[0]).toMatchObject(mockJobs[0]); + expect(jobs).toEqual(mockJobs); }); describe('When "Next" page is clicked', () => { diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index eca4bbc3490..7b58a81bb0d 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -88,9 +88,7 @@ describe('RunnerList', () => { createComponent({}, mountExtended); // Badges - expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( - 'never contacted paused', - ); + expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('never contacted'); // Runner summary expect(findCell({ fieldKey: 'summary' }).text()).toContain( @@ -124,10 +122,10 @@ describe('RunnerList', () => { expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true); }); - it('Emits a checked event', () => { + it('Emits a checked event', async () => { const checkbox = findCell({ fieldKey: 'checkbox' }).find('input'); - checkbox.setChecked(); + await checkbox.setChecked(); expect(wrapper.emitted('checked')).toHaveLength(1); expect(wrapper.emitted('checked')[0][0]).toEqual({ diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/runner/components/runner_pagination_spec.js index e144b52ceb3..499cc59250d 100644 --- a/spec/frontend/runner/components/runner_pagination_spec.js +++ b/spec/frontend/runner/components/runner_pagination_spec.js @@ -1,5 +1,5 @@ -import { GlPagination } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlKeysetPagination } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; const mockStartCursor = 'START_CURSOR'; @@ -8,21 +8,11 @@ const mockEndCursor = 'END_CURSOR'; describe('RunnerPagination', () => { let wrapper; - const findPagination = () => wrapper.findComponent(GlPagination); + const findPagination = () => wrapper.findComponent(GlKeysetPagination); - const createComponent = ({ page = 1, hasPreviousPage = false, hasNextPage = true } = {}) => { - wrapper = mount(RunnerPagination, { - propsData: { - value: { - page, - }, - pageInfo: { - hasPreviousPage, - hasNextPage, - startCursor: mockStartCursor, - endCursor: mockEndCursor, - }, - }, + const createComponent = (propsData = {}) => { + wrapper = shallowMount(RunnerPagination, { + propsData, }); }; @@ -30,114 +20,96 @@ describe('RunnerPagination', () => { wrapper.destroy(); }); - describe('When on the first page', () => { - beforeEach(() => { - createComponent({ - page: 1, - hasPreviousPage: false, - hasNextPage: true, - }); - }); - - it('Contains the current page information', () => { - expect(findPagination().props('value')).toBe(1); - expect(findPagination().props('prevPage')).toBe(null); - expect(findPagination().props('nextPage')).toBe(2); - }); - - it('Goes to the second page', () => { - findPagination().vm.$emit('input', 2); - - expect(wrapper.emitted('input')[0]).toEqual([ - { - after: mockEndCursor, - page: 2, - }, - ]); - }); - }); - describe('When in between pages', () => { + const mockPageInfo = { + startCursor: mockStartCursor, + endCursor: mockEndCursor, + hasPreviousPage: true, + hasNextPage: true, + }; + beforeEach(() => { createComponent({ - page: 2, - hasPreviousPage: true, - hasNextPage: true, + pageInfo: mockPageInfo, }); }); it('Contains the current page information', () => { - expect(findPagination().props('value')).toBe(2); - expect(findPagination().props('prevPage')).toBe(1); - expect(findPagination().props('nextPage')).toBe(3); + expect(findPagination().props()).toMatchObject(mockPageInfo); }); - it('Shows the next and previous pages', () => { - const links = findPagination().findAll('a'); - - expect(links).toHaveLength(2); - expect(links.at(0).text()).toBe('Previous'); - expect(links.at(1).text()).toBe('Next'); - }); - - it('Goes to the last page', () => { - findPagination().vm.$emit('input', 3); + it('Goes to the prev page', () => { + findPagination().vm.$emit('prev'); expect(wrapper.emitted('input')[0]).toEqual([ { - after: mockEndCursor, - page: 3, + before: mockStartCursor, }, ]); }); - it('Goes to the first page', () => { - findPagination().vm.$emit('input', 1); + it('Goes to the next page', () => { + findPagination().vm.$emit('next'); expect(wrapper.emitted('input')[0]).toEqual([ { - page: 1, + after: mockEndCursor, }, ]); }); }); - describe('When in the last page', () => { + describe.each` + page | hasPreviousPage | hasNextPage + ${'first'} | ${false} | ${true} + ${'last'} | ${true} | ${false} + `('When on the $page page', ({ page, hasPreviousPage, hasNextPage }) => { + const mockPageInfo = { + startCursor: mockStartCursor, + endCursor: mockEndCursor, + hasPreviousPage, + hasNextPage, + }; + beforeEach(() => { createComponent({ - page: 3, - hasPreviousPage: true, - hasNextPage: false, + pageInfo: mockPageInfo, }); }); - it('Contains the current page', () => { - expect(findPagination().props('value')).toBe(3); - expect(findPagination().props('prevPage')).toBe(2); - expect(findPagination().props('nextPage')).toBe(null); + it(`Contains the ${page} page information`, () => { + expect(findPagination().props()).toMatchObject(mockPageInfo); }); }); - describe('When only one page', () => { + describe('When no other pages', () => { beforeEach(() => { createComponent({ - page: 1, - hasPreviousPage: false, - hasNextPage: false, + pageInfo: { + hasPreviousPage: false, + hasNextPage: false, + }, }); }); - it('does not display pagination', () => { - expect(wrapper.html()).toBe(''); + it('is not shown', () => { + expect(findPagination().exists()).toBe(false); }); + }); - it('Contains the current page', () => { - expect(findPagination().props('value')).toBe(1); + describe('When adding more attributes', () => { + beforeEach(() => { + createComponent({ + pageInfo: { + hasPreviousPage: true, + hasNextPage: false, + }, + disabled: true, + }); }); - it('Shows no more page buttons', () => { - expect(findPagination().props('prevPage')).toBe(null); - expect(findPagination().props('nextPage')).toBe(null); + it('attributes are passed', () => { + expect(findPagination().props('disabled')).toBe(true); }); }); }); diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js index 6932b3b5197..c988fb8477d 100644 --- a/spec/frontend/runner/components/runner_projects_spec.js +++ b/spec/frontend/runner/components/runner_projects_spec.js @@ -95,6 +95,7 @@ describe('RunnerProjects', () => { name, fullName: nameWithNamespace, avatarUrl, + isOwner: true, // first project is always owner }); }); diff --git a/spec/frontend/runner/components/stat/runner_count_spec.js b/spec/frontend/runner/components/stat/runner_count_spec.js index 89b51b1b4a7..2a6a745099f 100644 --- a/spec/frontend/runner/components/stat/runner_count_spec.js +++ b/spec/frontend/runner/components/stat/runner_count_spec.js @@ -7,8 +7,8 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { captureException } from '~/runner/sentry_utils'; -import allRunnersCountQuery from '~/runner/graphql/list/all_runners_count.query.graphql'; -import groupRunnersCountQuery from '~/runner/graphql/list/group_runners_count.query.graphql'; +import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql'; +import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql'; import { runnersCountData, groupRunnersCountData } from '../../mock_data'; diff --git a/spec/frontend/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/runner/components/stat/runner_single_stat_spec.js new file mode 100644 index 00000000000..964a6a6ff71 --- /dev/null +++ b/spec/frontend/runner/components/stat/runner_single_stat_spec.js @@ -0,0 +1,61 @@ +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { shallowMount } from '@vue/test-utils'; +import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue'; +import RunnerCount from '~/runner/components/stat/runner_count.vue'; +import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants'; + +describe('RunnerStats', () => { + let wrapper; + + const findRunnerCount = () => wrapper.findComponent(RunnerCount); + const findGlSingleStat = () => wrapper.findComponent(GlSingleStat); + + const createComponent = ({ props = {}, count, mountFn = shallowMount, ...options } = {}) => { + wrapper = mountFn(RunnerSingleStat, { + propsData: { + scope: INSTANCE_TYPE, + title: 'My title', + variables: {}, + ...props, + }, + stubs: { + RunnerCount: { + props: ['scope', 'variables', 'skip'], + render() { + return this.$scopedSlots.default({ + count, + }); + }, + }, + }, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + case | count | value + ${'number'} | ${99} | ${'99'} + ${'long number'} | ${1000} | ${'1,000'} + ${'empty number'} | ${null} | ${'-'} + `('formats $case', ({ count, value }) => { + createComponent({ count }); + + expect(findGlSingleStat().props('value')).toBe(value); + }); + + it('Passes runner count props', () => { + const props = { + scope: GROUP_TYPE, + variables: { paused: true }, + skip: true, + }; + + createComponent({ props }); + + expect(findRunnerCount().props()).toEqual(props); + }); +}); diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js index f1ba6403dfb..7f1f22be94f 100644 --- a/spec/frontend/runner/components/stat/runner_stats_spec.js +++ b/spec/frontend/runner/components/stat/runner_stats_spec.js @@ -1,15 +1,13 @@ import { shallowMount, mount } from '@vue/test-utils'; import { s__ } from '~/locale'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; -import RunnerCount from '~/runner/components/stat/runner_count.vue'; -import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; +import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue'; import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; describe('RunnerStats', () => { let wrapper; - const findRunnerCountAt = (i) => wrapper.findAllComponents(RunnerCount).at(i); - const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i); + const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat).wrappers; const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => { wrapper = mountFn(RunnerStats, { @@ -53,31 +51,12 @@ describe('RunnerStats', () => { expect(text).toMatch(`${s__('Runners|Stale runners')} 1`); }); - it('Displays counts for filtered searches', () => { - createComponent({ props: { variables: { paused: true } } }); + it('Displays all counts for filtered searches', () => { + const mockVariables = { paused: true }; + createComponent({ props: { variables: mockVariables } }); - expect(findRunnerCountAt(0).props('variables').paused).toBe(true); - expect(findRunnerCountAt(1).props('variables').paused).toBe(true); - expect(findRunnerCountAt(2).props('variables').paused).toBe(true); - }); - - it('Skips overlapping statuses', () => { - createComponent({ props: { variables: { status: STATUS_ONLINE } } }); - - expect(findRunnerCountAt(0).props('skip')).toBe(false); - expect(findRunnerCountAt(1).props('skip')).toBe(true); - expect(findRunnerCountAt(2).props('skip')).toBe(true); - }); - - it.each` - i | status - ${0} | ${STATUS_ONLINE} - ${1} | ${STATUS_OFFLINE} - ${2} | ${STATUS_STALE} - `('Displays status $status at index $i', ({ i, status }) => { - createComponent({ mountFn: mount }); - - expect(findRunnerCountAt(i).props('variables').status).toBe(status); - expect(findRunnerStatusStatAt(i).props('status')).toBe(status); + findSingleStats().forEach((stat) => { + expect(stat.props('variables')).toMatchObject(mockVariables); + }); }); }); diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js deleted file mode 100644 index 3218272eac7..00000000000 --- a/spec/frontend/runner/components/stat/runner_status_stat_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import { GlSingleStat } from '@gitlab/ui/dist/charts'; -import { shallowMount, mount } from '@vue/test-utils'; -import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; -import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; - -describe('RunnerStatusStat', () => { - let wrapper; - - const findSingleStat = () => wrapper.findComponent(GlSingleStat); - - const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { - wrapper = mountFn(RunnerStatusStat, { - propsData: { - status: STATUS_ONLINE, - value: 99, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe.each` - status | variant | title | badge - ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'} - ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'} - ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'} - `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => { - beforeEach(() => { - createComponent({ props: { status } }, mount); - }); - - it('Renders text', () => { - expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`)); - }); - - it(`Uses variant ${variant}`, () => { - expect(findSingleStat().props('variant')).toBe(variant); - }); - }); - - it('Formats stat number', () => { - createComponent({ props: { value: 1000 } }, mount); - - expect(wrapper.text()).toMatch('Online runners 1,000'); - }); - - it('Shows a null result', () => { - createComponent({ props: { value: null } }, mount); - - expect(wrapper.text()).toMatch('Online runners -'); - }); - - it('Shows an undefined result', () => { - createComponent({ props: { value: undefined } }, mount); - - expect(wrapper.text()).toMatch('Online runners -'); - }); - - it('Shows result for an unknown status', () => { - createComponent({ props: { status: 'UNKNOWN' } }, mount); - - expect(wrapper.text()).toMatch('Runners 99'); - }); -}); diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/runner/graphql/local_state_spec.js index 5c4302e4aa2..ae874fef00d 100644 --- a/spec/frontend/runner/graphql/local_state_spec.js +++ b/spec/frontend/runner/graphql/local_state_spec.js @@ -1,6 +1,8 @@ +import { gql } from '@apollo/client/core'; import createApolloClient from '~/lib/graphql'; import { createLocalState } from '~/runner/graphql/list/local_state'; import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql'; +import { RUNNER_TYPENAME } from '~/runner/constants'; describe('~/runner/graphql/list/local_state', () => { let localState; @@ -18,6 +20,21 @@ describe('~/runner/graphql/list/local_state', () => { apolloClient = createApolloClient({}, { cacheConfig, typeDefs }); }; + const addMockRunnerToCache = (id) => { + // mock some runners in the cache to prevent dangling references + apolloClient.writeFragment({ + id: `${RUNNER_TYPENAME}:${id}`, + fragment: gql` + fragment DummyRunner on CiRunner { + __typename + } + `, + data: { + __typename: RUNNER_TYPENAME, + }, + }); + }; + const queryCheckedRunnerIds = () => { const { checkedRunnerIds } = apolloClient.readQuery({ query: getCheckedRunnerIdsQuery, @@ -34,10 +51,25 @@ describe('~/runner/graphql/list/local_state', () => { apolloClient = null; }); - describe('default', () => { - it('has empty checked list', () => { + describe('queryCheckedRunnerIds', () => { + it('has empty checked list by default', () => { expect(queryCheckedRunnerIds()).toEqual([]); }); + + it('returns checked runners that have a reference in the cache', () => { + addMockRunnerToCache('a'); + localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, isChecked: true }); + + expect(queryCheckedRunnerIds()).toEqual(['a']); + }); + + it('return checked runners that are not dangling references', () => { + addMockRunnerToCache('a'); // 'b' is missing from the cache, perhaps because it was deleted + localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, isChecked: true }); + localState.localMutations.setRunnerChecked({ runner: { id: 'b' }, isChecked: true }); + + expect(queryCheckedRunnerIds()).toEqual(['a']); + }); }); describe.each` @@ -48,6 +80,7 @@ describe('~/runner/graphql/list/local_state', () => { `('setRunnerChecked', ({ inputs, expected }) => { beforeEach(() => { inputs.forEach(([id, isChecked]) => { + addMockRunnerToCache(id); localState.localMutations.setRunnerChecked({ runner: { id }, isChecked }); }); }); @@ -56,9 +89,34 @@ describe('~/runner/graphql/list/local_state', () => { }); }); + describe.each` + inputs | expected + ${[[['a', 'b'], true]]} | ${['a', 'b']} + ${[[['a', 'b'], false]]} | ${[]} + ${[[['a', 'b'], true], [['c', 'd'], true]]} | ${['a', 'b', 'c', 'd']} + ${[[['a', 'b'], true], [['a', 'b'], false]]} | ${[]} + ${[[['a', 'b'], true], [['b'], false]]} | ${['a']} + `('setRunnersChecked', ({ inputs, expected }) => { + beforeEach(() => { + inputs.forEach(([ids, isChecked]) => { + ids.forEach(addMockRunnerToCache); + + localState.localMutations.setRunnersChecked({ + runners: ids.map((id) => ({ id })), + isChecked, + }); + }); + }); + + it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => { + expect(queryCheckedRunnerIds()).toEqual(expected); + }); + }); + describe('clearChecked', () => { it('clears all checked items', () => { ['a', 'b', 'c'].forEach((id) => { + addMockRunnerToCache(id); localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true }); }); diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js index 2065874c288..cee1d436942 100644 --- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js +++ b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js @@ -92,10 +92,10 @@ describe('GroupRunnerShowApp', () => { }); it('shows basic runner details', () => { - const expected = `Description Instance runner + const expected = `Description My Runner Last contact Never contacted Version 1.0.0 - IP Address 127.0.0.1 + IP Address None Executor None Architecture None Platform darwin @@ -178,13 +178,10 @@ describe('GroupRunnerShowApp', () => { }); describe('When loading', () => { - beforeEach(() => { + it('does not show runner details', () => { mockRunnerQueryResult(); createComponent(); - }); - - it('does not show runner details', () => { expect(findRunnerDetails().exists()).toBe(false); }); }); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 9c42b0d6865..57d64202219 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -13,13 +13,13 @@ import { createAlert } from '~/flash'; import { s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; +import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerList from '~/runner/components/runner_list.vue'; import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; -import RunnerCount from '~/runner/components/stat/runner_count.vue'; import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -32,15 +32,14 @@ import { GROUP_TYPE, PARAM_KEY_PAUSED, PARAM_KEY_STATUS, - PARAM_KEY_TAG, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE, RUNNER_PAGE_SIZE, I18N_EDIT, } from '~/runner/constants'; -import groupRunnersQuery from '~/runner/graphql/list/group_runners.query.graphql'; -import groupRunnersCountQuery from '~/runner/graphql/list/group_runners_count.query.graphql'; +import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql'; +import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql'; import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue'; import { captureException } from '~/runner/sentry_utils'; import { @@ -49,6 +48,7 @@ import { groupRunnersCountData, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyPageInfo, emptyStateSvgPath, emptyStateFilteredSvgPath, } from '../mock_data'; @@ -82,7 +82,7 @@ describe('GroupRunnersApp', () => { const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState); const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`)); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); - const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page'); + const findRunnerPaginationNext = () => findRunnerPagination().findByText(s__('Pagination|Next')); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { @@ -111,7 +111,7 @@ describe('GroupRunnersApp', () => { return waitForPromises(); }; - beforeEach(async () => { + beforeEach(() => { mockGroupRunnersHandler.mockResolvedValue(groupRunnersData); mockGroupRunnersCountHandler.mockResolvedValue(groupRunnersCountData); }); @@ -197,6 +197,7 @@ describe('GroupRunnersApp', () => { type: PARAM_KEY_STATUS, options: expect.any(Array), }), + upgradeStatusTokenConfig, ]); }); @@ -254,12 +255,7 @@ describe('GroupRunnersApp', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`); - await createComponent({ - stubs: { - RunnerStats, - RunnerCount, - }, - }); + await createComponent({ mountFn: mountExtended }); }); it('sets the filters in the search bar', () => { @@ -267,7 +263,7 @@ describe('GroupRunnersApp', () => { runnerType: INSTANCE_TYPE, filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }], sort: 'CREATED_DESC', - pagination: { page: 1 }, + pagination: {}, }); }); @@ -292,19 +288,11 @@ describe('GroupRunnersApp', () => { describe('when a filter is selected by the user', () => { beforeEach(async () => { - createComponent({ - stubs: { - RunnerStats, - RunnerCount, - }, - }); + await createComponent({ mountFn: mountExtended }); findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, - filters: [ - { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, - { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } }, - ], + filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], sort: CREATED_ASC, }); @@ -314,7 +302,7 @@ describe('GroupRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: expect.stringContaining('?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC'), + url: expect.stringContaining('?status[]=ONLINE&sort=CREATED_ASC'), }); }); @@ -322,7 +310,6 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, - tagList: ['tag1'], sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); @@ -331,7 +318,6 @@ describe('GroupRunnersApp', () => { it('fetches count results for requested status', () => { expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ groupFullPath: mockGroupFullPath, - tagList: ['tag1'], status: STATUS_ONLINE, }); }); @@ -340,6 +326,7 @@ describe('GroupRunnersApp', () => { it('when runners have not loaded, shows a loading state', () => { createComponent(); expect(findRunnerList().props('loading')).toBe(true); + expect(findRunnerPagination().attributes('disabled')).toBe('true'); }); describe('when no runners are found', () => { @@ -348,13 +335,20 @@ describe('GroupRunnersApp', () => { data: { group: { id: '1', - runners: { nodes: [] }, + runners: { + edges: [], + pageInfo: emptyPageInfo, + }, }, }, }); await createComponent(); }); + it('shows no errors', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); + it('shows an empty state', async () => { expect(findRunnerListEmptyState().exists()).toBe(true); }); @@ -379,12 +373,18 @@ describe('GroupRunnersApp', () => { }); describe('Pagination', () => { + const { pageInfo } = groupRunnersDataPaginated.data.group.runners; + beforeEach(async () => { mockGroupRunnersHandler.mockResolvedValue(groupRunnersDataPaginated); await createComponent({ mountFn: mountExtended }); }); + it('passes the page info', () => { + expect(findRunnerPagination().props('pageInfo')).toEqual(pageInfo); + }); + it('navigates to the next page', async () => { await findRunnerPaginationNext().trigger('click'); @@ -392,7 +392,7 @@ describe('GroupRunnersApp', () => { groupFullPath: mockGroupFullPath, sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, - after: groupRunnersDataPaginated.data.group.runners.pageInfo.endCursor, + after: pageInfo.endCursor, }); }); }); diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index e5472ace817..555ec40184f 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -19,6 +19,14 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runne import { RUNNER_PAGE_SIZE } from '~/runner/constants'; +const emptyPageInfo = { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', +}; + // Other mock data // Mock searches and their corresponding urls @@ -26,7 +34,7 @@ export const mockSearchExamples = [ { name: 'a default query', urlQuery: '', - search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }, + search: { runnerType: null, filters: [], pagination: {}, sort: 'CREATED_DESC' }, graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, isDefault: true, }, @@ -36,7 +44,7 @@ export const mockSearchExamples = [ search: { runnerType: null, filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -52,7 +60,7 @@ export const mockSearchExamples = [ value: { data: 'something' }, }, ], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -72,7 +80,7 @@ export const mockSearchExamples = [ value: { data: 'else' }, }, ], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -83,7 +91,7 @@ export const mockSearchExamples = [ search: { runnerType: 'INSTANCE_TYPE', filters: [], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -97,7 +105,7 @@ export const mockSearchExamples = [ { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, { type: 'status', value: { data: 'PAUSED', operator: '=' } }, ], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -108,7 +116,7 @@ export const mockSearchExamples = [ search: { runnerType: 'INSTANCE_TYPE', filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_ASC', }, graphqlVariables: { @@ -124,7 +132,7 @@ export const mockSearchExamples = [ search: { runnerType: null, filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { @@ -142,7 +150,7 @@ export const mockSearchExamples = [ { type: 'tag', value: { data: 'tag-1', operator: '=' } }, { type: 'tag', value: { data: 'tag-2', operator: '=' } }, ], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { @@ -153,22 +161,22 @@ export const mockSearchExamples = [ }, { name: 'the next page', - urlQuery: '?page=2&after=AFTER_CURSOR', + urlQuery: '?after=AFTER_CURSOR', search: { runnerType: null, filters: [], - pagination: { page: 2, after: 'AFTER_CURSOR' }, + pagination: { after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC', }, graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE }, }, { name: 'the previous page', - urlQuery: '?page=2&before=BEFORE_CURSOR', + urlQuery: '?before=BEFORE_CURSOR', search: { runnerType: null, filters: [], - pagination: { page: 2, before: 'BEFORE_CURSOR' }, + pagination: { before: 'BEFORE_CURSOR' }, sort: 'CREATED_DESC', }, graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE }, @@ -176,7 +184,7 @@ export const mockSearchExamples = [ { name: 'the next page filtered by a status, an instance type, tags and a non default sort', urlQuery: - '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR', + '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR', search: { runnerType: 'INSTANCE_TYPE', filters: [ @@ -184,7 +192,7 @@ export const mockSearchExamples = [ { type: 'tag', value: { data: 'tag-1', operator: '=' } }, { type: 'tag', value: { data: 'tag-2', operator: '=' } }, ], - pagination: { page: 2, after: 'AFTER_CURSOR' }, + pagination: { after: 'AFTER_CURSOR' }, sort: 'CREATED_ASC', }, graphqlVariables: { @@ -202,7 +210,7 @@ export const mockSearchExamples = [ search: { runnerType: null, filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -213,7 +221,7 @@ export const mockSearchExamples = [ search: { runnerType: null, filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }], - pagination: { page: 1 }, + pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, @@ -233,6 +241,7 @@ export { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData, + emptyPageInfo, runnerData, runnerWithGroupData, runnerProjectsData, diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js index 6f954143ab1..e1f90482b34 100644 --- a/spec/frontend/runner/runner_search_utils_spec.js +++ b/spec/frontend/runner/runner_search_utils_spec.js @@ -24,11 +24,14 @@ describe('search_params.js', () => { }); it.each` - query | updatedQuery - ${'status[]=ACTIVE'} | ${'paused[]=false'} - ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'} - ${'status[]=ACTIVE'} | ${'paused[]=false'} - ${'status[]=PAUSED'} | ${'paused[]=true'} + query | updatedQuery + ${'status[]=ACTIVE'} | ${'paused[]=false'} + ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'} + ${'status[]=ACTIVE'} | ${'paused[]=false'} + ${'status[]=PAUSED'} | ${'paused[]=true'} + ${'page=2&after=AFTER'} | ${'after=AFTER'} + ${'page=2&before=BEFORE'} | ${'before=BEFORE'} + ${'status[]=PAUSED&page=2&after=AFTER'} | ${'after=AFTER&paused[]=true'} `('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => { const mockUrl = 'http://test.host/admin/runners?'; @@ -49,24 +52,6 @@ describe('search_params.js', () => { { type: 'filtered-search-term', value: { data: 'text' } }, ]); }); - - it('When a page cannot be parsed as a number, it defaults to `1`', () => { - expect(fromUrlQueryToSearch('?page=NONSENSE&after=AFTER_CURSOR').pagination).toEqual({ - page: 1, - }); - }); - - it('When a page is less than 1, it defaults to `1`', () => { - expect(fromUrlQueryToSearch('?page=0&after=AFTER_CURSOR').pagination).toEqual({ - page: 1, - }); - }); - - it('When a page with no cursor is given, it defaults to `1`', () => { - expect(fromUrlQueryToSearch('?page=2').pagination).toEqual({ - page: 1, - }); - }); }); describe('fromSearchToUrl', () => { @@ -143,8 +128,11 @@ describe('search_params.js', () => { }); }); - it('given a missing pagination, evaluates as not filtered', () => { - expect(isSearchFiltered({ pagination: null })).toBe(false); - }); + it.each([null, undefined, {}])( + 'given a missing pagination, evaluates as not filtered', + (mockPagination) => { + expect(isSearchFiltered({ pagination: mockPagination })).toBe(false); + }, + ); }); }); diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 184c16fda6e..b6451af57d7 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -402,7 +402,7 @@ describe('TrainingProviderList component', () => { it('has disabled state for radio', () => { findPrimaryProviderRadios().wrappers.forEach((radio) => { - expect(radio.attributes('disabled')).toBeTruthy(); + expect(radio.attributes('disabled')).toBe('true'); }); }); diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js index a4474ead956..c2aff456abb 100644 --- a/spec/frontend/sidebar/assignees_spec.js +++ b/spec/frontend/sidebar/assignees_spec.js @@ -70,7 +70,7 @@ describe('Assignee component', () => { wrapper.find('[data-testid="assign-yourself"]').trigger('click'); await nextTick(); - expect(wrapper.emitted('assign-self')).toBeTruthy(); + expect(wrapper.emitted('assign-self')).toHaveLength(1); }); }); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js index 5fd364afbe4..88015ed42a3 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -148,6 +148,7 @@ describe('Sidebar assignees widget', () => { expect(findAssignees().props('users')).toEqual([ { + __typename: 'UserCore', id: 'gid://gitlab/User/2', avatarUrl: 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon', diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js deleted file mode 100644 index 58fa878a189..00000000000 --- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js +++ /dev/null @@ -1,121 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue'; - -let wrapper; - -function factory(propsData = {}) { - wrapper = mount(AttentionRequestedToggle, { propsData }); -} - -const findToggle = () => wrapper.findComponent(GlButton); - -describe('Attention require toggle', () => { - afterEach(() => { - wrapper.destroy(); - }); - - it('renders button', () => { - factory({ - type: 'reviewer', - user: { attention_requested: false, can_update_merge_request: true }, - }); - - expect(findToggle().exists()).toBe(true); - }); - - it.each` - attentionRequested | icon - ${true} | ${'attention-solid'} - ${false} | ${'attention'} - `( - 'renders $icon icon when attention_requested is $attentionRequested', - ({ attentionRequested, icon }) => { - factory({ - type: 'reviewer', - user: { attention_requested: attentionRequested, can_update_merge_request: true }, - }); - - expect(findToggle().props('icon')).toBe(icon); - }, - ); - - it.each` - attentionRequested | selected - ${true} | ${true} - ${false} | ${false} - `( - 'renders button with as selected when $selected when attention_requested is $attentionRequested', - ({ attentionRequested, selected }) => { - factory({ - type: 'reviewer', - user: { attention_requested: attentionRequested, can_update_merge_request: true }, - }); - - expect(findToggle().props('selected')).toBe(selected); - }, - ); - - it('emits toggle-attention-requested on click', async () => { - factory({ - type: 'reviewer', - user: { attention_requested: true, can_update_merge_request: true }, - }); - - await findToggle().trigger('click'); - - expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([ - { - user: { attention_requested: true, can_update_merge_request: true }, - callback: expect.anything(), - direction: 'remove', - }, - ]); - }); - - it('does not emit toggle-attention-requested on click if can_update_merge_request is false', async () => { - factory({ - type: 'reviewer', - user: { attention_requested: true, can_update_merge_request: false }, - }); - - await findToggle().trigger('click'); - - expect(wrapper.emitted('toggle-attention-requested')).toBe(undefined); - }); - - it('sets loading on click', async () => { - factory({ - type: 'reviewer', - user: { attention_requested: true, can_update_merge_request: true }, - }); - - await findToggle().trigger('click'); - - expect(findToggle().props('loading')).toBe(true); - }); - - it.each` - type | attentionRequested | tooltip | canUpdateMergeRequest - ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequest} | ${true} - ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true} - ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true} - ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false} - ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false} - ${'assignee'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false} - ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false} - `( - 'sets tooltip as $tooltip when attention_requested is $attentionRequested, type is $type and, can_update_merge_request is $canUpdateMergeRequest', - ({ type, attentionRequested, tooltip, canUpdateMergeRequest }) => { - factory({ - type, - user: { - attention_requested: attentionRequested, - can_update_merge_request: canUpdateMergeRequest, - }, - }); - - expect(findToggle().attributes('aria-label')).toBe(tooltip); - }, - ); -}); diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js index 1ea035c7184..7775ed6aa37 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js @@ -71,7 +71,12 @@ describe('Sidebar Confidentiality Form', () => { it('creates a flash if mutation contains errors', async () => { createComponent({ mutate: jest.fn().mockResolvedValue({ - data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } }, + data: { + issuableSetConfidential: { + issuable: { confidential: false }, + errors: ['Houston, we have a problem!'], + }, + }, }), }); findConfidentialToggle().vm.$emit('click', new MouseEvent('click')); @@ -82,6 +87,24 @@ describe('Sidebar Confidentiality Form', () => { }); }); + it('emits `closeForm` event with confidentiality value when mutation is successful', async () => { + createComponent({ + mutate: jest.fn().mockResolvedValue({ + data: { + issuableSetConfidential: { + issuable: { confidential: true }, + errors: [], + }, + }, + }), + }); + + findConfidentialToggle().vm.$emit('click', new MouseEvent('click')); + await waitForPromises(); + + expect(wrapper.emitted('closeForm')).toEqual([[{ confidential: true }]]); + }); + describe('when issue is not confidential', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js index 1de71e52264..18ee423d12e 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js @@ -132,6 +132,7 @@ describe('Sidebar Confidentiality Widget', () => { it('closes the form and dispatches an event when `closeForm` is emitted', async () => { createComponent(); const el = wrapper.vm.$el; + const closeFormPayload = { confidential: true }; jest.spyOn(el, 'dispatchEvent'); await waitForPromises(); @@ -140,12 +141,12 @@ describe('Sidebar Confidentiality Widget', () => { expect(findConfidentialityForm().isVisible()).toBe(true); - findConfidentialityForm().vm.$emit('closeForm'); + findConfidentialityForm().vm.$emit('closeForm', closeFormPayload); await nextTick(); expect(findConfidentialityForm().isVisible()).toBe(false); expect(el.dispatchEvent).toHaveBeenCalled(); - expect(wrapper.emitted('closeForm')).toHaveLength(1); + expect(wrapper.emitted('closeForm')).toEqual([[closeFormPayload]]); }); it('emits `expandSidebar` event when it is emitted from child component', async () => { diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js index 8d8c10d10f1..83764cb6739 100644 --- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js +++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js @@ -1,4 +1,5 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue'; @@ -61,6 +62,8 @@ describe('EscalationStatus', () => { createComponent(); // Open dropdown await toggleDropdown(); + jest.runOnlyPendingTimers(); + await nextTick(); expect(findDropdownMenu().classes('show')).toBe(true); @@ -74,6 +77,8 @@ describe('EscalationStatus', () => { createComponent({ preventDropdownClose: true }); // Open dropdown await toggleDropdown(); + jest.runOnlyPendingTimers(); + await nextTick(); expect(findDropdownMenu().classes('show')).toBe(true); diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js index 338ecf944f3..859e63b3df6 100644 --- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js +++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js @@ -1,7 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { stripTypenames } from 'helpers/graphql_helpers'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import Participants from '~/sidebar/components/participants/participants.vue'; @@ -67,11 +66,9 @@ describe('Sidebar Participants Widget', () => { }); it('passes participants to child component', () => { - const participantsWithoutTypename = stripTypenames( + expect(findParticipants().props('participants')).toEqual( epicParticipantsResponse().data.workspace.issuable.participants.nodes, ); - - expect(findParticipants().props('participants')).toEqual(participantsWithoutTypename); }); }); diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js index 8999f120a0f..2c24df2436a 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -1,9 +1,22 @@ import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; -import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue'; import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue'; import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue'; -import userDataMock from '../../user_data_mock'; + +const userDataMock = () => ({ + id: 1, + name: 'Root', + state: 'active', + username: 'root', + webUrl: `${TEST_HOST}/root`, + avatarUrl: `${TEST_HOST}/avatar/root.png`, + mergeRequestInteraction: { + canMerge: true, + canUpdate: true, + reviewed: true, + approved: false, + }, +}); describe('UncollapsedReviewerList component', () => { let wrapper; @@ -70,7 +83,10 @@ describe('UncollapsedReviewerList component', () => { id: 2, name: 'nonrooty-nonrootersen', username: 'hello-world', - approved: true, + mergeRequestInteraction: { + ...user.mergeRequestInteraction, + approved: true, + }, }; beforeEach(() => { @@ -119,18 +135,4 @@ describe('UncollapsedReviewerList component', () => { expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true); }); }); - - it('hides re-request review button when attentionRequired feature flag is enabled', () => { - createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true }); - - expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0); - }); - - it('emits toggle-attention-requested', () => { - createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true }); - - wrapper.find(AttentionRequestedToggle).vm.$emit('toggle-attention-requested', 'data'); - - expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual(['data']); - }); }); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 229757ff40c..9c6e23e928c 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -343,6 +343,14 @@ export const issuableQueryResponse = { __typename: 'Issue', id: 'gid://gitlab/Issue/1', iid: '1', + author: { + id: '1', + avatarUrl: '/avatar', + name: 'root', + username: 'root', + webUrl: 'root', + status: null, + }, assignees: { nodes: [ { @@ -450,7 +458,7 @@ export const subscriptionResponse = { }, }; -const mockUser1 = { +export const mockUser1 = { __typename: 'UserCore', id: 'gid://gitlab/User/1', avatarUrl: @@ -459,6 +467,7 @@ const mockUser1 = { username: 'root', webUrl: '/root', status: null, + canMerge: false, }; export const mockUser2 = { @@ -469,6 +478,7 @@ export const mockUser2 = { username: 'rookie', webUrl: 'rookie', status: null, + canMerge: false, }; export const searchResponse = { diff --git a/spec/frontend/sidebar/reviewer_title_spec.js b/spec/frontend/sidebar/reviewer_title_spec.js index 3c250be5d5e..6b4eed5ad0f 100644 --- a/spec/frontend/sidebar/reviewer_title_spec.js +++ b/spec/frontend/sidebar/reviewer_title_spec.js @@ -47,7 +47,7 @@ describe('ReviewerTitle component', () => { editable: false, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); }); it('renders spinner when loading', () => { @@ -57,7 +57,7 @@ describe('ReviewerTitle component', () => { editable: false, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); it('does not render edit link when not editable', () => { diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/reviewers_spec.js index 351dfc9a6ed..88bacc9b7f7 100644 --- a/spec/frontend/sidebar/reviewers_spec.js +++ b/spec/frontend/sidebar/reviewers_spec.js @@ -1,9 +1,23 @@ import { GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { trimText } from 'helpers/text_helper'; -import UsersMockHelper from 'helpers/user_mock_data_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import Reviewer from '~/sidebar/components/reviewers/reviewers.vue'; -import UsersMock from './mock_data'; + +const usersMock = (id = 1) => ({ + id, + name: 'Root', + state: 'active', + username: 'root', + webUrl: `${TEST_HOST}/root`, + avatarUrl: `${TEST_HOST}/avatar/root.png`, + mergeRequestInteraction: { + canMerge: true, + canUpdate: true, + reviewed: true, + approved: false, + }, +}); describe('Reviewer component', () => { const getDefaultProps = () => ({ @@ -42,23 +56,23 @@ describe('Reviewer component', () => { it('displays one reviewer icon when collapsed', () => { createWrapper({ ...getDefaultProps(), - users: [UsersMock.user], + users: [usersMock()], }); const collapsedChildren = findCollapsedChildren(); const reviewer = collapsedChildren.at(0); expect(collapsedChildren.length).toBe(1); - expect(reviewer.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar); - expect(reviewer.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`); + expect(reviewer.find('.avatar').attributes('src')).toContain('avatar/root.png'); + expect(reviewer.find('.avatar').attributes('alt')).toBe(`Root's avatar`); - expect(trimText(reviewer.find('.author').text())).toBe(UsersMock.user.name); + expect(trimText(reviewer.find('.author').text())).toBe('Root'); }); }); describe('Two or more reviewers/users', () => { it('displays two reviewer icons when collapsed', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); + const users = [usersMock(), usersMock(2)]; createWrapper({ ...getDefaultProps(), users, @@ -70,21 +84,21 @@ describe('Reviewer component', () => { const first = collapsedChildren.at(0); - expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url); + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl); expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); expect(trimText(first.find('.author').text())).toBe(users[0].name); const second = collapsedChildren.at(1); - expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url); + expect(second.find('.avatar').attributes('src')).toBe(users[1].avatarUrl); expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`); expect(trimText(second.find('.author').text())).toBe(users[1].name); }); it('displays one reviewer icon and counter when collapsed', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); + const users = [usersMock(), usersMock(2), usersMock(3)]; createWrapper({ ...getDefaultProps(), users, @@ -96,7 +110,7 @@ describe('Reviewer component', () => { const first = collapsedChildren.at(0); - expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url); + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl); expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); expect(trimText(first.find('.author').text())).toBe(users[0].name); @@ -107,7 +121,7 @@ describe('Reviewer component', () => { }); it('Shows two reviewers', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); + const users = [usersMock(), usersMock(2)]; createWrapper({ ...getDefaultProps(), users, @@ -118,10 +132,10 @@ describe('Reviewer component', () => { }); it('shows sorted reviewer where "can merge" users are sorted first', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; + const users = [usersMock(), usersMock(2), usersMock(3)]; + users[0].mergeRequestInteraction.canMerge = false; + users[1].mergeRequestInteraction.canMerge = false; + users[2].mergeRequestInteraction.canMerge = true; createWrapper({ ...getDefaultProps(), @@ -129,14 +143,14 @@ describe('Reviewer component', () => { editable: true, }); - expect(wrapper.vm.sortedReviewers[0].can_merge).toBe(true); + expect(wrapper.vm.sortedReviewers[0].mergeRequestInteraction.canMerge).toBe(true); }); it('passes the sorted reviewers to the uncollapsed-reviewer-list', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; + const users = [usersMock(), usersMock(2), usersMock(3)]; + users[0].mergeRequestInteraction.canMerge = false; + users[1].mergeRequestInteraction.canMerge = false; + users[2].mergeRequestInteraction.canMerge = true; createWrapper({ ...getDefaultProps(), @@ -149,10 +163,10 @@ describe('Reviewer component', () => { }); it('passes the sorted reviewers to the collapsed-reviewer-list', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; + const users = [usersMock(), usersMock(2), usersMock(3)]; + users[0].mergeRequestInteraction.canMerge = false; + users[1].mergeRequestInteraction.canMerge = false; + users[2].mergeRequestInteraction.canMerge = true; createWrapper({ ...getDefaultProps(), diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index 82fb10ab1d2..e32694abcce 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -1,12 +1,9 @@ import MockAdapter from 'axios-mock-adapter'; -import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as urlUtility from '~/lib/utils/url_utility'; import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; -import toast from '~/vue_shared/plugins/global_toast'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import Mock from './mock_data'; jest.mock('~/flash'); @@ -122,93 +119,4 @@ describe('Sidebar mediator', () => { urlSpy.mockRestore(); }); }); - - describe('toggleAttentionRequested', () => { - let requestAttentionMock; - let removeAttentionRequestMock; - - beforeEach(() => { - requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue(); - removeAttentionRequestMock = jest - .spyOn(mediator.service, 'removeAttentionRequest') - .mockResolvedValue(); - }); - - it.each` - attentionIsCurrentlyRequested | serviceMethod - ${true} | ${'remove'} - ${false} | ${'add'} - `( - "calls the $serviceMethod service method when the user's attention request is set to $attentionIsCurrentlyRequested", - async ({ serviceMethod }) => { - const methods = { - add: requestAttentionMock, - remove: removeAttentionRequestMock, - }; - mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }]; - - await mediator.toggleAttentionRequested('reviewer', { - user: { id: 1, username: 'root' }, - callback: jest.fn(), - direction: serviceMethod, - }); - - expect(methods[serviceMethod]).toHaveBeenCalledWith(1); - expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); - }, - ); - - it.each` - type | method - ${'reviewer'} | ${'findReviewer'} - `('finds $type', ({ type, method }) => { - const methodSpy = jest.spyOn(mediator.store, method); - - mediator.toggleAttentionRequested(type, { user: { id: 1 }, callback: jest.fn() }); - - expect(methodSpy).toHaveBeenCalledWith({ id: 1 }); - }); - - it.each` - attentionRequested | toastMessage - ${true} | ${'Removed attention request from @root'} - ${false} | ${'Requested attention from @root'} - `( - 'it creates toast $toastMessage when attention_requested is $attentionRequested', - async ({ attentionRequested, toastMessage }) => { - mediator.store.reviewers = [ - { id: 1, attention_requested: attentionRequested, username: 'root' }, - ]; - - await mediator.toggleAttentionRequested('reviewer', { - user: { id: 1, username: 'root' }, - callback: jest.fn(), - }); - - expect(toast).toHaveBeenCalledWith(toastMessage); - }, - ); - - describe('errors', () => { - beforeEach(() => { - jest - .spyOn(mediator.service, 'removeAttentionRequest') - .mockRejectedValueOnce(new Error('Something went wrong')); - }); - - it('shows an error message', async () => { - await mediator.toggleAttentionRequested('reviewer', { - user: { id: 1, username: 'root' }, - callback: jest.fn(), - direction: 'remove', - }); - - expect(createFlash).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'Updating the attention request for root failed.', - }), - ); - }); - }); - }); }); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap index 6fc358a6a15..76e84fa183c 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap @@ -16,6 +16,7 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = ` <source-editor-stub debouncevalue="250" editoroptions="[object Object]" + extensions="[object Object]" fileglobalid="blob_local_7" filename="foo/bar/test.md" value="Lorem ipsum dolar sit amet, diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js index 6e8cc660b1d..cd549155914 100644 --- a/spec/frontend/surveys/merge_request_performance/app_spec.js +++ b/spec/frontend/surveys/merge_request_performance/app_spec.js @@ -25,6 +25,9 @@ describe('MergeRequestExperienceSurveyApp', () => { shouldShowCallout, }); wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, { + propsData: { + accountAge: 0, + }, stubs: { UserCalloutDismisser: dismisserComponent, GlSprintf, @@ -82,11 +85,17 @@ describe('MergeRequestExperienceSurveyApp', () => { expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { value: 5, label: 'overall', + extra: { + accountAge: 0, + }, }); rate.vm.$emit('rate', 4); expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { value: 4, label: 'performance', + extra: { + accountAge: 0, + }, }); }); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index b4626625f31..3fb226e5ed3 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -1,8 +1,7 @@ /* Setup for unit test environment */ +// eslint-disable-next-line no-restricted-syntax +import { setImmediate } from 'timers'; import 'helpers/shared_test_setup'; -import { initializeTestTimeout } from 'helpers/timeout'; - -initializeTestTimeout(process.env.CI ? 6000 : 500); afterEach(() => // give Promises a bit more time so they fail the right test diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js index b171c8fc9ed..0530569c9df 100644 --- a/spec/frontend/user_popovers_spec.js +++ b/spec/frontend/user_popovers_spec.js @@ -10,6 +10,8 @@ jest.mock('~/api/user_api', () => ({ })); describe('User Popovers', () => { + let origGon; + const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html'; const selector = '.js-user-link[data-user], .js-user-link[data-user-id]'; @@ -39,7 +41,7 @@ describe('User Popovers', () => { el.dispatchEvent(event); }; - beforeEach(() => { + const setupTestSubject = () => { loadHTMLFixture(fixtureTemplate); const usersCacheSpy = () => Promise.resolve(dummyUser); @@ -56,147 +58,179 @@ describe('User Popovers', () => { document.body.appendChild(mountingRoot); popoverInstance.$mount(mountingRoot); }); + }; + + beforeEach(() => { + origGon = window.gon; + window.gon = {}; }); afterEach(() => { - resetHTMLFixture(); + window.gon = origGon; }); - describe('shows a placeholder popover on hover', () => { - let linksWithUsers; + describe('when signed out', () => { beforeEach(() => { - linksWithUsers = findFixtureLinks(); + setupTestSubject(); + }); + + it('does not show a placeholder popover on hover', () => { + const linksWithUsers = findFixtureLinks(); linksWithUsers.forEach((el) => { triggerEvent('mouseover', el); }); + + expect(findPopovers().length).toBe(0); }); + }); - it('for initial links', () => { - expect(findPopovers().length).toBe(linksWithUsers.length); + describe('when signed in', () => { + beforeEach(() => { + window.gon.current_user_id = 7; + + setupTestSubject(); }); - it('for elements added after initial load', async () => { - const addedLinks = [createUserLink(), createUserLink()]; - addedLinks.forEach((link) => { - document.body.appendChild(link); - }); + afterEach(() => { + resetHTMLFixture(); + }); - jest.runOnlyPendingTimers(); + describe('shows a placeholder popover on hover', () => { + let linksWithUsers; + beforeEach(() => { + linksWithUsers = findFixtureLinks(); + linksWithUsers.forEach((el) => { + triggerEvent('mouseover', el); + }); + }); - addedLinks.forEach((link) => { - triggerEvent('mouseover', link); + it('for initial links', () => { + expect(findPopovers().length).toBe(linksWithUsers.length); }); - expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length); + it('for elements added after initial load', async () => { + const addedLinks = [createUserLink(), createUserLink()]; + addedLinks.forEach((link) => { + document.body.appendChild(link); + }); + + jest.runOnlyPendingTimers(); + + addedLinks.forEach((link) => { + triggerEvent('mouseover', link); + }); + + expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length); + }); }); - }); - it('does not initialize the popovers for group references', async () => { - const [groupLink] = Array.from(document.querySelectorAll('.js-user-link[data-group]')); + it('does not initialize the popovers for group references', async () => { + const [groupLink] = Array.from(document.querySelectorAll('.js-user-link[data-group]')); - triggerEvent('mouseover', groupLink); - jest.runOnlyPendingTimers(); + triggerEvent('mouseover', groupLink); + jest.runOnlyPendingTimers(); - expect(findPopovers().length).toBe(0); - }); + expect(findPopovers().length).toBe(0); + }); - it('does not initialize the popovers for @all references', async () => { - const [projectLink] = Array.from(document.querySelectorAll('.js-user-link[data-project]')); + it('does not initialize the popovers for @all references', async () => { + const [projectLink] = Array.from(document.querySelectorAll('.js-user-link[data-project]')); - triggerEvent('mouseover', projectLink); - jest.runOnlyPendingTimers(); + triggerEvent('mouseover', projectLink); + jest.runOnlyPendingTimers(); - expect(findPopovers().length).toBe(0); - }); + expect(findPopovers().length).toBe(0); + }); - it('does not initialize the user popovers twice for the same element', async () => { - const [firstUserLink] = findFixtureLinks(); - triggerEvent('mouseover', firstUserLink); - jest.runOnlyPendingTimers(); - triggerEvent('mouseleave', firstUserLink); - jest.runOnlyPendingTimers(); - triggerEvent('mouseover', firstUserLink); - jest.runOnlyPendingTimers(); + it('does not initialize the user popovers twice for the same element', async () => { + const [firstUserLink] = findFixtureLinks(); + triggerEvent('mouseover', firstUserLink); + jest.runOnlyPendingTimers(); + triggerEvent('mouseleave', firstUserLink); + jest.runOnlyPendingTimers(); + triggerEvent('mouseover', firstUserLink); + jest.runOnlyPendingTimers(); - expect(findPopovers().length).toBe(1); - }); + expect(findPopovers().length).toBe(1); + }); - describe('when user link emits mouseenter event with empty user cache', () => { - let userLink; + describe('when user link emits mouseenter event with empty user cache', () => { + let userLink; - beforeEach(() => { - UsersCache.retrieveById.mockReset(); + beforeEach(() => { + UsersCache.retrieveById.mockReset(); - [userLink] = findFixtureLinks(); + [userLink] = findFixtureLinks(); - triggerEvent('mouseover', userLink); - }); + triggerEvent('mouseover', userLink); + }); - it('populates popover with preloaded user data', () => { - const { name, userId, username } = userLink.dataset; + it('populates popover with preloaded user data', () => { + const { name, userId, username } = userLink.dataset; - expect(userLink.user).toEqual( - expect.objectContaining({ - name, - userId, - username, - }), - ); + expect(userLink.user).toEqual( + expect.objectContaining({ + name, + userId, + username, + }), + ); + }); }); - }); - describe('when user link emits mouseenter event', () => { - let userLink; + describe('when user link emits mouseenter event', () => { + let userLink; - beforeEach(() => { - [userLink] = findFixtureLinks(); + beforeEach(() => { + [userLink] = findFixtureLinks(); - triggerEvent('mouseover', userLink); - }); + triggerEvent('mouseover', userLink); + }); - it('removes title attribute from user links', () => { - expect(userLink.getAttribute('title')).toBeFalsy(); - expect(userLink.dataset.originalTitle).toBeFalsy(); - }); + it('removes title attribute from user links', () => { + expect(userLink.getAttribute('title')).toBeFalsy(); + expect(userLink.dataset.originalTitle).toBeFalsy(); + }); - it('fetches user info and status from the user cache', () => { - const { userId } = userLink.dataset; + it('fetches user info and status from the user cache', () => { + const { userId } = userLink.dataset; - expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId); - expect(UsersCache.retrieveStatusById).toHaveBeenCalledWith(userId); - }); + expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId); + expect(UsersCache.retrieveStatusById).toHaveBeenCalledWith(userId); + }); - it('removes aria-describedby attribute from the user link on mouseleave', () => { - userLink.setAttribute('aria-describedby', 'popover'); - triggerEvent('mouseleave', userLink); + it('removes aria-describedby attribute from the user link on mouseleave', () => { + userLink.setAttribute('aria-describedby', 'popover'); + triggerEvent('mouseleave', userLink); - expect(userLink.getAttribute('aria-describedby')).toBe(null); - }); + expect(userLink.getAttribute('aria-describedby')).toBe(null); + }); - it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => { - const [firstPopover] = findPopovers(); - const withinFirstPopover = within(firstPopover); - const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' }); - const findUnfollowButton = () => - withinFirstPopover.queryByRole('button', { name: 'Unfollow' }); + it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => { + const [firstPopover] = findPopovers(); + const withinFirstPopover = within(firstPopover); + const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' }); + const findUnfollowButton = () => + withinFirstPopover.queryByRole('button', { name: 'Unfollow' }); - jest.runOnlyPendingTimers(); + jest.runOnlyPendingTimers(); - const { userId } = document.querySelector(selector).dataset; + const { userId } = document.querySelector(selector).dataset; - triggerEvent('click', findFollowButton()); + triggerEvent('click', findFollowButton()); - await waitForPromises(); + await waitForPromises(); - expect(findUnfollowButton()).not.toBe(null); - expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true }); + expect(findUnfollowButton()).not.toBe(null); + expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true }); - triggerEvent('click', findUnfollowButton()); + triggerEvent('click', findUnfollowButton()); - await waitForPromises(); + await waitForPromises(); - expect(findFollowButton()).not.toBe(null); - expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false }); + expect(findFollowButton()).not.toBe(null); + expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false }); + }); }); }); }); diff --git a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js b/spec/frontend/vue_merge_request_widget/components/action_buttons.js index a13db2f4d72..6d714aeaf18 100644 --- a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/action_buttons.js @@ -1,6 +1,6 @@ import { GlButton, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Actions from '~/vue_merge_request_widget/components/extensions/actions.vue'; +import Actions from '~/vue_merge_request_widget/components/action_buttons.vue'; let wrapper; diff --git a/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js index 150680caa7e..cb53dc1fb61 100644 --- a/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js @@ -10,11 +10,6 @@ function factory(propsData) { targetBranch: 'main', ...propsData, }, - provide: { - glFeatures: { - restructuredMrWidget: true.valueOf, - }, - }, }); } diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js index 05cd1bb5b3d..05cd1bb5b3d 100644 --- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js index 65cafc647e0..65cafc647e0 100644 --- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js index c2606346292..c2606346292 100644 --- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js diff --git a/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/humanized_text_spec.js index d6776c00b29..d6776c00b29 100644 --- a/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/humanized_text_spec.js diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js index e2386bc7f2b..e2386bc7f2b 100644 --- a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js index 712abfe228a..712abfe228a 100644 --- a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js diff --git a/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js index 198a4c2823a..198a4c2823a 100644 --- a/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js diff --git a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/index_spec.js index dc25596655a..dc25596655a 100644 --- a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/extensions/index_spec.js diff --git a/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js index f3aa5bb774f..f3aa5bb774f 100644 --- a/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js diff --git a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/utils_spec.js index 5799799ad5e..5799799ad5e 100644 --- a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/extensions/utils_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js index 01fbcb2154f..01fbcb2154f 100644 --- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js index 5d923d0383f..5d923d0383f 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js index 8a42e2e2ce7..8a42e2e2ce7 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js index 8fd93809e01..8fd93809e01 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js index 4e3e918f7fb..4e3e918f7fb 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js index 631aef412a6..631aef412a6 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js index ebd10f31fa7..ebd10f31fa7 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js index f0106914674..193a16bae8d 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js @@ -80,20 +80,20 @@ describe('MemoryUsage', () => { it('should have default data', () => { const data = MemoryUsage.data(); - expect(Array.isArray(data.memoryMetrics)).toBeTruthy(); + expect(Array.isArray(data.memoryMetrics)).toBe(true); expect(data.memoryMetrics.length).toBe(0); expect(typeof data.deploymentTime).toBe('number'); expect(data.deploymentTime).toBe(0); expect(typeof data.hasMetrics).toBe('boolean'); - expect(data.hasMetrics).toBeFalsy(); + expect(data.hasMetrics).toBe(false); expect(typeof data.loadFailed).toBe('boolean'); - expect(data.loadFailed).toBeFalsy(); + expect(data.loadFailed).toBe(false); expect(typeof data.loadingMetrics).toBe('boolean'); - expect(data.loadingMetrics).toBeTruthy(); + expect(data.loadingMetrics).toBe(true); expect(typeof data.backOffRequestCounter).toBe('number'); expect(data.backOffRequestCounter).toBe(0); @@ -144,7 +144,7 @@ describe('MemoryUsage', () => { vm.computeGraphData(metrics, deployment_time); const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm; - expect(hasMetrics).toBeTruthy(); + expect(hasMetrics).toBe(true); expect(memoryMetrics.length).toBeGreaterThan(0); expect(deploymentTime).toEqual(deployment_time); expect(memoryFrom).toEqual('9.13'); @@ -171,7 +171,7 @@ describe('MemoryUsage', () => { describe('template', () => { it('should render template elements correctly', () => { - expect(el.classList.contains('mr-memory-usage')).toBeTruthy(); + expect(el.classList.contains('mr-memory-usage')).toBe(true); expect(el.querySelector('.js-usage-info')).toBeDefined(); }); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js index efe2bf75c3f..efe2bf75c3f 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js index 6347e3c3be3..6347e3c3be3 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js index 6db82cedd80..534c0baf35d 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; @@ -8,8 +8,8 @@ jest.mock('~/vue_shared/plugins/global_toast'); let wrapper; -function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) { - wrapper = shallowMount(WidgetRebase, { +function createWrapper(propsData, mergeRequestWidgetGraphql) { + wrapper = mount(WidgetRebase, { propsData, data() { return { @@ -22,7 +22,7 @@ function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) }, }; }, - provide: { glFeatures: { mergeRequestWidgetGraphql, rebaseWithoutCiUi } }, + provide: { glFeatures: { mergeRequestWidgetGraphql } }, mocks: { $apollo: { queries: { @@ -110,7 +110,7 @@ describe('Merge request widget rebase component', () => { expect(findRebaseMessageText()).toContain('Something went wrong!'); }); - describe('Rebase buttons with flag rebaseWithoutCiUi', () => { + describe('Rebase buttons with', () => { beforeEach(() => { createWrapper( { @@ -124,7 +124,6 @@ describe('Merge request widget rebase component', () => { }, }, mergeRequestWidgetGraphql, - { rebaseWithoutCiUi: true }, ); }); @@ -149,35 +148,6 @@ describe('Merge request widget rebase component', () => { expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); }); }); - - describe('Rebase button with rebaseWithoutCiUI flag disabled', () => { - beforeEach(() => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: true, - }, - service: { - rebase: rebaseMock, - poll: pollMock, - }, - }, - mergeRequestWidgetGraphql, - ); - }); - - it('standard rebase button is rendered', () => { - expect(findStandardRebaseButton().exists()).toBe(true); - expect(findRebaseWithoutCiButton().exists()).toBe(false); - }); - - it('calls rebase method with skip_ci false', () => { - findStandardRebaseButton().vm.$emit('click'); - - expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); - }); - }); }); describe('without permissions', () => { @@ -216,24 +186,7 @@ describe('Merge request widget rebase component', () => { }); }); - it('does not render the "Rebase without pipeline" button with rebaseWithoutCiUI flag enabled', () => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: false, - targetBranch: exampleTargetBranch, - }, - service: {}, - }, - mergeRequestWidgetGraphql, - { rebaseWithoutCiUi: true }, - ); - - expect(findRebaseWithoutCiButton().exists()).toBe(false); - }); - - it('does not render the standard rebase button with rebaseWithoutCiUI flag disabled', () => { + it('does render the "Rebase without pipeline" button', () => { createWrapper( { mr: { @@ -246,7 +199,7 @@ describe('Merge request widget rebase component', () => { mergeRequestWidgetGraphql, ); - expect(findStandardRebaseButton().exists()).toBe(false); + expect(findRebaseWithoutCiButton().exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js index 15522f7ac1d..15522f7ac1d 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js index c25e10c5249..11373be578a 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js @@ -6,7 +6,6 @@ describe('MR widget status icon component', () => { let wrapper; const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findDisabledMergeButton = () => wrapper.find('[data-testid="disabled-merge-button"]'); const createWrapper = (props, mountFn = shallowMount) => { wrapper = mountFn(mrStatusIcon, { @@ -41,20 +40,4 @@ describe('MR widget status icon component', () => { expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true); }); }); - - describe('with disabled button', () => { - it('renders a disabled button', () => { - createWrapper({ status: 'failed', showDisabledButton: true }); - - expect(findDisabledMergeButton().exists()).toBe(true); - }); - }); - - describe('without disabled button', () => { - it('does not render a disabled button', () => { - createWrapper({ status: 'failed' }); - - expect(findDisabledMergeButton().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js index 352bc1a08ea..352bc1a08ea 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js diff --git a/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js b/spec/frontend/vue_merge_request_widget/components/pipeline_tour_mock_data.js index eef087d62b8..eef087d62b8 100644 --- a/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js +++ b/spec/frontend/vue_merge_request_widget/components/pipeline_tour_mock_data.js diff --git a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js index e393b56034d..e393b56034d 100644 --- a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap new file mode 100644 index 00000000000..de25e2a0450 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap @@ -0,0 +1,241 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = ` +<div + class="mr-widget-body media" +> + <svg + aria-hidden="true" + class="gl-text-blue-500 gl-mr-3 gl-mt-1 gl-icon s24" + data-testid="status_scheduled-icon" + role="img" + > + <use + href="#status_scheduled" + /> + </svg> + + <div + class="media-body gl-display-flex" + > + + <h4 + class="gl-mr-3" + data-testid="statusText" + > + Set by + <a + class="author-link inline" + > + <img + class="avatar avatar-inline s16" + src="no_avatar.png" + /> + + <span + class="author" + > + + </span> + </a> + to be merged automatically when the pipeline succeeds + </h4> + + <div + class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1" + > + <div> + <div + class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" + lazy="" + no-caret="" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="dropdown-icon gl-icon s16" + data-testid="ellipsis_v-icon" + role="img" + > + <use + href="#ellipsis_v" + /> + </svg> + + <span + class="gl-new-dropdown-button-text gl-sr-only" + > + + </span> + + <svg + aria-hidden="true" + class="gl-button-icon dropdown-chevron gl-icon s16" + data-testid="chevron-down-icon" + role="img" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + class="dropdown-menu dropdown-menu-right" + role="menu" + tabindex="-1" + > + <!----> + </ul> + </div> + + <button + class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" + data-qa-selector="cancel_auto_merge_button" + data-testid="cancelAutomaticMergeButton" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Cancel auto-merge + + </span> + </button> + </div> + </div> + </div> +</div> +`; + +exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = ` +<div + class="mr-widget-body media" +> + <svg + aria-hidden="true" + class="gl-text-blue-500 gl-mr-3 gl-mt-1 gl-icon s24" + data-testid="status_scheduled-icon" + role="img" + > + <use + href="#status_scheduled" + /> + </svg> + + <div + class="media-body gl-display-flex" + > + + <h4 + class="gl-mr-3" + data-testid="statusText" + > + Set by + <a + class="author-link inline" + > + <img + class="avatar avatar-inline s16" + src="no_avatar.png" + /> + + <span + class="author" + > + + </span> + </a> + to be merged automatically when the pipeline succeeds + </h4> + + <div + class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1" + > + <div> + <div + class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" + lazy="" + no-caret="" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="dropdown-icon gl-icon s16" + data-testid="ellipsis_v-icon" + role="img" + > + <use + href="#ellipsis_v" + /> + </svg> + + <span + class="gl-new-dropdown-button-text gl-sr-only" + > + + </span> + + <svg + aria-hidden="true" + class="gl-button-icon dropdown-chevron gl-icon s16" + data-testid="chevron-down-icon" + role="img" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + class="dropdown-menu dropdown-menu-right" + role="menu" + tabindex="-1" + > + <!----> + </ul> + </div> + + <button + class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" + data-qa-selector="cancel_auto_merge_button" + data-testid="cancelAutomaticMergeButton" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Cancel auto-merge + + </span> + </button> + </div> + </div> + </div> +</div> +`; diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap index 98297630792..7e741bf4660 100644 --- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap @@ -5,7 +5,7 @@ exports[`PipelineFailed should render error message with a disabled merge button class="mr-widget-body media" > <status-icon-stub - showdisabledbutton="true" + show-disabled-button="true" status="warning" /> @@ -13,7 +13,7 @@ exports[`PipelineFailed should render error message with a disabled merge button class="media-body space-children" > <span - class="bold" + class="gl-ml-0! gl-text-body! bold" > <gl-sprintf-stub message="Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}" diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap index f9936f22ea3..f9936f22ea3 100644 --- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js index c0add94e6ed..c0add94e6ed 100644 --- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js index 1900b53ac11..1900b53ac11 100644 --- a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js index 0e1c38437f0..c9aca01083d 100644 --- a/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js @@ -50,7 +50,7 @@ describe('MergeFailedPipelineConfirmationDialog', () => { it('should emit the mergeWithFailedPipeline event', () => { findMergeBtn().vm.$emit('click'); - expect(wrapper.emitted('mergeWithFailedPipeline')).toBeTruthy(); + expect(wrapper.emitted('mergeWithFailedPipeline')).toHaveLength(1); }); it('when the cancel button is clicked should emit cancel and call hide', () => { @@ -58,14 +58,14 @@ describe('MergeFailedPipelineConfirmationDialog', () => { findCancelBtn().vm.$emit('click'); - expect(wrapper.emitted('cancel')).toBeTruthy(); + expect(wrapper.emitted('cancel')).toHaveLength(1); expect(findModal().vm.hide).toHaveBeenCalled(); }); it('should emit cancel when the hide event is emitted', () => { findModal().vm.$emit('hide'); - expect(wrapper.emitted('cancel')).toBeTruthy(); + expect(wrapper.emitted('cancel')).toHaveLength(1); }); it('when modal is shown it will focus the cancel button', () => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js index f3061d792d0..9332b7e334a 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js @@ -18,11 +18,6 @@ describe('MRWidgetArchived', () => { expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull(); }); - it('renders a disabled button', () => { - expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Merge'); - }); - it('renders information', () => { expect(vm.$el.querySelector('.bold').textContent.trim()).toEqual( 'Merge unavailable: merge requests are read-only on archived projects.', diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js index 7387ed2d5e9..28182793683 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { trimText } from 'helpers/text_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -37,7 +37,7 @@ function factory(propsData, stateOverride = {}) { } wrapper = extendedWrapper( - shallowMount(autoMergeEnabledComponent, { + mount(autoMergeEnabledComponent, { propsData: { mr: propsData, service: new MRWidgetService({}), @@ -73,7 +73,7 @@ const defaultMrProps = () => ({ autoMergeStrategy: MWPS_MERGE_STRATEGY, }); -const getStatusText = () => wrapper.findByTestId('statusText').attributes('message'); +const getStatusText = () => wrapper.findByTestId('statusText').text(); describe('MRWidgetAutoMergeEnabled', () => { let oldWindowGl; @@ -102,74 +102,6 @@ describe('MRWidgetAutoMergeEnabled', () => { }); describe('computed', () => { - describe('canRemoveSourceBranch', () => { - it('should return true when user is able to remove source branch', () => { - factory({ - ...defaultMrProps(), - }); - - expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true); - }); - - it.each` - mergeUserId | currentUserId - ${2} | ${1} - ${1} | ${2} - `( - 'should return false when user id is not the same with who set the MWPS', - ({ mergeUserId, currentUserId }) => { - factory({ - ...defaultMrProps(), - mergeUserId, - currentUserId, - }); - - expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false); - }, - ); - - it('should not find "Delete" button when shouldRemoveSourceBranch set to true', () => { - factory({ - ...defaultMrProps(), - shouldRemoveSourceBranch: true, - }); - - expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false); - }); - - it('should find "Delete" button when shouldRemoveSourceBranch overrides state.forceRemoveSourceBranch', () => { - factory( - { - ...defaultMrProps(), - shouldRemoveSourceBranch: false, - }, - { - forceRemoveSourceBranch: true, - }, - ); - - expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true); - }); - - it('should find "Delete" button when shouldRemoveSourceBranch set to false', () => { - factory({ - ...defaultMrProps(), - shouldRemoveSourceBranch: false, - }); - - expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true); - }); - - it('should return false if user is not able to remove the source branch', () => { - factory({ - ...defaultMrProps(), - canRemoveSourceBranch: false, - }); - - expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false); - }); - }); - describe('cancelButtonText', () => { it('should return "Cancel" if MWPS is selected', () => { factory({ @@ -205,7 +137,7 @@ describe('MRWidgetAutoMergeEnabled', () => { await waitForPromises(); - expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy(); + expect(wrapper.vm.isCancellingAutoMerge).toBe(true); if (mergeRequestWidgetGraphql) { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); } else { @@ -265,50 +197,14 @@ describe('MRWidgetAutoMergeEnabled', () => { expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true); }); - it('should show source branch will be deleted text when it source branch set to remove', () => { - factory({ - ...defaultMrProps(), - shouldRemoveSourceBranch: true, - }); - - const normalizedText = wrapper.text().replace(/\s+/g, ' '); - - expect(normalizedText).toContain('Deletes the source branch'); - expect(normalizedText).not.toContain('Does not delete the source branch'); - }); - - it('should not show delete source branch button when user not able to delete source branch', () => { - factory({ - ...defaultMrProps(), - currentUserId: 4, - }); - - expect(wrapper.find('.js-remove-source-branch').exists()).toBe(false); - }); - - it('should disable delete source branch button when the action is in progress', async () => { - factory({ - ...defaultMrProps(), - }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isRemovingSourceBranch: true, - }); - - await nextTick(); - - expect(wrapper.find('.js-remove-source-branch').props('loading')).toBe(true); - }); - it('should render the status text as "...to merged automatically" if MWPS is selected', () => { factory({ ...defaultMrProps(), autoMergeStrategy: MWPS_MERGE_STRATEGY, }); - expect(getStatusText()).toBe( - 'Set by %{merge_author} to be merged automatically when the pipeline succeeds', + expect(getStatusText()).toContain( + 'to be merged automatically when the pipeline succeeds', ); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js index 24198096564..9320e733636 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -1,5 +1,5 @@ import { GlLoadingIcon, GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; @@ -10,7 +10,7 @@ describe('MRWidgetAutoMergeFailed', () => { const findButton = () => wrapper.find(GlButton); const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => { - wrapper = shallowMount(AutoMergeFailedComponent, { + wrapper = mount(AutoMergeFailedComponent, { propsData: { ...props }, data() { if (mergeRequestWidgetGraphql) { @@ -60,7 +60,7 @@ describe('MRWidgetAutoMergeFailed', () => { await nextTick(); - expect(findButton().attributes('disabled')).toBe('true'); + expect(findButton().attributes('disabled')).toBe('disabled'); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js index afe6bd0e767..02de426204b 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js @@ -15,10 +15,6 @@ describe('MRWidgetChecking', () => { vm.$destroy(); }); - it('renders disabled button', () => { - expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - }); - it('renders loading icon', () => { expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner'); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js index 6ae218ce6f8..f7d046eb8f9 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js @@ -36,28 +36,4 @@ describe('MRWidgetClosed', () => { it('renders warning icon', () => { expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); }); - - it('renders closed by information with author and time', () => { - expect( - vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '), - ).toContain('Closed by Administrator less than a minute ago'); - }); - - it('links to the user that closed the MR', () => { - expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual( - 'http://localhost:3000/root', - ); - }); - - it('renders information about the changes not being merged', () => { - expect( - vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '), - ).toContain('The changes were not merged into so_long_jquery'); - }); - - it('renders link for target branch', () => { - expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual( - '/twitter/flight/commits/so_long_jquery', - ); - }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js index 663fabb761c..663fabb761c 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js index 2796403b7d0..774e2bafed3 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js @@ -27,7 +27,6 @@ describe('Commits header component', () => { const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count'); const findCommitToggle = () => wrapper.find('.commit-edit-toggle'); - const findCommitsCountMessage = () => wrapper.find('.commits-count-message'); const findTargetBranchMessage = () => wrapper.find('.label-branch'); const findModifyButton = () => wrapper.find('.modify-message-button'); @@ -40,7 +39,7 @@ describe('Commits header component', () => { }); it('has commits count message showing 1 commit', () => { - expect(findCommitsCountMessage().text()).toBe('1 commit'); + expect(wrapper.text()).toContain('1 commit'); }); it('has button with modify commit message', () => { @@ -75,7 +74,7 @@ describe('Commits header component', () => { }); it('has commits count message showing correct amount of commits', () => { - expect(findCommitsCountMessage().text()).toBe('5 commits'); + expect(wrapper.text()).toContain('5 commits'); }); it('has button with modify merge commit message', () => { @@ -89,7 +88,7 @@ describe('Commits header component', () => { }); it('has commits count message showing one commit when squash is enabled', () => { - expect(findCommitsCountMessage().text()).toBe('1 commit'); + expect(wrapper.text()).toContain('1 commit'); }); it('has button with modify commit messages text', () => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js index 7a92484695c..7a9fd5b002d 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import { removeBreakLine } from 'helpers/text_helper'; @@ -23,7 +23,7 @@ describe('MRWidgetConflicts', () => { async function createComponent(propsData = {}) { wrapper = extendedWrapper( - shallowMount(ConflictsComponent, { + mount(ConflictsComponent, { propsData, provide: { glFeatures: { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js index 6d8e7056366..989aa76f09b 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -1,6 +1,5 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import MrWidgetFailedToMerge from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; @@ -54,7 +53,31 @@ describe('MRWidgetFailedToMerge', () => { await nextTick(); - expect(wrapper.vm.mergeError).toBe('contains line breaks.'); + expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains line breaks.'); + }); + + it('does not append an extra period', async () => { + createComponent({ mr: { mergeError: 'contains a period.' } }); + + await nextTick(); + + expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains a period.'); + }); + + it('does not insert an extra space between the final character and the period', async () => { + createComponent({ mr: { mergeError: 'contains a <a href="http://example.com">link</a>.' } }); + + await nextTick(); + + expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains a link.'); + }); + + it('removes extra spaces', async () => { + createComponent({ mr: { mergeError: 'contains a lot of spaces .' } }); + + await nextTick(); + + expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains a lot of spaces.'); }); }); @@ -116,7 +139,6 @@ describe('MRWidgetFailedToMerge', () => { it('renders warning icon and disabled merge button', () => { expect(wrapper.find('.js-ci-status-icon-warning')).not.toBeNull(); - expect(wrapper.find(StatusIcon).props('showDisabledButton')).toBe(true); }); it('renders given error', () => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js index 29ee7e0010f..2606933450e 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js @@ -1,5 +1,5 @@ import { getByRole } from '@testing-library/dom'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants'; @@ -10,14 +10,6 @@ import eventHub from '~/vue_merge_request_widget/event_hub'; describe('MRWidgetMerged', () => { let vm; const targetBranch = 'foo'; - const selectors = { - get copyMergeShaButton() { - return vm.$el.querySelector('button.js-mr-merged-copy-sha'); - }, - get mergeCommitShaLink() { - return vm.$el.querySelector('a.js-mr-merged-commit-sha'); - }, - }; beforeEach(() => { jest.spyOn(document, 'dispatchEvent'); @@ -177,58 +169,11 @@ describe('MRWidgetMerged', () => { expect(vm.$el.textContent).toContain('Administrator'); }); - it('renders branch information', () => { - expect(vm.$el.textContent).toContain('The changes were merged into'); - expect(vm.$el.textContent).toContain(targetBranch); - }); - - it('renders information about branch being deleted', () => { - expect(vm.$el.textContent).toContain('The source branch has been deleted'); - }); - it('shows revert and cherry-pick buttons', () => { expect(vm.$el.textContent).toContain('Revert'); expect(vm.$el.textContent).toContain('Cherry-pick'); }); - it('shows button to copy commit SHA to clipboard', () => { - expect(selectors.copyMergeShaButton).not.toBe(null); - expect(selectors.copyMergeShaButton.dataset.clipboardText).toBe(vm.mr.mergeCommitSha); - }); - - it('hides button to copy commit SHA if SHA does not exist', async () => { - vm.mr.mergeCommitSha = null; - - await nextTick(); - - expect(selectors.copyMergeShaButton).toBe(null); - expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with'); - }); - - it('shows merge commit SHA link', () => { - expect(selectors.mergeCommitShaLink).not.toBe(null); - expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha); - expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath); - }); - - it('should not show source branch deleted text', async () => { - vm.mr.sourceBranchRemoved = false; - - await nextTick(); - - expect(vm.$el.innerText).not.toContain('The source branch has been deleted'); - }); - - it('should show source branch deleting text', async () => { - vm.mr.isRemovingSourceBranch = true; - vm.mr.sourceBranchRemoved = false; - - await nextTick(); - - expect(vm.$el.innerText).toContain('The source branch is being deleted'); - expect(vm.$el.innerText).not.toContain('The source branch has been deleted'); - }); - it('should use mergedEvent mergedAt as tooltip title', () => { expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm UTC'); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js index e16c897a49b..49bd3739fdb 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js @@ -43,19 +43,6 @@ describe('MRWidgetMerging', () => { ).toContain('Merging!'); }); - it('renders branch information', () => { - expect( - wrapper - .find('.mr-info-list') - .text() - .trim() - .replace(/\s\s+/g, ' ') - .replace(/[\r\n]+/g, ' '), - ).toEqual('Merges changes into branch'); - - expect(wrapper.find('a').attributes('href')).toBe('/branch-path'); - }); - describe('initiateMergePolling', () => { it('should call simplePoll', () => { wrapper.vm.initiateMergePolling(); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js index ddce07954ab..ddce07954ab 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js index 63e93074857..63e93074857 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js index c7c0b69425d..6de0c06c33d 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js @@ -13,7 +13,7 @@ describe('NothingToMerge', () => { }); it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(vm.$el.classList.contains('mr-widget-body')).toBe(true); expect(vm.$el.querySelector('[data-testid="createFileButton"]').href).toContain(newBlobPath); expect(vm.$el.innerText).toContain('Use merge requests to propose changes to your project'); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js index 9b10b078e89..9b10b078e89 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js index 3e0840fef4e..4e44ac539f2 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -1,5 +1,4 @@ import { shallowMount } from '@vue/test-utils'; -import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue'; describe('PipelineFailed', () => { @@ -9,8 +8,6 @@ describe('PipelineFailed', () => { wrapper = shallowMount(PipelineFailed); }; - const findStatusIcon = () => wrapper.find(statusIcon); - beforeEach(() => { createComponent(); }); @@ -23,8 +20,4 @@ describe('PipelineFailed', () => { it('should render error message with a disabled merge button', () => { expect(wrapper.element).toMatchSnapshot(); }); - - it('merge button should be disabled', () => { - expect(findStatusIcon().props('showDisabledButton')).toBe(true); - }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js index 46d90ddc83c..6e89cd41559 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,5 +1,5 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import { GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import produce from 'immer'; @@ -10,7 +10,6 @@ import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/state import simplePoll from '~/lib/utils/simple_poll'; import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; -import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; import MergeFailedPipelineConfirmationDialog from '~/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue'; @@ -60,6 +59,7 @@ const createTestMr = (customConfig) => { transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition), translateStateToMachine: () => this.transitionStateMachine(), state: 'open', + canMerge: true, }; Object.assign(mr, customConfig.mr); @@ -71,8 +71,8 @@ const createTestService = () => ({ merge: jest.fn(), poll: jest.fn().mockResolvedValue(), }); -const localVue = createLocalVue(); -localVue.use(VueApollo); + +Vue.use(VueApollo); let wrapper; let readyToMergeResponseSpy; @@ -90,10 +90,9 @@ const createReadyToMergeResponse = (customMr) => { const createComponent = ( customConfig = {}, mergeRequestWidgetGraphql = false, - restructuredMrWidget = false, + restructuredMrWidget = true, ) => { wrapper = shallowMount(ReadyToMerge, { - localVue, propsData: { mr: createTestMr(customConfig), service: createTestService(), @@ -112,7 +111,6 @@ const createComponent = ( }; const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); -const findCommitsHeaderElement = () => wrapper.find(CommitsHeader); const findCommitEditElements = () => wrapper.findAll(CommitEdit); const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown); const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label'); @@ -371,7 +369,7 @@ describe('ReadyToMerge', () => { const params = wrapper.vm.service.merge.mock.calls[0][0]; - expect(params.should_remove_source_branch).toBeTruthy(); + expect(params.should_remove_source_branch).toBe(true); expect(params.auto_merge_strategy).toBeUndefined(); }); @@ -395,7 +393,7 @@ describe('ReadyToMerge', () => { const params = wrapper.vm.service.merge.mock.calls[0][0]; - expect(params.should_remove_source_branch).toBeTruthy(); + expect(params.should_remove_source_branch).toBe(true); expect(params.auto_merge_strategy).toBeUndefined(); }); @@ -471,8 +469,8 @@ describe('ReadyToMerge', () => { expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]); - expect(cpc).toBeFalsy(); - expect(spc).toBeTruthy(); + expect(cpc).toBe(false); + expect(spc).toBe(true); }); it('should continue polling until MR is merged', async () => { @@ -494,8 +492,8 @@ describe('ReadyToMerge', () => { await waitForPromises(); - expect(cpc).toBeTruthy(); - expect(spc).toBeFalsy(); + expect(cpc).toBe(true); + expect(spc).toBe(false); }); }); }); @@ -529,13 +527,13 @@ describe('ReadyToMerge', () => { mr: { commitsCount: 2, enableSquashBeforeMerge: true }, }); - expect(findCheckboxElement().exists()).toBeTruthy(); + expect(findCheckboxElement().exists()).toBe(true); }); it('should not be rendered when squash before merge is disabled', () => { createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); - expect(findCheckboxElement().exists()).toBeFalsy(); + expect(findCheckboxElement().exists()).toBe(false); }); it('should be rendered when there is only 1 commit', () => { @@ -576,71 +574,9 @@ describe('ReadyToMerge', () => { }); }); - describe('commits count collapsible header', () => { - it('should be rendered when fast-forward is disabled', () => { - createComponent(); - - expect(findCommitsHeaderElement().exists()).toBeTruthy(); - }); - - describe('when fast-forward is enabled', () => { - it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => { - createComponent({ - mr: { - ffOnlyEnabled: true, - enableSquashBeforeMerge: true, - squashIsSelected: true, - commitsCount: 2, - }, - }); - - expect(findCommitsHeaderElement().exists()).toBeTruthy(); - }); - - it('should not be rendered if squash before merge is disabled', () => { - createComponent({ - mr: { - ffOnlyEnabled: true, - enableSquashBeforeMerge: false, - squash: true, - commitsCount: 2, - }, - }); - - expect(findCommitsHeaderElement().exists()).toBeFalsy(); - }); - - it('should not be rendered if squash is disabled', () => { - createComponent({ - mr: { - ffOnlyEnabled: true, - squash: false, - enableSquashBeforeMerge: true, - commitsCount: 2, - }, - }); - - expect(findCommitsHeaderElement().exists()).toBeFalsy(); - }); - - it('should not be rendered if commits count is 1', () => { - createComponent({ - mr: { - ffOnlyEnabled: true, - squash: true, - enableSquashBeforeMerge: true, - commitsCount: 1, - }, - }); - - expect(findCommitsHeaderElement().exists()).toBeFalsy(); - }); - }); - }); - describe('commits edit components', () => { describe('when fast-forward merge is enabled', () => { - it('should not be rendered if squash is disabled', () => { + it('should not be rendered if squash is disabled', async () => { createComponent({ mr: { ffOnlyEnabled: true, @@ -679,7 +615,7 @@ describe('ReadyToMerge', () => { expect(findCommitEditElements().length).toBe(0); }); - it('should have one edit component if squash is enabled and there is more than 1 commit', () => { + it('should have one edit component if squash is enabled and there is more than 1 commit', async () => { createComponent({ mr: { ffOnlyEnabled: true, @@ -689,18 +625,14 @@ describe('ReadyToMerge', () => { }, }); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); + expect(findCommitEditElements().length).toBe(1); expect(findFirstCommitEditLabel()).toBe('Squash commit message'); }); }); - it('should have one edit component when squash is disabled', () => { - createComponent(); - - expect(findCommitEditElements().length).toBe(1); - }); - - it('should have two edit components when squash is enabled and there is more than 1 commit', () => { + it('should have two edit components when squash is enabled and there is more than 1 commit', async () => { createComponent({ mr: { commitsCount: 2, @@ -709,6 +641,8 @@ describe('ReadyToMerge', () => { }, }); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); + expect(findCommitEditElements().length).toBe(2); }); @@ -738,11 +672,12 @@ describe('ReadyToMerge', () => { }, }); await nextTick(); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); expect(findCommitEditElements().length).toBe(2); }); - it('should have one edit components when squash is enabled and there is 1 commit only', () => { + it('should have one edit components when squash is enabled and there is 1 commit only', async () => { createComponent({ mr: { commitsCount: 1, @@ -751,16 +686,12 @@ describe('ReadyToMerge', () => { }, }); - expect(findCommitEditElements().length).toBe(1); - }); - - it('should have correct edit merge commit label', () => { - createComponent(); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); - expect(findFirstCommitEditLabel()).toBe('Merge commit message'); + expect(findCommitEditElements().length).toBe(1); }); - it('should have correct edit squash commit label', () => { + it('should have correct edit squash commit label', async () => { createComponent({ mr: { commitsCount: 2, @@ -769,6 +700,8 @@ describe('ReadyToMerge', () => { }, }); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); + expect(findFirstCommitEditLabel()).toBe('Squash commit message'); }); }); @@ -777,48 +710,26 @@ describe('ReadyToMerge', () => { it('should not be rendered if squash is disabled', () => { createComponent(); - expect(findCommitDropdownElement().exists()).toBeFalsy(); + expect(findCommitDropdownElement().exists()).toBe(false); }); - it('should be rendered if squash is enabled and there is more than 1 commit', () => { + it('should be rendered if squash is enabled and there is more than 1 commit', async () => { createComponent({ mr: { enableSquashBeforeMerge: true, squashIsSelected: true, commitsCount: 2 }, }); - expect(findCommitDropdownElement().exists()).toBeTruthy(); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); + + expect(findCommitDropdownElement().exists()).toBe(true); }); }); - it('renders a tip including a link to docs on templates', () => { + it('renders a tip including a link to docs on templates', async () => { createComponent(); - expect(findTipLink().exists()).toBe(true); - }); - }); - - describe('Merge request project settings', () => { - describe('when the merge commit merge method is enabled', () => { - beforeEach(() => { - createComponent({ - mr: { ffOnlyEnabled: false }, - }); - }); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); - it('should not show fast forward message', () => { - expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(false); - }); - }); - - describe('when the fast-forward merge method is enabled', () => { - beforeEach(() => { - createComponent({ - mr: { ffOnlyEnabled: true }, - }); - }); - - it('should show fast forward message', () => { - expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(true); - }); + expect(findTipLink().exists()).toBe(true); }); }); @@ -873,6 +784,7 @@ describe('ReadyToMerge', () => { createDefaultGqlComponent(); await waitForPromises(); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); expect(finderFn()).toBe(initialValue); }); @@ -880,6 +792,7 @@ describe('ReadyToMerge', () => { it('should have updated value after graphql refetch', async () => { createDefaultGqlComponent(); await waitForPromises(); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); triggerApprovalUpdated(); await waitForPromises(); @@ -890,6 +803,7 @@ describe('ReadyToMerge', () => { it('should not update if user has touched', async () => { createDefaultGqlComponent(); await waitForPromises(); + await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true); const input = wrapper.find(inputId); input.element.value = USER_COMMIT_MESSAGE; diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js index 2a343997cf5..2a343997cf5 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js index 6ea2e8675d3..6ea2e8675d3 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js index e2d79c61b9b..e2d79c61b9b 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js index 4998147c6b6..af52901f508 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js @@ -26,11 +26,11 @@ describe('Wip', () => { it('should have props', () => { const { mr, service } = WorkInProgress.props; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); + expect(mr.type instanceof Object).toBe(true); + expect(mr.required).toBe(true); - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); + expect(service.type instanceof Object).toBe(true); + expect(service.required).toBe(true); }); }); @@ -64,7 +64,7 @@ describe('Wip', () => { await waitForPromises(); - expect(vm.isMakingRequest).toBeTruthy(); + expect(vm.isMakingRequest).toBe(true); expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.'); }); @@ -81,12 +81,10 @@ describe('Wip', () => { }); it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(el.classList.contains('mr-widget-body')).toBe(true); expect(el.innerText).toContain( "Merge blocked: merge request must be marked as ready. It's still marked as draft.", ); - expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(el.querySelector('button').innerText).toContain('Merge'); expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain( 'Mark as ready', ); @@ -97,7 +95,7 @@ describe('Wip', () => { await nextTick(); - expect(el.querySelector('.js-remove-draft')).toEqual(null); + expect(el.querySelector('.js-remove-draft')).toBeNull(); }); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js index 5ec9654a4af..5ec9654a4af 100644 --- a/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js diff --git a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js b/spec/frontend/vue_merge_request_widget/components/terraform/mock_data.js index 8e46af5dfd6..8e46af5dfd6 100644 --- a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js +++ b/spec/frontend/vue_merge_request_widget/components/terraform/mock_data.js diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js index 8f20d6a8fc9..8f20d6a8fc9 100644 --- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js diff --git a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js index 3c9f6c2e165..3c9f6c2e165 100644 --- a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js new file mode 100644 index 00000000000..6bb718082a4 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js @@ -0,0 +1,19 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import App from '~/vue_merge_request_widget/components/widget/app.vue'; + +describe('MR Widget App', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(App, { + propsData: { + mr: {}, + }, + }); + }; + + it('mounts the component', () => { + createComponent(); + expect(wrapper.findByTestId('mr-widget-app').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js new file mode 100644 index 00000000000..3c08ffdef18 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js @@ -0,0 +1,167 @@ +import { nextTick } from 'vue'; +import * as Sentry from '@sentry/browser'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; +import Widget from '~/vue_merge_request_widget/components/widget/widget.vue'; + +describe('MR Widget', () => { + let wrapper; + + const findStatusIcon = () => wrapper.findComponent(StatusIcon); + + const createComponent = ({ propsData, slots } = {}) => { + wrapper = shallowMountExtended(Widget, { + propsData: { + loadingText: 'Loading widget', + widgetName: 'MyWidget', + value: { + collapsed: null, + expanded: null, + }, + ...propsData, + }, + slots, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('on mount', () => { + it('fetches collapsed', async () => { + const fetchCollapsedData = jest + .fn() + .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} })); + + createComponent({ propsData: { fetchCollapsedData } }); + await waitForPromises(); + expect(fetchCollapsedData).toHaveBeenCalled(); + expect(wrapper.vm.error).toBe(null); + }); + + it('sets the error text when fetch method fails', async () => { + const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject()); + createComponent({ propsData: { fetchCollapsedData } }); + await waitForPromises(); + expect(wrapper.vm.error).toBe('Failed to load'); + }); + + it('displays loading icon until request is made and then displays status icon when the request is complete', async () => { + const fetchCollapsedData = jest + .fn() + .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} })); + + createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } }); + + // Let on mount be called + await nextTick(); + + expect(findStatusIcon().props('isLoading')).toBe(true); + + // Wait until `fetchCollapsedData` is resolved + await waitForPromises(); + + expect(findStatusIcon().props('isLoading')).toBe(false); + expect(findStatusIcon().props('iconName')).toBe('warning'); + }); + + it('displays the loading text', async () => { + const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject()); + createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } }); + expect(wrapper.text()).not.toContain('Loading'); + await nextTick(); + expect(wrapper.text()).toContain('Loading'); + }); + }); + + describe('fetch', () => { + it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => { + const mockData = { headers: {}, status: 200, data: { vulnerabilities: [] } }; + createComponent({ propsData: { fetchCollapsedData: async () => mockData } }); + await waitForPromises(); + expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null }); + }); + + it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => { + const mockData1 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 1 }] } }; + const mockData2 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 2 }] } }; + + createComponent({ + propsData: { + multiPolling: true, + fetchCollapsedData: () => [ + () => Promise.resolve(mockData1), + () => Promise.resolve(mockData2), + ], + }, + }); + + await waitForPromises(); + + expect(wrapper.emitted('input')[0][0]).toEqual({ + collapsed: [mockData1.data, mockData2.data], + expanded: null, + }); + }); + + it('calls sentry when failed', async () => { + const error = new Error('Something went wrong'); + jest.spyOn(Sentry, 'captureException').mockImplementation(); + createComponent({ + propsData: { + fetchCollapsedData: async () => Promise.reject(error), + }, + }); + await waitForPromises(); + expect(wrapper.emitted('input')).toBeUndefined(); + expect(Sentry.captureException).toHaveBeenCalledWith(error); + }); + }); + + describe('content', () => { + it('displays summary property when summary slot is not provided', () => { + createComponent({ + propsData: { + summary: 'Hello world', + fetchCollapsedData: async () => Promise.resolve(), + }, + }); + + expect(wrapper.findByTestId('widget-extension-top-level-summary').text()).toBe('Hello world'); + }); + + it.todo('displays content property when content slot is not provided'); + + it('displays the summary slot when provided', () => { + createComponent({ + propsData: { + fetchCollapsedData: async () => Promise.resolve(), + }, + slots: { + summary: '<b>More complex summary</b>', + }, + }); + + expect(wrapper.findByTestId('widget-extension-top-level-summary').text()).toBe( + 'More complex summary', + ); + }); + + it('displays the content slot when provided', () => { + createComponent({ + propsData: { + fetchCollapsedData: async () => Promise.resolve(), + }, + slots: { + content: '<b>More complex content</b>', + }, + }); + + expect(wrapper.findByTestId('widget-extension-collapsed-section').text()).toBe( + 'More complex content', + ); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js index 7e7438bcc0f..7e7438bcc0f 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js index a285d26f404..a285d26f404 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_list_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js index 948d7ebab5e..948d7ebab5e 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_list_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js index e98b1160ae4..e98b1160ae4 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js index c27cbd8b781..c27cbd8b781 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js index eb6e3711e2e..eb6e3711e2e 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js index 5c1d3c8e8e8..5c1d3c8e8e8 100644 --- a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/utils_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/utils_spec.js index 69ea70549fe..69ea70549fe 100644 --- a/spec/frontend/vue_mr_widget/extensions/test_report/utils_spec.js +++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/utils_spec.js diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js index a06ad930abe..a06ad930abe 100644 --- a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/mock_data.js index 06dc93d101f..06dc93d101f 100644 --- a/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js +++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/mock_data.js diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js index 9a72e4a086b..9a72e4a086b 100644 --- a/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js index f5ad0ce7377..f5ad0ce7377 100644 --- a/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js +++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js diff --git a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js index d9faa7b2d25..d9faa7b2d25 100644 --- a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js index 20d00a116bb..20d00a116bb 100644 --- a/spec/frontend/vue_mr_widget/mock_data.js +++ b/spec/frontend/vue_merge_request_widget/mock_data.js diff --git a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js index 295b9df30b9..295b9df30b9 100644 --- a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js +++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js index b3af5eba364..819841317f9 100644 --- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js @@ -20,7 +20,6 @@ import { import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; import eventHub from '~/vue_merge_request_widget/event_hub'; import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; -import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; @@ -125,25 +124,13 @@ describe('MrWidgetOptions', () => { it('should return true when hasCI is true', () => { wrapper.vm.mr.hasCI = true; - expect(wrapper.vm.shouldRenderPipelines).toBeTruthy(); + expect(wrapper.vm.shouldRenderPipelines).toBe(true); }); it('should return false when hasCI is false', () => { wrapper.vm.mr.hasCI = false; - expect(wrapper.vm.shouldRenderPipelines).toBeFalsy(); - }); - }); - - describe('shouldRenderRelatedLinks', () => { - it('should return false for the initial data', () => { - expect(wrapper.vm.shouldRenderRelatedLinks).toBeFalsy(); - }); - - it('should return true if there is relatedLinks in MR', () => { - Vue.set(wrapper.vm.mr, 'relatedLinks', {}); - - expect(wrapper.vm.shouldRenderRelatedLinks).toBeTruthy(); + expect(wrapper.vm.shouldRenderPipelines).toBe(false); }); }); @@ -316,7 +303,7 @@ describe('MrWidgetOptions', () => { expect(wrapper.vm.service.checkStatus).toHaveBeenCalled(); expect(wrapper.vm.mr.setData).toHaveBeenCalled(); expect(wrapper.vm.handleNotification).toHaveBeenCalledWith(mockData); - expect(isCbExecuted).toBeTruthy(); + expect(isCbExecuted).toBe(true); }); }); }); @@ -519,61 +506,6 @@ describe('MrWidgetOptions', () => { }); }); - describe('rendering relatedLinks', () => { - beforeEach(() => { - return createComponent({ - ...mockData, - issues_links: { - closing: ` - <a class="close-related-link" href="#"> - Close - </a> - `, - }, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders if there are relatedLinks', () => { - expect(wrapper.find('.close-related-link').exists()).toBe(true); - }); - - it('does not render if state is nothingToMerge', async () => { - wrapper.vm.mr.state = stateKey.nothingToMerge; - await nextTick(); - expect(wrapper.find('.close-related-link').exists()).toBe(false); - }); - }); - - describe('rendering source branch removal status', () => { - it('renders when user cannot remove branch and branch should be removed', async () => { - wrapper.vm.mr.canRemoveSourceBranch = false; - wrapper.vm.mr.shouldRemoveSourceBranch = true; - wrapper.vm.mr.state = 'readyToMerge'; - - await nextTick(); - const tooltip = wrapper.find('[data-testid="question-o-icon"]'); - - expect(wrapper.text()).toContain('Deletes the source branch'); - expect(tooltip.attributes('title')).toBe( - 'A user with write access to the source branch selected this option', - ); - }); - - it('does not render in merged state', async () => { - wrapper.vm.mr.canRemoveSourceBranch = false; - wrapper.vm.mr.shouldRemoveSourceBranch = true; - wrapper.vm.mr.state = 'merged'; - - await nextTick(); - expect(wrapper.text()).toContain('The source branch has been deleted'); - expect(wrapper.text()).not.toContain('Deletes the source branch'); - }); - }); - describe('rendering deployments', () => { const changes = [ { @@ -1062,7 +994,7 @@ describe('MrWidgetOptions', () => { await createComponent(); - expect(pollRequest).toHaveBeenCalledTimes(6); + expect(pollRequest).toHaveBeenCalledTimes(4); }); }); @@ -1100,14 +1032,14 @@ describe('MrWidgetOptions', () => { registerExtension(pollingErrorExtension); await createComponent(); - expect(pollRequest).toHaveBeenCalledTimes(6); + expect(pollRequest).toHaveBeenCalledTimes(4); }); it('captures sentry error and displays error when poll has failed', async () => { registerExtension(pollingErrorExtension); await createComponent(); - expect(Sentry.captureException).toHaveBeenCalledTimes(5); + expect(Sentry.captureException).toHaveBeenCalled(); expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error')); expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed'); }); @@ -1126,7 +1058,7 @@ describe('MrWidgetOptions', () => { expect( wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]').exists(), ).toBe(false); - expect(Sentry.captureException).toHaveBeenCalledTimes(5); + expect(Sentry.captureException).toHaveBeenCalled(); expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error')); expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed'); }); diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js index 22562bb4ddb..22562bb4ddb 100644 --- a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/getters_spec.js index dc90fef63c6..dc90fef63c6 100644 --- a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/getters_spec.js diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/mutations_spec.js index a4e6788c7f6..a4e6788c7f6 100644 --- a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/mutations_spec.js diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js index fc760f5c5be..0246a8d4b0f 100644 --- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js @@ -25,10 +25,6 @@ describe('getStateKey', () => { expect(bound()).toEqual('readyToMerge'); - context.canMerge = false; - - expect(bound()).toEqual('notAllowedToMerge'); - context.autoMergeEnabled = true; context.hasMergeableDiscussionsState = true; @@ -105,22 +101,4 @@ describe('getStateKey', () => { expect(bound()).toEqual('rebase'); }); - - it.each` - canMerge | isSHAMismatch | stateKey - ${true} | ${true} | ${'shaMismatch'} - ${false} | ${true} | ${'notAllowedToMerge'} - ${false} | ${false} | ${'notAllowedToMerge'} - `( - 'returns $stateKey when canMerge is $canMerge and isSHAMismatch is $isSHAMismatch', - ({ canMerge, isSHAMismatch, stateKey }) => { - const bound = getStateKey.bind({ - canMerge, - isSHAMismatch, - commitsCount: 2, - }); - - expect(bound()).toEqual(stateKey); - }, - ); }); diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js index 3cdb4265ef0..3cdb4265ef0 100644 --- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_merge_request_widget/test_extensions.js index 1977f550577..1977f550577 100644 --- a/spec/frontend/vue_mr_widget/test_extensions.js +++ b/spec/frontend/vue_merge_request_widget/test_extensions.js diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap deleted file mode 100644 index 56a0218b374..00000000000 --- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = ` -<div - class="mr-widget-body media" -> - <gl-icon-stub - class="gl-text-blue-500 gl-mr-3 gl-mt-1" - name="status_scheduled" - size="24" - /> - - <div - class="media-body" - > - <h4 - class="gl-display-flex" - > - <span - class="gl-mr-3" - > - <gl-sprintf-stub - data-testid="statusText" - message="Set by %{merge_author} to be merged automatically when the pipeline succeeds" - /> - </span> - - <gl-button-stub - buttontextclasses="" - category="primary" - class="js-cancel-auto-merge" - data-qa-selector="cancel_auto_merge_button" - data-testid="cancelAutomaticMergeButton" - icon="" - size="small" - variant="default" - > - - Cancel auto-merge - - </gl-button-stub> - </h4> - - <section - class="mr-info-list" - > - <p - class="gl-display-flex" - > - <span - class="gl-mr-3" - > - Does not delete the source branch - </span> - - <gl-button-stub - buttontextclasses="" - category="primary" - class="js-remove-source-branch" - data-testid="removeSourceBranchButton" - icon="" - size="small" - variant="default" - > - - Delete source branch - - </gl-button-stub> - </p> - </section> - </div> -</div> -`; - -exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = ` -<div - class="mr-widget-body media" -> - <gl-icon-stub - class="gl-text-blue-500 gl-mr-3 gl-mt-1" - name="status_scheduled" - size="24" - /> - - <div - class="media-body" - > - <h4 - class="gl-display-flex" - > - <span - class="gl-mr-3" - > - <gl-sprintf-stub - data-testid="statusText" - message="Set by %{merge_author} to be merged automatically when the pipeline succeeds" - /> - </span> - - <gl-button-stub - buttontextclasses="" - category="primary" - class="js-cancel-auto-merge" - data-qa-selector="cancel_auto_merge_button" - data-testid="cancelAutomaticMergeButton" - icon="" - size="small" - variant="default" - > - - Cancel auto-merge - - </gl-button-stub> - </h4> - - <section - class="mr-info-list" - > - <p - class="gl-display-flex" - > - <span - class="gl-mr-3" - > - Does not delete the source branch - </span> - - <gl-button-stub - buttontextclasses="" - category="primary" - class="js-remove-source-branch" - data-testid="removeSourceBranchButton" - icon="" - size="small" - variant="default" - > - - Delete source branch - - </gl-button-stub> - </p> - </section> - </div> -</div> -`; diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js index ce51af31a70..59e21b2ff40 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -285,14 +285,14 @@ describe('AlertDetails', () => { }); it('displays a loading state when loading', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); describe('error state', () => { it('displays a error state correctly', () => { mountComponent({ data: { errored: true } }); - expect(wrapper.find(GlAlert).exists()).toBe(true); + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); }); it('renders html-errors correctly', () => { @@ -304,7 +304,7 @@ describe('AlertDetails', () => { it('does not display an error when dismissed', () => { mountComponent({ data: { errored: true, isErrorDismissed: true } }); - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js index 1216681038f..cf04c1eb24a 100644 --- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js @@ -28,8 +28,8 @@ describe('Alert Metrics', () => { }); } - const findChart = () => wrapper.find(MetricEmbed); - const findEmptyState = () => wrapper.find({ ref: 'emptyState' }); + const findChart = () => wrapper.findComponent(MetricEmbed); + const findEmptyState = () => wrapper.findComponent({ ref: 'emptyState' }); afterEach(() => { if (wrapper) { diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js index ba3b0335a8e..2a37ff2b784 100644 --- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js @@ -13,7 +13,7 @@ describe('AlertManagementStatus', () => { let wrapper; const findStatusDropdown = () => wrapper.findComponent(GlDropdown); const findFirstStatusOption = () => findStatusDropdown().findComponent(GlDropdownItem); - const findAllStatusOptions = () => findStatusDropdown().findAll(GlDropdownItem); + const findAllStatusOptions = () => findStatusDropdown().findAllComponents(GlDropdownItem); const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header'); const selectFirstStatusOption = () => { diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js index 29569734621..5a0ee5a59ba 100644 --- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js @@ -128,7 +128,7 @@ describe('Alert Details Sidebar Assignees', () => { wrapper.setData({ isDropdownSearching: false }); await nextTick(); - wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); + wrapper.findComponent(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: AlertSetAssignees, @@ -156,7 +156,7 @@ describe('Alert Details Sidebar Assignees', () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult); await nextTick(); - const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); + const SideBarAssigneeItem = wrapper.findAllComponents(SidebarAssignee).at(0); await SideBarAssigneeItem.vm.$emit('update-alert-assignees'); expect(wrapper.emitted('alert-error')).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js index ef75e038bff..3b38349622f 100644 --- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js @@ -65,7 +65,7 @@ describe('Alert Details Sidebar', () => { mountMethod: mount, alert: mockAlert, }); - expect(wrapper.find(SidebarAssignees).exists()).toBe(true); + expect(wrapper.findComponent(SidebarAssignees).exists()).toBe(true); }); it('should render side bar status dropdown', () => { @@ -73,7 +73,7 @@ describe('Alert Details Sidebar', () => { mountMethod: mount, alert: mockAlert, }); - expect(wrapper.find(SidebarStatus).exists()).toBe(true); + expect(wrapper.findComponent(SidebarStatus).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js index a5a9fb55737..6a750bb99c0 100644 --- a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js +++ b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js @@ -31,7 +31,7 @@ describe('Alert Details System Note', () => { it('renders the correct system note', () => { const noteId = wrapper.find('.note-wrapper').attributes('id'); - const iconName = wrapper.find(GlIcon).attributes('name'); + const iconName = wrapper.findComponent(GlIcon).attributes('name'); expect(noteId).toBe('note_1628'); expect(iconName).toBe(mockAlert.notes.nodes[0].systemNoteIconName); diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js index e5b7b693cb5..07c53c04723 100644 --- a/spec/frontend/vue_shared/components/actions_button_spec.js +++ b/spec/frontend/vue_shared/components/actions_button_spec.js @@ -45,9 +45,9 @@ describe('Actions button component', () => { return directiveBinding.value; }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); const findButtonTooltip = () => getTooltip(findButton()); - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownTooltip = () => getTooltip(findDropdown()); const parseDropdownItems = () => findDropdown() diff --git a/spec/frontend/vue_shared/components/alert_details_table_spec.js b/spec/frontend/vue_shared/components/alert_details_table_spec.js index b9a8a5bee97..8a9ee4699bd 100644 --- a/spec/frontend/vue_shared/components/alert_details_table_spec.js +++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js @@ -74,7 +74,7 @@ describe('AlertDetails', () => { }); it('displays a loading state when loading', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); @@ -130,7 +130,7 @@ describe('AlertDetails', () => { environmentData = { name: null, path: null }; mountComponent(); - expect(findTableFieldValueByKey('Environment').text()).toBeFalsy(); + expect(findTableFieldValueByKey('Environment').text()).toBe(''); }); }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js index d14f3e5559f..ce7fd40937f 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js @@ -43,6 +43,6 @@ describe('Blob Rich Viewer component', () => { }); it('is using Markdown View Field', () => { - expect(wrapper.find(MarkdownFieldView).exists()).toBe(true); + expect(wrapper.findComponent(MarkdownFieldView).exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js index 6b9658a6d18..ea708b6f3fe 100644 --- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -25,7 +25,7 @@ describe('Changed file icon', () => { wrapper.destroy(); }); - const findIcon = () => wrapper.find(GlIcon); + const findIcon = () => wrapper.findComponent(GlIcon); const findIconName = () => findIcon().props('name'); const findIconClasses = () => findIcon().classes(); const findTooltipText = () => wrapper.attributes('title'); @@ -51,7 +51,7 @@ describe('Changed file icon', () => { showTooltip: false, }); - expect(findTooltipText()).toBeFalsy(); + expect(findTooltipText()).toBeUndefined(); }); describe.each` @@ -87,7 +87,7 @@ describe('Changed file icon', () => { }); it('does not have tooltip text', () => { - expect(findTooltipText()).toBeFalsy(); + expect(findTooltipText()).toBeUndefined(); }); }); diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js index 1b502f9587c..2064bee9673 100644 --- a/spec/frontend/vue_shared/components/ci_icon_spec.js +++ b/spec/frontend/vue_shared/components/ci_icon_spec.js @@ -22,7 +22,7 @@ describe('CI Icon component', () => { }); expect(wrapper.find('span').exists()).toBe(true); - expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); }); describe('active icons', () => { diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js index fca5e664a96..b18b00e70bb 100644 --- a/spec/frontend/vue_shared/components/clipboard_button_spec.js +++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js @@ -21,7 +21,7 @@ describe('clipboard button', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); const expectConfirmationTooltip = async ({ event, message }) => { const title = 'Copy this value'; diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js index eefd1838988..31c08260dd0 100644 --- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js @@ -38,9 +38,9 @@ describe('Clone Dropdown Button', () => { ${'HTTP'} | ${1} | ${httpLink} `('renders correct link and a copy-button for $name', ({ index, value }) => { createComponent(); - const group = wrapper.findAll(GlFormInputGroup).at(index); + const group = wrapper.findAllComponents(GlFormInputGroup).at(index); expect(group.props('value')).toBe(value); - expect(group.find(GlFormInputGroup).exists()).toBe(true); + expect(group.findComponent(GlFormInputGroup).exists()).toBe(true); }); it.each` @@ -50,8 +50,8 @@ describe('Clone Dropdown Button', () => { `('does not fail if only $name is set', ({ name, value }) => { createComponent({ [name]: value }); - expect(wrapper.find(GlFormInputGroup).props('value')).toBe(value); - expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1); + expect(wrapper.findComponent(GlFormInputGroup).props('value')).toBe(value); + expect(wrapper.findAllComponents(GlDropdownSectionHeader).length).toBe(1); }); }); @@ -63,12 +63,12 @@ describe('Clone Dropdown Button', () => { `('allows null values for the props', ({ name, value }) => { createComponent({ ...defaultPropsData, [name]: value }); - expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1); + expect(wrapper.findAllComponents(GlDropdownSectionHeader).length).toBe(1); }); it('correctly calculates httpLabel for HTTPS protocol', () => { createComponent({ httpLink: httpsLink }); - expect(wrapper.find(GlDropdownSectionHeader).text()).toContain('HTTPS'); + expect(wrapper.findComponent(GlDropdownSectionHeader).text()).toContain('HTTPS'); }); }); }); diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js index 8cbe0630426..060048c4bbd 100644 --- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js +++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js @@ -16,14 +16,14 @@ describe('ColorPicker', () => { const setColor = '#000000'; const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value'; - const findGlFormGroup = () => wrapper.find(GlFormGroup); + const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); const colorPreview = () => wrapper.find('[data-testid="color-preview"]'); - const colorPicker = () => wrapper.find(GlFormInput); + const colorPicker = () => wrapper.findComponent(GlFormInput); const colorInput = () => wrapper.find('input[type="color"]'); - const colorTextInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]'); + const colorTextInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]'); const invalidFeedback = () => wrapper.find('.invalid-feedback'); - const description = () => wrapper.find(GlFormGroup).attributes('description'); - const presetColors = () => wrapper.findAll(GlLink); + const description = () => wrapper.findComponent(GlFormGroup).attributes('description'); + const presetColors = () => wrapper.findAllComponents(GlLink); beforeEach(() => { gon.suggested_label_colors = { diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js index d91853e7b79..1893e127f6f 100644 --- a/spec/frontend/vue_shared/components/commit_spec.js +++ b/spec/frontend/vue_shared/components/commit_spec.js @@ -9,11 +9,11 @@ describe('Commit component', () => { let wrapper; const findIcon = (name) => { - const icons = wrapper.findAll(GlIcon).filter((c) => c.attributes('name') === name); + const icons = wrapper.findAllComponents(GlIcon).filter((c) => c.attributes('name') === name); return icons.length ? icons.at(0) : icons; }; - const findUserAvatar = () => wrapper.find(UserAvatarLink); + const findUserAvatar = () => wrapper.findComponent(UserAvatarLink); const findRefName = () => wrapper.findByTestId('ref-name'); const createComponent = (propsData) => { @@ -47,7 +47,7 @@ describe('Commit component', () => { }, }); - expect(wrapper.find('.icon-container').find(GlIcon).exists()).toBe(true); + expect(wrapper.find('.icon-container').findComponent(GlIcon).exists()).toBe(true); }); describe('Given all the props', () => { diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js index 3ca1c943398..c1e682a1aae 100644 --- a/spec/frontend/vue_shared/components/confirm_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js @@ -51,13 +51,13 @@ describe('vue_shared/components/confirm_modal', () => { wrapper.destroy(); }); - const findModal = () => wrapper.find(GlModalStub); + const findModal = () => wrapper.findComponent(GlModalStub); const findForm = () => wrapper.find('form'); const findFormData = () => findForm() .findAll('input') .wrappers.map((x) => ({ name: x.attributes('name'), value: x.attributes('value') })); - const findDomElementListener = () => wrapper.find(DomElementListener); + const findDomElementListener = () => wrapper.findComponent(DomElementListener); const triggerOpenWithEventHub = (modalData) => { eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, modalData); }; @@ -104,7 +104,7 @@ describe('vue_shared/components/confirm_modal', () => { }); it('renders GlModal with data', () => { - expect(findModal().exists()).toBeTruthy(); + expect(findModal().exists()).toBe(true); expect(findModal().attributes()).toEqual( expect.objectContaining({ oktitle: MOCK_MODAL_DATA.modalAttributes.okTitle, diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js index 879d4aba441..8b1189f25d5 100644 --- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js @@ -20,7 +20,7 @@ describe('vue_shared/components/dismissible_alert', () => { wrapper.destroy(); }); - const findAlert = () => wrapper.find(GlAlert); + const findAlert = () => wrapper.findComponent(GlAlert); describe('default', () => { beforeEach(() => { @@ -45,7 +45,7 @@ describe('vue_shared/components/dismissible_alert', () => { }); it('emmits alertDismissed', () => { - expect(wrapper.emitted('alertDismissed')).toBeTruthy(); + expect(wrapper.emitted()).toHaveProperty('alertDismissed'); }); }); }); diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js index b8aeea38e77..f7030f38709 100644 --- a/spec/frontend/vue_shared/components/dismissible_container_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js @@ -33,7 +33,7 @@ describe('DismissibleContainer', () => { button.trigger('click'); - expect(wrapper.emitted().dismiss).toBeTruthy(); + expect(wrapper.emitted().dismiss).toEqual(expect.any(Array)); }); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js index 08e5d828b8f..e34ed31b4bf 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js @@ -1,80 +1,71 @@ -import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; -import { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; -import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue'; +describe('DropdownButton component', () => { + let wrapper; -const defaultLabel = 'Select'; -const customLabel = 'Select project'; + const defaultLabel = 'Select'; + const customLabel = 'Select project'; -const createComponent = (props, slots = {}) => { - const Component = Vue.extend(dropdownButtonComponent); - - return mountComponentWithSlots(Component, { props, slots }); -}; - -describe('DropdownButtonComponent', () => { - let vm; - - beforeEach(() => { - vm = createComponent(); - }); + const createComponent = (props, slots = {}) => { + wrapper = mount(DropdownButton, { propsData: props, slots }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('computed', () => { describe('dropdownToggleText', () => { it('returns default toggle text', () => { - expect(vm.toggleText).toBe(defaultLabel); + createComponent(); + + expect(wrapper.vm.toggleText).toBe(defaultLabel); }); it('returns custom toggle text when provided via props', () => { - const vmEmptyLabels = createComponent({ toggleText: customLabel }); + createComponent({ toggleText: customLabel }); - expect(vmEmptyLabels.toggleText).toBe(customLabel); - vmEmptyLabels.$destroy(); + expect(wrapper.vm.toggleText).toBe(customLabel); }); }); }); describe('template', () => { it('renders component container element of type `button`', () => { - expect(vm.$el.nodeName).toBe('BUTTON'); + createComponent(); + + expect(wrapper.element.nodeName).toBe('BUTTON'); }); it('renders component container element with required data attributes', () => { - expect(vm.$el.dataset.abilityName).toBe(vm.abilityName); - expect(vm.$el.dataset.fieldName).toBe(vm.fieldName); - expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath); - expect(vm.$el.dataset.labels).toBe(vm.labelsPath); - expect(vm.$el.dataset.namespacePath).toBe(vm.namespace); - expect(vm.$el.dataset.showAny).not.toBeDefined(); + createComponent(); + + expect(wrapper.element.dataset.abilityName).toBe(wrapper.vm.abilityName); + expect(wrapper.element.dataset.fieldName).toBe(wrapper.vm.fieldName); + expect(wrapper.element.dataset.issueUpdate).toBe(wrapper.vm.updatePath); + expect(wrapper.element.dataset.labels).toBe(wrapper.vm.labelsPath); + expect(wrapper.element.dataset.namespacePath).toBe(wrapper.vm.namespace); + expect(wrapper.element.dataset.showAny).toBeUndefined(); }); it('renders dropdown toggle text element', () => { - const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); + createComponent(); - expect(dropdownToggleTextEl).not.toBeNull(); - expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel); + expect(wrapper.find('.dropdown-toggle-text').text()).toBe(defaultLabel); }); it('renders dropdown button icon', () => { - const dropdownIconEl = vm.$el.querySelector('[data-testid="chevron-down-icon"]'); + createComponent(); - expect(dropdownIconEl).not.toBeNull(); + expect(wrapper.find('[data-testid="chevron-down-icon"]').exists()).toBe(true); }); it('renders slot, if default slot exists', () => { - vm = createComponent( - {}, - { - default: ['Lorem Ipsum Dolar'], - }, - ); - - expect(vm.$el.querySelector('.dropdown-toggle-text')).toBeNull(); - expect(vm.$el).toHaveText('Lorem Ipsum Dolar'); + createComponent({}, { default: ['Lorem Ipsum Dolar'] }); + + expect(wrapper.find('.dropdown-toggle-text').exists()).toBe(false); + expect(wrapper.text()).toBe('Lorem Ipsum Dolar'); }); }); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js index 084d0559665..dd3e55c82bb 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -8,7 +8,7 @@ describe('DropdownWidget component', () => { let wrapper; const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findSearch = () => wrapper.findComponent(GlSearchBoxByType); const createComponent = ({ props = {} } = {}) => { diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js index 87d6ed6b21f..170c947e520 100644 --- a/spec/frontend/vue_shared/components/expand_button_spec.js +++ b/spec/frontend/vue_shared/components/expand_button_spec.js @@ -37,11 +37,11 @@ describe('Expand button', () => { }); it('renders no text when short text is not provided', () => { - expect(wrapper.find(ExpandButton).text()).toBe(''); + expect(wrapper.findComponent(ExpandButton).text()).toBe(''); }); it('does not render expanded text', () => { - expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.short); + expect(wrapper.findComponent(ExpandButton).text().trim()).not.toBe(text.short); }); describe('when short text is provided', () => { @@ -55,13 +55,13 @@ describe('Expand button', () => { }); it('renders short text', () => { - expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short); + expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.short); }); it('renders button before text', () => { expect(expanderPrependEl().isVisible()).toBe(true); expect(expanderAppendEl().isVisible()).toBe(false); - expect(wrapper.find(ExpandButton).element).toMatchSnapshot(); + expect(wrapper.findComponent(ExpandButton).element).toMatchSnapshot(); }); }); @@ -81,7 +81,7 @@ describe('Expand button', () => { }); it('renders the expanded text', () => { - expect(wrapper.find(ExpandButton).text()).toContain(text.expanded); + expect(wrapper.findComponent(ExpandButton).text()).toContain(text.expanded); }); describe('when short text is provided', () => { @@ -98,13 +98,13 @@ describe('Expand button', () => { }); it('only renders expanded text', () => { - expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded); + expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.expanded); }); it('renders button after text', () => { expect(expanderPrependEl().isVisible()).toBe(false); expect(expanderAppendEl().isVisible()).toBe(true); - expect(wrapper.find(ExpandButton).element).toMatchSnapshot(); + expect(wrapper.findComponent(ExpandButton).element).toMatchSnapshot(); }); }); }); @@ -124,11 +124,11 @@ describe('Expand button', () => { }); it('clicking hides expanded text', async () => { - expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded); + expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.expanded); expanderAppendEl().trigger('click'); await nextTick(); - expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.expanded); + expect(wrapper.findComponent(ExpandButton).text().trim()).not.toBe(text.expanded); }); describe('when short text is provided', () => { @@ -145,11 +145,11 @@ describe('Expand button', () => { }); it('clicking reveals short text', async () => { - expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded); + expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.expanded); expanderAppendEl().trigger('click'); await nextTick(); - expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short); + expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.short); }); }); }); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js index b0e623520a8..3f4bfc86b67 100644 --- a/spec/frontend/vue_shared/components/file_icon_spec.js +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -6,7 +6,7 @@ import { FILE_SYMLINK_MODE } from '~/vue_shared/constants'; describe('File Icon component', () => { let wrapper; const findSvgIcon = () => wrapper.find('svg'); - const findGlIcon = () => wrapper.find(GlIcon); + const findGlIcon = () => wrapper.findComponent(GlIcon); const getIconName = () => findSvgIcon() .find('use') @@ -61,7 +61,7 @@ describe('File Icon component', () => { loading: true, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('should add a special class and a size class', () => { diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js index 62fb29c455c..f5a545891d5 100644 --- a/spec/frontend/vue_shared/components/file_row_spec.js +++ b/spec/frontend/vue_shared/components/file_row_spec.js @@ -119,7 +119,7 @@ describe('File row component', () => { level: 0, }); - expect(wrapper.find(FileHeader).exists()).toBe(true); + expect(wrapper.findComponent(FileHeader).exists()).toBe(true); }); it('matches the current route against encoded file URL', () => { @@ -164,6 +164,6 @@ describe('File row component', () => { level: 0, }); - expect(wrapper.find(FileIcon).props('submodule')).toBe(submodule); + expect(wrapper.findComponent(FileIcon).props('submodule')).toBe(submodule); }); }); diff --git a/spec/frontend/vue_shared/components/file_tree_spec.js b/spec/frontend/vue_shared/components/file_tree_spec.js index 39a7c7a2b3a..e8818e09dc0 100644 --- a/spec/frontend/vue_shared/components/file_tree_spec.js +++ b/spec/frontend/vue_shared/components/file_tree_spec.js @@ -25,8 +25,8 @@ describe('File Tree component', () => { }); }; - const findFileRow = () => wrapper.find(MockFileRow); - const findChildrenTrees = () => wrapper.findAll(FileTree).wrappers.slice(1); + const findFileRow = () => wrapper.findComponent(MockFileRow); + const findChildrenTrees = () => wrapper.findAllComponents(FileTree).wrappers.slice(1); const findChildrenTreeProps = () => findChildrenTrees().map((x) => ({ ...x.props(), diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index e44bc8771f5..1b9ca8e6092 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -88,10 +88,10 @@ describe('FilteredSearchBarRoot', () => { expect(wrapper.vm.filterValue).toEqual([]); expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]); expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending); - expect(wrapper.find(GlButtonGroup).exists()).toBe(true); - expect(wrapper.find(GlButton).exists()).toBe(true); - expect(wrapper.find(GlDropdown).exists()).toBe(true); - expect(wrapper.find(GlDropdownItem).exists()).toBe(true); + expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true); + expect(wrapper.findComponent(GlButton).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); }); it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => { @@ -99,10 +99,10 @@ describe('FilteredSearchBarRoot', () => { expect(wrapperNoSort.vm.filterValue).toEqual([]); expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined); - expect(wrapperNoSort.find(GlButtonGroup).exists()).toBe(false); - expect(wrapperNoSort.find(GlButton).exists()).toBe(false); - expect(wrapperNoSort.find(GlDropdown).exists()).toBe(false); - expect(wrapperNoSort.find(GlDropdownItem).exists()).toBe(false); + expect(wrapperNoSort.findComponent(GlButtonGroup).exists()).toBe(false); + expect(wrapperNoSort.findComponent(GlButton).exists()).toBe(false); + expect(wrapperNoSort.findComponent(GlDropdown).exists()).toBe(false); + expect(wrapperNoSort.findComponent(GlDropdownItem).exists()).toBe(false); }); }); @@ -217,7 +217,7 @@ describe('FilteredSearchBarRoot', () => { it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => { wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); - wrapper.find(GlFilteredSearch).vm.$emit('clear'); + wrapper.findComponent(GlFilteredSearch).vm.$emit('clear'); await nextTick(); expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); @@ -362,7 +362,7 @@ describe('FilteredSearchBarRoot', () => { it('calls `blurSearchInput` method to remove focus from filter input field', () => { jest.spyOn(wrapper.vm, 'blurSearchInput'); - wrapper.find(GlFilteredSearch).vm.$emit('submit', mockFilters); + wrapper.findComponent(GlFilteredSearch).vm.$emit('submit', mockFilters); expect(wrapper.vm.blurSearchInput).toHaveBeenCalled(); }); @@ -392,7 +392,7 @@ describe('FilteredSearchBarRoot', () => { }); it('renders gl-filtered-search component', () => { - const glFilteredSearchEl = wrapper.find(GlFilteredSearch); + const glFilteredSearchEl = wrapper.findComponent(GlFilteredSearch); expect(glFilteredSearchEl.props('placeholder')).toBe('Filter requirements'); expect(glFilteredSearchEl.props('availableTokens')).toEqual(mockAvailableTokens); @@ -404,8 +404,10 @@ describe('FilteredSearchBarRoot', () => { showCheckbox: true, }); - expect(wrapperWithCheckbox.find(GlFormCheckbox).exists()).toBe(true); - expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).not.toBeDefined(); + expect(wrapperWithCheckbox.findComponent(GlFormCheckbox).exists()).toBe(true); + expect( + wrapperWithCheckbox.findComponent(GlFormCheckbox).attributes('checked'), + ).not.toBeDefined(); wrapperWithCheckbox.destroy(); @@ -414,7 +416,7 @@ describe('FilteredSearchBarRoot', () => { checkboxChecked: true, }); - expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).toBe('true'); + expect(wrapperWithCheckbox.findComponent(GlFormCheckbox).attributes('checked')).toBe('true'); wrapperWithCheckbox.destroy(); }); @@ -448,7 +450,7 @@ describe('FilteredSearchBarRoot', () => { await nextTick(); - expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := Direct'); + expect(wrapperFullMount.findComponent(GlDropdownItem).text()).toBe('Membership := Direct'); wrapperFullMount.destroy(); }); @@ -466,20 +468,20 @@ describe('FilteredSearchBarRoot', () => { await nextTick(); - expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := exclude'); + expect(wrapperFullMount.findComponent(GlDropdownItem).text()).toBe('Membership := exclude'); wrapperFullMount.destroy(); }); }); it('renders sort dropdown component', () => { - expect(wrapper.find(GlButtonGroup).exists()).toBe(true); - expect(wrapper.find(GlDropdown).exists()).toBe(true); - expect(wrapper.find(GlDropdown).props('text')).toBe(mockSortOptions[0].title); + expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdown).props('text')).toBe(mockSortOptions[0].title); }); it('renders sort dropdown items', () => { - const dropdownItemsEl = wrapper.findAll(GlDropdownItem); + const dropdownItemsEl = wrapper.findAllComponents(GlDropdownItem); expect(dropdownItemsEl).toHaveLength(mockSortOptions.length); expect(dropdownItemsEl.at(0).text()).toBe(mockSortOptions[0].title); @@ -488,7 +490,7 @@ describe('FilteredSearchBarRoot', () => { }); it('renders sort direction button', () => { - const sortButtonEl = wrapper.find(GlButton); + const sortButtonEl = wrapper.findComponent(GlButton); expect(sortButtonEl.attributes('title')).toBe('Sort direction: Descending'); expect(sortButtonEl.props('icon')).toBe('sort-highest'); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index 86d1f21fd04..a6713b7e7e4 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -66,12 +66,14 @@ export const mockMilestones = [ export const mockCrmContacts = [ { + __typename: 'CustomerRelationsContact', id: 'gid://gitlab/CustomerRelations::Contact/1', firstName: 'John', lastName: 'Smith', email: 'john@smith.com', }, { + __typename: 'CustomerRelationsContact', id: 'gid://gitlab/CustomerRelations::Contact/2', firstName: 'Andy', lastName: 'Green', @@ -81,10 +83,12 @@ export const mockCrmContacts = [ export const mockCrmOrganizations = [ { + __typename: 'CustomerRelationsOrganization', id: 'gid://gitlab/CustomerRelations::Organization/1', name: 'First Org Ltd.', }, { + __typename: 'CustomerRelationsOrganization', id: 'gid://gitlab/CustomerRelations::Organization/2', name: 'Organizer S.p.a.', }, @@ -102,11 +106,9 @@ export const mockProjectCrmContactsQueryResponse = { __typename: 'CustomerRelationsContactConnection', nodes: [ { - __typename: 'CustomerRelationsContact', ...mockCrmContacts[0], }, { - __typename: 'CustomerRelationsContact', ...mockCrmContacts[1], }, ], @@ -128,11 +130,9 @@ export const mockProjectCrmOrganizationsQueryResponse = { __typename: 'CustomerRelationsOrganizationConnection', nodes: [ { - __typename: 'CustomerRelationsOrganization', ...mockCrmOrganizations[0], }, { - __typename: 'CustomerRelationsOrganization', ...mockCrmOrganizations[1], }, ], diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 3f24d5df858..302dfabffb2 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -195,7 +195,7 @@ describe('AuthorToken', () => { }); await nextTick(); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator" @@ -207,7 +207,7 @@ describe('AuthorToken', () => { it('renders token value with correct avatarUrl from author object', async () => { const getAvatarEl = () => - wrapper.findAll(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar); + wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar); wrapper = createComponent({ value: { data: mockAuthors[0].username }, @@ -252,7 +252,7 @@ describe('AuthorToken', () => { await activateSuggestionsList(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultAuthors.length + currentUserLength); defaultAuthors.forEach((label, index) => { @@ -266,12 +266,12 @@ describe('AuthorToken', () => { config: { ...mockAuthorToken, defaultAuthors: [] }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => { @@ -283,7 +283,7 @@ describe('AuthorToken', () => { await activateSuggestionsList(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(2 + currentUserLength); expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index 7b495ec9bee..1de35daa3a5 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -114,7 +114,7 @@ describe('BranchToken', () => { describe('template', () => { const defaultBranches = DEFAULT_NONE_ANY; async function showSuggestions() { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); @@ -133,11 +133,11 @@ describe('BranchToken', () => { }); it('renders gl-filtered-search-token component', () => { - expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + expect(wrapper.findComponent(GlFilteredSearchToken).exists()).toBe(true); }); it('renders token item when value is selected', () => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); expect(tokenSegments.at(2).text()).toBe(mockBranches[0].name); @@ -150,7 +150,7 @@ describe('BranchToken', () => { stubs: { Portal: true }, }); await showSuggestions(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultBranches.length); defaultBranches.forEach((branch, index) => { @@ -166,8 +166,8 @@ describe('BranchToken', () => { }); await showSuggestions(); - expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders no suggestions as default', async () => { @@ -177,7 +177,7 @@ describe('BranchToken', () => { stubs: { Portal: true }, }); await showSuggestions(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(0); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js index 157e021fc60..c9879987931 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js @@ -195,7 +195,7 @@ describe('CrmContactToken', () => { value: { data: '1' }, }); - const baseTokenEl = wrapper.find(BaseToken); + const baseTokenEl = wrapper.findComponent(BaseToken); expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ @@ -210,7 +210,7 @@ describe('CrmContactToken', () => { value: { data: `${getIdFromGraphQLId(contact.id)}` }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); // Contact, =, Contact name expect(tokenSegments.at(2).text()).toBe(`${contact.firstName} ${contact.lastName}`); // Contact name @@ -222,12 +222,12 @@ describe('CrmContactToken', () => { config: { ...mockCrmContactToken, defaultContacts }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultContacts.length); defaultContacts.forEach((contact, index) => { @@ -241,13 +241,13 @@ describe('CrmContactToken', () => { config: { ...mockCrmContactToken, defaultContacts: [] }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders `DEFAULT_NONE_ANY` as default suggestions', () => { @@ -256,11 +256,11 @@ describe('CrmContactToken', () => { config: { ...mockCrmContactToken }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length); DEFAULT_NONE_ANY.forEach((contact, index) => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js index 977f8bbef61..16333b052e6 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js @@ -194,7 +194,7 @@ describe('CrmOrganizationToken', () => { value: { data: '1' }, }); - const baseTokenEl = wrapper.find(BaseToken); + const baseTokenEl = wrapper.findComponent(BaseToken); expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ @@ -209,7 +209,7 @@ describe('CrmOrganizationToken', () => { value: { data: `${getIdFromGraphQLId(organization.id)}` }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); // Organization, =, Organization name expect(tokenSegments.at(2).text()).toBe(organization.name); // Organization name @@ -221,12 +221,12 @@ describe('CrmOrganizationToken', () => { config: { ...mockCrmOrganizationToken, defaultOrganizations }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultOrganizations.length); defaultOrganizations.forEach((organization, index) => { @@ -240,13 +240,13 @@ describe('CrmOrganizationToken', () => { config: { ...mockCrmOrganizationToken, defaultOrganizations: [] }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders `DEFAULT_NONE_ANY` as default suggestions', () => { @@ -255,11 +255,11 @@ describe('CrmOrganizationToken', () => { config: { ...mockCrmOrganizationToken }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length); DEFAULT_NONE_ANY.forEach((organization, index) => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index dcb0d095b1b..bf4a6eb7635 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -135,14 +135,16 @@ describe('EmojiToken', () => { }); it('renders gl-filtered-search-token component', () => { - expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + expect(wrapper.findComponent(GlFilteredSearchToken).exists()).toBe(true); }); it('renders token item when value is selected', () => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbsup" - expect(tokenSegments.at(2).find(GlEmoji).attributes('data-name')).toEqual('thumbsup'); + expect(tokenSegments.at(2).findComponent(GlEmoji).attributes('data-name')).toEqual( + 'thumbsup', + ); }); it('renders provided defaultEmojis as suggestions', async () => { @@ -151,12 +153,12 @@ describe('EmojiToken', () => { config: { ...mockReactionEmojiToken, defaultEmojis }, stubs: { Portal: true, GlEmoji }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultEmojis.length); defaultEmojis.forEach((emoji, index) => { @@ -170,13 +172,13 @@ describe('EmojiToken', () => { config: { ...mockReactionEmojiToken, defaultEmojis: [] }, stubs: { Portal: true, GlEmoji }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders `DEFAULT_LABEL_NONE` and `DEFAULT_LABEL_ANY` as default suggestions', async () => { @@ -185,12 +187,12 @@ describe('EmojiToken', () => { config: { ...mockReactionEmojiToken }, stubs: { Portal: true, GlEmoji }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(2); expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_NONE.text); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index 51161a1a0ef..01e281884ed 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -156,7 +156,7 @@ describe('LabelToken', () => { }); it('renders base-token component', () => { - const baseTokenEl = wrapper.find(BaseToken); + const baseTokenEl = wrapper.findComponent(BaseToken); expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ @@ -166,7 +166,7 @@ describe('LabelToken', () => { }); it('renders token item when value is selected', () => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label" expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label" @@ -181,12 +181,12 @@ describe('LabelToken', () => { config: { ...mockLabelToken, defaultLabels }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultLabels.length); defaultLabels.forEach((label, index) => { @@ -200,13 +200,13 @@ describe('LabelToken', () => { config: { ...mockLabelToken, defaultLabels: [] }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders `DEFAULT_NONE_ANY` as default suggestions', () => { @@ -215,11 +215,11 @@ describe('LabelToken', () => { config: { ...mockLabelToken }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length); DEFAULT_NONE_ANY.forEach((label, index) => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index 7c545f76c0b..f71ba51fc5b 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -155,11 +155,11 @@ describe('MilestoneToken', () => { }); it('renders gl-filtered-search-token component', () => { - expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + expect(wrapper.findComponent(GlFilteredSearchToken).exists()).toBe(true); }); it('renders token item when value is selected', () => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"' expect(tokenSegments.at(2).text()).toBe(`%${mockRegularMilestone.title}`); // "4.0 RC1" @@ -171,12 +171,12 @@ describe('MilestoneToken', () => { config: { ...mockMilestoneToken, defaultMilestones }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(defaultMilestones.length); defaultMilestones.forEach((milestone, index) => { @@ -190,13 +190,13 @@ describe('MilestoneToken', () => { config: { ...mockMilestoneToken, defaultMilestones: [] }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); - expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); it('renders `DEFAULT_MILESTONES` as default suggestions', async () => { @@ -205,12 +205,12 @@ describe('MilestoneToken', () => { config: { ...mockMilestoneToken }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); expect(suggestions).toHaveLength(DEFAULT_MILESTONES.length); DEFAULT_MILESTONES.forEach((milestone, index) => { diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js index b180e8c12dd..6699ae5fb69 100644 --- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js +++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js @@ -26,13 +26,44 @@ describe('GitlabVersionCheck', () => { wrapper = shallowMount(GitlabVersionCheck); }; + const dummyGon = { + relative_url_root: '/', + }; + + let originalGon; + afterEach(() => { wrapper.destroy(); mock.restore(); + window.gon = originalGon; }); const findGlBadge = () => wrapper.findComponent(GlBadge); + describe.each` + root | description + ${'/'} | ${'not used (uses its own (sub)domain)'} + ${'/gitlab'} | ${'custom path'} + ${'/service/gitlab'} | ${'custom path with 2 depth'} + `('path for version_check.json', ({ root, description }) => { + describe(`when relative url is ${description}: ${root}`, () => { + beforeEach(async () => { + originalGon = window.gon; + window.gon = { ...dummyGon }; + window.gon.relative_url_root = root; + createComponent(defaultResponse); + await waitForPromises(); // Ensure we wrap up the axios call + }); + + it('reflects the relative url setting', () => { + expect(mock.history.get.length).toBe(1); + + const pathRegex = new RegExp(`^${root}`); + expect(mock.history.get[0].url).toMatch(pathRegex); + }); + }); + }); + describe('template', () => { describe.each` description | mockResponse | renders diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js index c0a6588833e..2dcd91f737f 100644 --- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -59,7 +59,7 @@ describe('GlModalVuex', () => { default: `<div>${TEST_SLOT}</div>`, }, }); - const glModal = wrapper.find(GlModal); + const glModal = wrapper.findComponent(GlModal); expect(glModal.props('modalId')).toBe(TEST_MODAL_ID); expect(glModal.text()).toContain(TEST_SLOT); @@ -76,7 +76,7 @@ describe('GlModalVuex', () => { okVariant, }, }); - const glModal = wrapper.find(GlModal); + const glModal = wrapper.findComponent(GlModal); expect(glModal.attributes('title')).toEqual(title); expect(glModal.attributes('oktitle')).toEqual(title); @@ -90,7 +90,7 @@ describe('GlModalVuex', () => { listeners: { ok }, }); - const glModal = wrapper.find(GlModal); + const glModal = wrapper.findComponent(GlModal); glModal.vm.$emit('ok'); expect(ok).toHaveBeenCalledTimes(1); @@ -101,7 +101,7 @@ describe('GlModalVuex', () => { factory(); - const glModal = wrapper.find(GlModal); + const glModal = wrapper.findComponent(GlModal); glModal.vm.$emit('shown'); expect(actions.show).toHaveBeenCalledTimes(1); @@ -112,7 +112,7 @@ describe('GlModalVuex', () => { factory(); - const glModal = wrapper.find(GlModal); + const glModal = wrapper.findComponent(GlModal); glModal.vm.$emit('hidden'); expect(actions.hide).toHaveBeenCalledTimes(1); diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js index 64dce194327..6fd5ae0e946 100644 --- a/spec/frontend/vue_shared/components/help_popover_spec.js +++ b/spec/frontend/vue_shared/components/help_popover_spec.js @@ -7,8 +7,8 @@ describe('HelpPopover', () => { const title = 'popover <strong>title</strong>'; const content = 'popover <b>content</b>'; - const findQuestionButton = () => wrapper.find(GlButton); - const findPopover = () => wrapper.find(GlPopover); + const findQuestionButton = () => wrapper.findComponent(GlButton); + const findPopover = () => wrapper.findComponent(GlPopover); const createComponent = ({ props, ...opts } = {}) => { wrapper = mount(HelpPopover, { diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js index c0e8b719007..c63e46313b3 100644 --- a/spec/frontend/vue_shared/components/integration_help_text_spec.js +++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js @@ -30,9 +30,9 @@ describe('IntegrationHelpText component', () => { it('should use the gl components', () => { wrapper = createComponent(); - expect(wrapper.find(GlSprintf).exists()).toBe(true); - expect(wrapper.find(GlIcon).exists()).toBe(true); - expect(wrapper.find(GlLink).exists()).toBe(true); + expect(wrapper.findComponent(GlSprintf).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLink).exists()).toBe(true); }); it('should render the help text', () => { @@ -44,9 +44,9 @@ describe('IntegrationHelpText component', () => { it('should not use the gl-link and gl-icon components', () => { wrapper = createComponent({ message: 'Click nowhere!' }); - expect(wrapper.find(GlSprintf).exists()).toBe(true); - expect(wrapper.find(GlIcon).exists()).toBe(false); - expect(wrapper.find(GlLink).exists()).toBe(false); + expect(wrapper.findComponent(GlSprintf).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLink).exists()).toBe(false); }); it('should not render the link when start and end is not provided', () => { diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 85a135d2b89..50864a4bf25 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -76,7 +76,7 @@ describe('Markdown field component', () => { const getMarkdownButton = () => subject.find('.js-md'); const getListBulletedButton = () => subject.findAll('.js-md[title="Add a bullet list"]'); const getVideo = () => subject.find('video'); - const getAttachButton = () => subject.find('.button-attach-file'); + const getAttachButton = () => subject.findByTestId('button-attach-file'); const clickAttachButton = () => getAttachButton().trigger('click'); const findDropzone = () => subject.find('.div-dropzone'); const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader); @@ -232,13 +232,10 @@ describe('Markdown field component', () => { }); }); - it('should render attach a file button', () => { - expect(getAttachButton().text()).toBe('Attach a file'); - }); - it('should trigger dropzone when attach button is clicked', () => { expect(dropzoneSpy).not.toHaveBeenCalled(); + getAttachButton().trigger('click'); clickAttachButton(); expect(dropzoneSpy).toHaveBeenCalled(); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 67222cab247..9831908f806 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -21,7 +21,7 @@ describe('Markdown field header component', () => { const findWriteTab = () => wrapper.findByTestId('write-tab'); const findPreviewTab = () => wrapper.findByTestId('preview-tab'); const findToolbar = () => wrapper.findByTestId('md-header-toolbar'); - const findToolbarButtons = () => wrapper.findAll(ToolbarButton); + const findToolbarButtons = () => wrapper.findAllComponents(ToolbarButton); const findToolbarButtonByProp = (prop, value) => findToolbarButtons() .filter((button) => button.props(prop) === value) @@ -44,16 +44,16 @@ describe('Markdown field header component', () => { describe('markdown header buttons', () => { it('renders the buttons with the correct title', () => { const buttons = [ + 'Insert suggestion', 'Add bold text (⌘B)', 'Add italic text (⌘I)', 'Add strikethrough text (⌘⇧X)', 'Insert a quote', - 'Insert suggestion', 'Insert code', 'Add a link (⌘K)', 'Add a bullet list', 'Add a numbered list', - 'Add a task list', + 'Add a checklist', 'Add a collapsible section', 'Add a table', 'Go full screen', @@ -65,6 +65,13 @@ describe('Markdown field header component', () => { }); }); + it('renders "Attach a file or image" button using gl-button', () => { + const button = wrapper.findByTestId('button-attach-file'); + + expect(button.element.tagName).toBe('GL-BUTTON-STUB'); + expect(button.attributes('title')).toBe('Attach a file or image'); + }); + describe('when the user is on a non-Mac', () => { beforeEach(() => { delete window.gl.client.isMac; @@ -118,8 +125,8 @@ describe('Markdown field header component', () => { ), ]); - expect(wrapper.emitted('preview-markdown')).toBeFalsy(); - expect(wrapper.emitted('write-markdown')).toBeFalsy(); + expect(wrapper.emitted('preview-markdown')).toBeUndefined(); + expect(wrapper.emitted('write-markdown')).toBeUndefined(); }); it('blurs preview link after click', () => { diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js index 9944267cf24..9db1b779a04 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -38,13 +38,13 @@ describe('Suggestion Diff component', () => { wrapper.destroy(); }); - const findApplyButton = () => wrapper.find(ApplySuggestion); + const findApplyButton = () => wrapper.findComponent(ApplySuggestion); const findApplyBatchButton = () => wrapper.find('.js-apply-batch-btn'); const findAddToBatchButton = () => wrapper.find('.js-add-to-batch-btn'); const findRemoveFromBatchButton = () => wrapper.find('.js-remove-from-batch-btn'); const findHeader = () => wrapper.find('.js-suggestion-diff-header'); const findHelpButton = () => wrapper.find('.js-help-btn'); - const findLoading = () => wrapper.find(GlLoadingIcon); + const findLoading = () => wrapper.findComponent(GlLoadingIcon); it('renders a suggestion header', () => { createComponent(); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js index af27e953776..d84483c1663 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js @@ -71,7 +71,7 @@ describe('Suggestion Diff component', () => { }); it('renders a correct amount of suggestion diff rows', () => { - expect(wrapper.findAll(SuggestionDiffRow)).toHaveLength(3); + expect(wrapper.findAllComponents(SuggestionDiffRow)).toHaveLength(3); }); it.each` @@ -81,14 +81,14 @@ describe('Suggestion Diff component', () => { ${'addToBatch'} | ${[]} | ${[suggestionId]} ${'removeFromBatch'} | ${[]} | ${[suggestionId]} `('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => { - wrapper.find(SuggestionDiffHeader).vm.$emit(event, ...childArgs); + wrapper.findComponent(SuggestionDiffHeader).vm.$emit(event, ...childArgs); expect(wrapper.emitted(event)).toBeDefined(); expect(wrapper.emitted(event)).toEqual([args]); }); it('passes suggestion batch props to suggestion diff header', () => { - expect(wrapper.find(SuggestionDiffHeader).props()).toMatchObject({ + expect(wrapper.findComponent(SuggestionDiffHeader).props()).toMatchObject({ batchSuggestionsCount: 1, isBatched: true, isApplyingBatch: MOCK_DATA.suggestion.is_applying_batch, diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js index 19e4f2d8c92..82210e79799 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js @@ -26,7 +26,7 @@ describe('toolbar_button', () => { }); const getButtonShortcutsAttr = () => { - return wrapper.find(GlButton).attributes('data-md-shortcuts'); + return wrapper.findComponent(GlButton).attributes('data-md-shortcuts'); }; describe('keyboard shortcuts', () => { diff --git a/spec/frontend/vue_shared/components/memory_graph_spec.js b/spec/frontend/vue_shared/components/memory_graph_spec.js index 53b96bd1b98..ae8d5ff78ba 100644 --- a/spec/frontend/vue_shared/components/memory_graph_spec.js +++ b/spec/frontend/vue_shared/components/memory_graph_spec.js @@ -47,7 +47,7 @@ describe('MemoryGraph', () => { it('should draw container with chart', () => { expect(wrapper.element).toMatchSnapshot(); expect(wrapper.find('.memory-graph-container').exists()).toBe(true); - expect(wrapper.find(GlSparklineChart).exists()).toBe(true); + expect(wrapper.findComponent(GlSparklineChart).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js index 2cefa77b72d..1789610dba9 100644 --- a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js +++ b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js @@ -114,7 +114,7 @@ describe('Metric images tab', () => { await waitForPromises(); - expect(findModal().attributes('visible')).toBeFalsy(); + expect(findModal().attributes('visible')).toBeUndefined(); }); it('should add files and url when selected', async () => { diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js index c11b20a692e..2c14d65186b 100644 --- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js +++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js @@ -1,5 +1,12 @@ import { nextTick } from 'vue'; -import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui'; +import { + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NamespaceSelect, { i18n, @@ -7,7 +14,7 @@ import NamespaceSelect, { } from '~/vue_shared/components/namespace_select/namespace_select.vue'; import { userNamespaces, groupNamespaces } from './mock_data'; -const FLAT_NAMESPACES = [...groupNamespaces, ...userNamespaces]; +const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces]; const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST'; const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE }; @@ -31,6 +38,8 @@ describe('Namespace Select', () => { const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownText = () => findDropdown().props('text'); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findGroupDropdownItems = () => + wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem); const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text()); const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); @@ -59,7 +68,7 @@ describe('Namespace Select', () => { it('splits group and user namespaces', () => { const headers = findSectionHeaders(); - expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]); + expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]); }); it('does not render wrapper as full width', () => { @@ -89,18 +98,20 @@ describe('Namespace Select', () => { describe('with search', () => { it.each` - term | includeEmptyNamespace | expectedItems - ${''} | ${false} | ${[...groupNamespaces, ...userNamespaces]} - ${'sub'} | ${false} | ${[groupNamespaces[1]]} - ${'User'} | ${false} | ${[...userNamespaces]} - ${'User'} | ${true} | ${[...userNamespaces]} - ${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]} + term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems + ${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]} + ${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]} + ${'User'} | ${false} | ${true} | ${[...userNamespaces]} + ${'User'} | ${true} | ${true} | ${[...userNamespaces]} + ${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]} + ${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]} `( - 'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length', - async ({ term, includeEmptyNamespace, expectedItems }) => { + 'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length', + async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => { wrapper = createComponent({ includeEmptyNamespace, emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE, + shouldFilterNamespaces, }); search(term); @@ -114,6 +125,18 @@ describe('Namespace Select', () => { ); }); + describe('when search is typed in', () => { + it('emits `search` event', async () => { + wrapper = createComponent(); + + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo'); + + await nextTick(); + + expect(wrapper.emitted('search')).toEqual([['foo']]); + }); + }); + describe('with a selected namespace', () => { const selectedGroupIndex = 1; const selectedItem = groupNamespaces[selectedGroupIndex]; @@ -121,7 +144,8 @@ describe('Namespace Select', () => { beforeEach(() => { wrapper = createComponent(); - findDropdownItems().at(selectedGroupIndex).vm.$emit('click'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo'); + findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click'); }); it('sets the dropdown text', () => { @@ -132,6 +156,10 @@ describe('Namespace Select', () => { const args = [selectedItem]; expect(wrapper.emitted('select')).toEqual([args]); }); + + it('clears search', () => { + expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe(''); + }); }); describe('with an empty namespace option', () => { @@ -166,4 +194,33 @@ describe('Namespace Select', () => { expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow); }); }); + + describe('when `hasNextPageOfGroups` prop is `true`', () => { + it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => { + wrapper = createComponent({ hasNextPageOfGroups: true }); + + const intersectionObserver = wrapper.findComponent(GlIntersectionObserver); + + intersectionObserver.vm.$emit('appear'); + + expect(intersectionObserver.exists()).toBe(true); + expect(wrapper.emitted('load-more-groups')).toEqual([[]]); + }); + + describe('when `isLoadingMoreGroups` prop is `true`', () => { + it('renders a loading icon', () => { + wrapper = createComponent({ hasNextPageOfGroups: true, isLoadingMoreGroups: true }); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + }); + }); + + describe('when `isSearchLoading` prop is `true`', () => { + it('sets `isLoading` prop to `true`', () => { + wrapper = createComponent({ isSearchLoading: true }); + + expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js index 30a89fed12f..b1bec28bffb 100644 --- a/spec/frontend/vue_shared/components/navigation_tabs_spec.js +++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js @@ -44,7 +44,7 @@ describe('navigation tabs component', () => { }); it('should render tabs', () => { - expect(wrapper.findAll(GlTab)).toHaveLength(data.length); + expect(wrapper.findAllComponents(GlTab)).toHaveLength(data.length); }); it('should render active tab', () => { diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js index 99b65ca6937..17a62ae8a33 100644 --- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js +++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js @@ -6,10 +6,11 @@ import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue' describe('Issue Warning Component', () => { let wrapper; - const findIcon = (w = wrapper) => w.find(GlIcon); - const findLockedBlock = (w = wrapper) => w.find({ ref: 'locked' }); - const findConfidentialBlock = (w = wrapper) => w.find({ ref: 'confidential' }); - const findLockedAndConfidentialBlock = (w = wrapper) => w.find({ ref: 'lockedAndConfidential' }); + const findIcon = (w = wrapper) => w.findComponent(GlIcon); + const findLockedBlock = (w = wrapper) => w.findComponent({ ref: 'locked' }); + const findConfidentialBlock = (w = wrapper) => w.findComponent({ ref: 'confidential' }); + const findLockedAndConfidentialBlock = (w = wrapper) => + w.findComponent({ ref: 'lockedAndConfidential' }); const createComponent = (props) => shallowMount(NoteableWarning, { @@ -73,7 +74,7 @@ describe('Issue Warning Component', () => { }); it('renders warning icon', () => { - expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); }); it('does not render information about locked noteable', () => { @@ -99,7 +100,7 @@ describe('Issue Warning Component', () => { }); it('does not render warning icon', () => { - expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); }); it('does not render information about locked noteable', () => { diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js index f951cfd5cd9..b86c8946e96 100644 --- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js @@ -14,7 +14,7 @@ const getters = { describe('Issue placeholder note component', () => { let wrapper; - const findNote = () => wrapper.find({ ref: 'note' }); + const findNote = () => wrapper.findComponent({ ref: 'note' }); const createComponent = (isIndividual = false, propsData = {}) => { wrapper = shallowMount(IssuePlaceholderNote, { diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js index 51a936c0509..c0c3c4a9729 100644 --- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js +++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js @@ -92,15 +92,15 @@ describe('AlertManagementEmptyState', () => { const EmptyState = () => wrapper.find('.empty-state'); const ItemsTable = () => wrapper.find('.gl-table'); - const ErrorAlert = () => wrapper.find(GlAlert); - const Pagination = () => wrapper.find(GlPagination); - const Tabs = () => wrapper.find(GlTabs); + const ErrorAlert = () => wrapper.findComponent(GlAlert); + const Pagination = () => wrapper.findComponent(GlPagination); + const Tabs = () => wrapper.findComponent(GlTabs); const ActionButton = () => wrapper.find('.header-actions > button'); - const Filters = () => wrapper.find(FilteredSearchBar); - const findPagination = () => wrapper.find(GlPagination); - const findStatusFilterTabs = () => wrapper.findAll(GlTab); - const findStatusTabs = () => wrapper.find(GlTabs); - const findStatusFilterBadge = () => wrapper.findAll(GlBadge); + const Filters = () => wrapper.findComponent(FilteredSearchBar); + const findPagination = () => wrapper.findComponent(GlPagination); + const findStatusFilterTabs = () => wrapper.findAllComponents(GlTab); + const findStatusTabs = () => wrapper.findComponent(GlTabs); + const findStatusFilterBadge = () => wrapper.findAllComponents(GlBadge); describe('Snowplow tracking', () => { beforeEach(() => { @@ -213,7 +213,7 @@ describe('AlertManagementEmptyState', () => { }); it('should render pagination', () => { - expect(wrapper.find(GlPagination).exists()).toBe(true); + expect(wrapper.findComponent(GlPagination).exists()).toBe(true); }); describe('prevPage', () => { diff --git a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js index 08119dee8af..b3be2f8a775 100644 --- a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js +++ b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js @@ -64,7 +64,7 @@ describe('Pagination bar', () => { }, }); - expect(wrapper.find(GlDropdown).find('button').text()).toMatchInterpolatedText( + expect(wrapper.findComponent(GlDropdown).find('button').text()).toMatchInterpolatedText( `${CURRENT_PAGE_SIZE} items per page`, ); }); diff --git a/spec/frontend/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js index 83f1e2844f9..d444ad7a733 100644 --- a/spec/frontend/vue_shared/components/pagination_links_spec.js +++ b/spec/frontend/vue_shared/components/pagination_links_spec.js @@ -41,7 +41,7 @@ describe('Pagination links component', () => { beforeEach(() => { createComponent(); - glPagination = wrapper.find(GlPagination); + glPagination = wrapper.findComponent(GlPagination); }); afterEach(() => { diff --git a/spec/frontend/vue_shared/components/project_avatar_spec.js b/spec/frontend/vue_shared/components/project_avatar_spec.js index d55f3127a74..af828fbca51 100644 --- a/spec/frontend/vue_shared/components/project_avatar_spec.js +++ b/spec/frontend/vue_shared/components/project_avatar_spec.js @@ -42,6 +42,42 @@ describe('ProjectAvatar', () => { }); }); + describe('with `projectId` prop', () => { + const validatorFunc = ProjectAvatar.props.projectId.validator; + + it('prop validators return true for valid types', () => { + expect(validatorFunc(1)).toBe(true); + expect(validatorFunc('gid://gitlab/Project/1')).toBe(true); + }); + + it('prop validators return false for invalid types', () => { + expect(validatorFunc('1')).toBe(false); + }); + + it('renders GlAvatar with `entityId` 0 when `projectId` is not informed', () => { + createComponent({ props: { projectId: undefined } }); + + const avatar = findGlAvatar(); + expect(avatar.props('entityId')).toBe(0); + }); + + it('renders GlAvatar with specified `entityId` when `projectId` is a Number', () => { + const mockProjectId = 1; + createComponent({ props: { projectId: mockProjectId } }); + + const avatar = findGlAvatar(); + expect(avatar.props('entityId')).toBe(mockProjectId); + }); + + it('renders GlAvatar with specified `entityId` when `projectId` is a gid String', () => { + const mockProjectId = 'gid://gitlab/Project/1'; + createComponent({ props: { projectId: mockProjectId } }); + + const avatar = findGlAvatar(); + expect(avatar.props('entityId')).toBe(1); + }); + }); + describe('with `projectAvatarUrl` prop', () => { it('renders GlAvatar with specified `src` prop', () => { const mockProjectAvatarUrl = 'https://gitlab.com'; diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js index 397ab2254b9..4e0c318c84e 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -56,6 +56,7 @@ describe('ProjectListItem component', () => { expect(avatar.exists()).toBe(true); expect(avatar.props()).toMatchObject({ + projectId: project.id, projectAvatarUrl: '', projectName: project.name_with_namespace, }); diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js index 379e60c1b2d..a0832dd7030 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -15,7 +15,7 @@ describe('ProjectSelector component', () => { let selected = []; selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); - const findSearchInput = () => wrapper.find(GlSearchBoxByType).find('input'); + const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType).find('input'); const findLegendText = () => wrapper.find('[data-testid="legend-text"]').text(); const search = (query) => { const searchInput = findSearchInput(); @@ -65,14 +65,14 @@ describe('ProjectSelector component', () => { it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => { jest.spyOn(vm, '$emit').mockImplementation(() => {}); - wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached'); + wrapper.findComponent(GlInfiniteScroll).vm.$emit('bottomReached'); expect(vm.$emit).toHaveBeenCalledWith('bottomReached'); }); it(`triggers a "projectClicked" event when a project is clicked`, () => { jest.spyOn(vm, '$emit').mockImplementation(() => {}); - wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults)); + wrapper.findComponent(ProjectListItem).vm.$emit('click', head(searchResults)); expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults)); }); diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js index 3a2ea263a05..8f19f0ea14d 100644 --- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js +++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js @@ -22,7 +22,7 @@ describe('Package code instruction', () => { }); } - const findCopyButton = () => wrapper.find(ClipboardButton); + const findCopyButton = () => wrapper.findComponent(ClipboardButton); const findInputElement = () => wrapper.find('[data-testid="instruction-input"]'); const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]'); diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js index 3134e0d3e21..ebc9816f983 100644 --- a/spec/frontend/vue_shared/components/registry/details_row_spec.js +++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js @@ -5,7 +5,7 @@ import component from '~/vue_shared/components/registry/details_row.vue'; describe('DetailsRow', () => { let wrapper; - const findIcon = () => wrapper.find(GlIcon); + const findIcon = () => wrapper.findComponent(GlIcon); const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); const mountComponent = (props) => { diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js index f146f87342f..947520567e6 100644 --- a/spec/frontend/vue_shared/components/registry/history_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js @@ -27,8 +27,8 @@ describe('History Item', () => { wrapper = null; }); - const findTimelineEntry = () => wrapper.find(TimelineEntryItem); - const findGlIcon = () => wrapper.find(GlIcon); + const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem); + const findGlIcon = () => wrapper.findComponent(GlIcon); const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); const findBodySlot = () => wrapper.find('[data-testid="body-slot"]'); diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js index 6e9abb2bfb3..b941eb77c32 100644 --- a/spec/frontend/vue_shared/components/registry/list_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -13,7 +13,7 @@ describe('list item', () => { const findRightSecondarySlot = () => wrapper.find('[data-testid="right-secondary"]'); const findRightActionSlot = () => wrapper.find('[data-testid="right-action"]'); const findDetailsSlot = (name) => wrapper.find(`[data-testid="${name}"]`); - const findToggleDetailsButton = () => wrapper.find(GlButton); + const findToggleDetailsButton = () => wrapper.findComponent(GlButton); const mountComponent = (propsData, slots) => { wrapper = shallowMount(component, { diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js index e4abdc15fd5..a04e1e237d4 100644 --- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js @@ -24,10 +24,10 @@ describe('Metadata Item', () => { wrapper = null; }); - const findIcon = () => wrapper.find(GlIcon); - const findLink = (w = wrapper) => w.find(GlLink); + const findIcon = () => wrapper.findComponent(GlIcon); + const findLink = (w = wrapper) => w.findComponent(GlLink); const findText = () => wrapper.find('[data-testid="metadata-item-text"]'); - const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate); + const findTooltipOnTruncate = (w = wrapper) => w.findComponent(TooltipOnTruncate); const findTextTooltip = () => wrapper.find('[data-testid="text-tooltip-container"]'); describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', (size) => { diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js index 20716e79a04..70f4693ae81 100644 --- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js +++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js @@ -6,9 +6,9 @@ import component from '~/vue_shared/components/registry/registry_search.vue'; describe('Registry Search', () => { let wrapper; - const findPackageListSorting = () => wrapper.find(GlSorting); - const findSortingItems = () => wrapper.findAll(GlSortingItem); - const findFilteredSearch = () => wrapper.find(GlFilteredSearch); + const findPackageListSorting = () => wrapper.findComponent(GlSorting); + const findSortingItems = () => wrapper.findAllComponents(GlSortingItem); + const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); const defaultProps = { filters: [], diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js index b62676b35be..efb57ddd310 100644 --- a/spec/frontend/vue_shared/components/registry/title_area_spec.js +++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js @@ -199,7 +199,7 @@ describe('title area', () => { const message = findInfoMessages().at(0); - expect(message.find(GlLink).attributes('href')).toBe('bar'); + expect(message.findComponent(GlLink).attributes('href')).toBe('bar'); expect(message.text()).toBe('foo link'); }); diff --git a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js new file mode 100644 index 00000000000..5d96fe27676 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js @@ -0,0 +1,41 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import { formatDate } from '~/lib/utils/datetime_utility'; +import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; + +describe('RichTimestampTooltip', () => { + const currentDate = new Date(); + const mockRawTimestamp = currentDate.toISOString(); + const mockTimestamp = formatDate(currentDate); + let wrapper; + + const createComponent = ({ + target = 'some-element', + rawTimestamp = mockRawTimestamp, + timestampTypeText = 'Created', + } = {}) => { + wrapper = shallowMountExtended(RichTimestampTooltip, { + propsData: { + target, + rawTimestamp, + timestampTypeText, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the tooltip text header', () => { + expect(wrapper.findByTestId('header-text').text()).toBe('Created just now'); + }); + + it('renders the tooltip text body', () => { + expect(wrapper.findByTestId('body-text').text()).toBe(mockTimestamp); + }); +}); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js index a38dcd626f4..7c5fc63856a 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js @@ -166,7 +166,7 @@ describe('RunnerInstructionsModal component', () => { }); it('sets the focus on the default selected platform', () => { - const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' }); + const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' }); findOsxPlatformButton().element.focus = jest.fn(); @@ -234,14 +234,14 @@ describe('RunnerInstructionsModal component', () => { MockResizeObserver.mockResize('xs'); await nextTick(); - expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy(); + expect(findPlatformButtonGroup().attributes('vertical')).toEqual('true'); }); it('to a non-xs viewport', async () => { MockResizeObserver.mockResize('sm'); await nextTick(); - expect(findPlatformButtonGroup().props('vertical')).toBeFalsy(); + expect(findPlatformButtonGroup().props('vertical')).toBeUndefined(); }); }); }); diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js index 71ebe561def..c5672bc28cc 100644 --- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js +++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js @@ -50,7 +50,7 @@ describe('Merge request artifact Download', () => { return createMockApollo(requestHandlers); }; - const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown); + const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js index ae86106d86e..08d3d5b19d4 100644 --- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js +++ b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js @@ -17,9 +17,9 @@ describe('HelpIcon component', () => { }); }; - const findLink = () => wrapper.find(GlLink); - const findPopover = () => wrapper.find(GlPopover); - const findPopoverTarget = () => wrapper.find({ ref: 'discoverProjectSecurity' }); + const findLink = () => wrapper.findComponent(GlLink); + const findPopover = () => wrapper.findComponent(GlPopover); + const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' }); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js index f213e37cbc1..9b1316677d7 100644 --- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js @@ -173,15 +173,15 @@ describe('IssuableMoveDropdown', () => { }); describe('template', () => { - const findDropdownEl = () => wrapper.find(GlDropdown); + const findDropdownEl = () => wrapper.findComponent(GlDropdown); it('renders collapsed state element with icon', () => { const collapsedEl = wrapper.find('[data-testid="move-collapsed"]'); expect(collapsedEl.exists()).toBe(true); expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle); - expect(collapsedEl.find(GlIcon).exists()).toBe(true); - expect(collapsedEl.find(GlIcon).props('name')).toBe('arrow-right'); + expect(collapsedEl.findComponent(GlIcon).exists()).toBe(true); + expect(collapsedEl.findComponent(GlIcon).props('name')).toBe('arrow-right'); }); describe('gl-dropdown component', () => { @@ -191,7 +191,7 @@ describe('IssuableMoveDropdown', () => { }); it('renders gl-dropdown-form component', () => { - expect(findDropdownEl().find(GlDropdownForm).exists()).toBe(true); + expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true); }); it('renders header element', () => { @@ -199,11 +199,11 @@ describe('IssuableMoveDropdown', () => { expect(headerEl.exists()).toBe(true); expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle); - expect(headerEl.find(GlButton).props('icon')).toBe('close'); + expect(headerEl.findComponent(GlButton).props('icon')).toBe('close'); }); it('renders gl-search-box-by-type component', () => { - const searchEl = findDropdownEl().find(GlSearchBoxByType); + const searchEl = findDropdownEl().findComponent(GlSearchBoxByType); expect(searchEl.exists()).toBe(true); expect(searchEl.attributes()).toMatchObject({ @@ -221,7 +221,7 @@ describe('IssuableMoveDropdown', () => { await nextTick(); - expect(findDropdownEl().find(GlLoadingIcon).exists()).toBe(true); + expect(findDropdownEl().findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders gl-dropdown-item components for available projects', async () => { @@ -234,7 +234,7 @@ describe('IssuableMoveDropdown', () => { await nextTick(); - const dropdownItems = wrapper.findAll(GlDropdownItem); + const dropdownItems = wrapper.findAllComponents(GlDropdownItem); expect(dropdownItems).toHaveLength(mockProjects.length); expect(dropdownItems.at(0).props()).toMatchObject({ @@ -285,7 +285,7 @@ describe('IssuableMoveDropdown', () => { }); it('renders gl-button within footer', async () => { - const moveButtonEl = wrapper.find('[data-testid="footer"]').find(GlButton); + const moveButtonEl = wrapper.find('[data-testid="footer"]').findComponent(GlButton); expect(moveButtonEl.text()).toBe('Move'); expect(moveButtonEl.attributes('disabled')).toBe('true'); @@ -299,7 +299,7 @@ describe('IssuableMoveDropdown', () => { await nextTick(); expect( - wrapper.find('[data-testid="footer"]').find(GlButton).attributes('disabled'), + wrapper.find('[data-testid="footer"]').findComponent(GlButton).attributes('disabled'), ).not.toBeDefined(); }); }); @@ -308,7 +308,7 @@ describe('IssuableMoveDropdown', () => { it('collapsed state element emits `toggle-collapse` event on component when clicked', () => { wrapper.find('[data-testid="move-collapsed"]').trigger('click'); - expect(wrapper.emitted('toggle-collapse')).toBeTruthy(); + expect(wrapper.emitted('toggle-collapse')).toHaveLength(1); }); it('gl-dropdown component calls `fetchProjects` on `shown` event', () => { @@ -337,11 +337,11 @@ describe('IssuableMoveDropdown', () => { it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => { findDropdownEl().vm.$emit('hide'); - expect(wrapper.emitted('dropdown-close')).toBeTruthy(); + expect(wrapper.emitted('dropdown-close')).toHaveLength(1); }); it('close icon in dropdown header closes the dropdown when clicked', () => { - wrapper.find('[data-testid="header"]').find(GlButton).vm.$emit('click', mockEvent); + wrapper.find('[data-testid="header"]').findComponent(GlButton).vm.$emit('click', mockEvent); expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled(); }); @@ -355,7 +355,7 @@ describe('IssuableMoveDropdown', () => { await nextTick(); - wrapper.findAll(GlDropdownItem).at(0).vm.$emit('click', mockEvent); + wrapper.findAllComponents(GlDropdownItem).at(0).vm.$emit('click', mockEvent); expect(wrapper.vm.selectedProject).toBe(mockProjects[0]); }); @@ -369,10 +369,10 @@ describe('IssuableMoveDropdown', () => { await nextTick(); - wrapper.find('[data-testid="footer"]').find(GlButton).vm.$emit('click'); + wrapper.find('[data-testid="footer"]').findComponent(GlButton).vm.$emit('click'); expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled(); - expect(wrapper.emitted('move-issuable')).toBeTruthy(); + expect(wrapper.emitted('move-issuable')).toHaveLength(1); expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js index c05513a6d5f..c0e5408e1bd 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js @@ -33,9 +33,9 @@ describe('DropdownButton', () => { wrapper.destroy(); }); - const findDropdownButton = () => wrapper.find(GlButton); + const findDropdownButton = () => wrapper.findComponent(GlButton); const findDropdownText = () => wrapper.find('.dropdown-toggle-text'); - const findDropdownIcon = () => wrapper.find(GlIcon); + const findDropdownIcon = () => wrapper.findComponent(GlIcon); describe('methods', () => { describe('handleButtonClick', () => { @@ -61,7 +61,7 @@ describe('DropdownButton', () => { describe('template', () => { it('renders component container element', () => { - expect(wrapper.find(GlButton).element).toBe(wrapper.element); + expect(wrapper.findComponent(GlButton).element).toBe(wrapper.element); }); it('renders default button text element', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js index 0673ffee22b..799e2c1d08e 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js @@ -127,7 +127,7 @@ describe('DropdownContentsCreateView', () => { }); it('renders dropdown back button element', () => { - const backBtnEl = wrapper.find('.dropdown-title').findAll(GlButton).at(0); + const backBtnEl = wrapper.find('.dropdown-title').findAllComponents(GlButton).at(0); expect(backBtnEl.exists()).toBe(true); expect(backBtnEl.attributes('aria-label')).toBe('Go back'); @@ -142,7 +142,7 @@ describe('DropdownContentsCreateView', () => { }); it('renders dropdown close button element', () => { - const closeBtnEl = wrapper.find('.dropdown-title').findAll(GlButton).at(1); + const closeBtnEl = wrapper.find('.dropdown-title').findAllComponents(GlButton).at(1); expect(closeBtnEl.exists()).toBe(true); expect(closeBtnEl.attributes('aria-label')).toBe('Close'); @@ -150,7 +150,7 @@ describe('DropdownContentsCreateView', () => { }); it('renders label title input element', () => { - const titleInputEl = wrapper.find('.dropdown-input').find(GlFormInput); + const titleInputEl = wrapper.find('.dropdown-input').findComponent(GlFormInput); expect(titleInputEl.exists()).toBe(true); expect(titleInputEl.attributes('placeholder')).toBe('Name new label'); @@ -158,7 +158,7 @@ describe('DropdownContentsCreateView', () => { }); it('renders color block element for all suggested colors', () => { - const colorBlocksEl = wrapper.find('.dropdown-content').findAll(GlLink); + const colorBlocksEl = wrapper.find('.dropdown-content').findAllComponents(GlLink); colorBlocksEl.wrappers.forEach((colorBlock, index) => { expect(colorBlock.attributes('style')).toContain('background-color'); @@ -175,7 +175,7 @@ describe('DropdownContentsCreateView', () => { await nextTick(); const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview'); - const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput); + const colorInputEl = wrapper.find('.color-input-container').findComponent(GlFormInput); expect(colorPreviewEl.exists()).toBe(true); expect(colorPreviewEl.attributes('style')).toContain('background-color'); @@ -185,7 +185,7 @@ describe('DropdownContentsCreateView', () => { }); it('renders create button element', () => { - const createBtnEl = wrapper.find('.dropdown-actions').findAll(GlButton).at(0); + const createBtnEl = wrapper.find('.dropdown-actions').findAllComponents(GlButton).at(0); expect(createBtnEl.exists()).toBe(true); expect(createBtnEl.text()).toContain('Create'); @@ -195,14 +195,14 @@ describe('DropdownContentsCreateView', () => { wrapper.vm.$store.dispatch('requestCreateLabel'); await nextTick(); - const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon); + const loadingIconEl = wrapper.find('.dropdown-actions').findComponent(GlLoadingIcon); expect(loadingIconEl.exists()).toBe(true); expect(loadingIconEl.isVisible()).toBe(true); }); it('renders cancel button element', () => { - const cancelBtnEl = wrapper.find('.dropdown-actions').findAll(GlButton).at(1); + const cancelBtnEl = wrapper.find('.dropdown-actions').findAllComponents(GlButton).at(1); expect(cancelBtnEl.exists()).toBe(true); expect(cancelBtnEl.text()).toContain('Cancel'); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 00c8e3a814a..cc9b9f393ce 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -58,7 +58,7 @@ describe('DropdownContentsLabelsView', () => { const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]'); const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]'); const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); describe('computed', () => { describe('visibleLabels', () => { @@ -285,7 +285,7 @@ describe('DropdownContentsLabelsView', () => { describe('template', () => { it('renders gl-intersection-observer as component root', () => { - expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true); + expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true); }); it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => { @@ -316,20 +316,20 @@ describe('DropdownContentsLabelsView', () => { }); it('renders dropdown close button element', () => { - const closeButtonEl = findDropdownTitle().find(GlButton); + const closeButtonEl = findDropdownTitle().findComponent(GlButton); expect(closeButtonEl.exists()).toBe(true); expect(closeButtonEl.props('icon')).toBe('close'); }); it('renders label search input element', () => { - const searchInputEl = wrapper.find(GlSearchBoxByType); + const searchInputEl = wrapper.findComponent(GlSearchBoxByType); expect(searchInputEl.exists()).toBe(true); }); it('renders label elements for all labels', () => { - expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length); + expect(wrapper.findAllComponents(LabelItem)).toHaveLength(mockLabels.length); }); it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', async () => { @@ -340,7 +340,7 @@ describe('DropdownContentsLabelsView', () => { }); await nextTick(); - const labelItemEl = findDropdownContent().find(LabelItem); + const labelItemEl = findDropdownContent().findComponent(LabelItem); expect(labelItemEl.attributes('highlight')).toBe('true'); }); @@ -373,7 +373,7 @@ describe('DropdownContentsLabelsView', () => { }); it('renders footer list items', () => { - const footerLinks = findDropdownFooter().findAll(GlLink); + const footerLinks = findDropdownFooter().findAllComponents(GlLink); const createLabelLink = footerLinks.at(0); const manageLabelsLink = footerLinks.at(1); @@ -387,7 +387,7 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.$store.state.allowLabelCreate = false; await nextTick(); - const createLabelLink = findDropdownFooter().findAll(GlLink).at(0); + const createLabelLink = findDropdownFooter().findAllComponents(GlLink).at(0); expect(createLabelLink.text()).not.toBe('Create label'); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js index 84e9f3f41c3..54804f85f81 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js @@ -41,7 +41,7 @@ describe('DropdownTitle', () => { }); it('renders edit link', () => { - const editBtnEl = wrapper.find(GlButton); + const editBtnEl = wrapper.findComponent(GlButton); expect(editBtnEl.exists()).toBe(true); expect(editBtnEl.text()).toBe('Edit'); @@ -53,7 +53,7 @@ describe('DropdownTitle', () => { }); await nextTick(); - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js index bedb6204088..bb0f1777de6 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js @@ -32,7 +32,7 @@ describe('LabelItem', () => { describe('template', () => { it('renders gl-link component', () => { - expect(wrapper.find(GlLink).exists()).toBe(true); + expect(wrapper.findComponent(GlLink).exists()).toBe(true); }); it('renders component root with class `is-focused` when `highlight` prop is true', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index c150410ff8e..4c7ac6e9a6f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -138,13 +138,13 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { createComponent(); await nextTick(); - expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); + expect(wrapper.findComponent(DropdownValueCollapsed).exists()).toBe(true); }); it('renders `dropdown-title` component', async () => { createComponent(); await nextTick(); - expect(wrapper.find(DropdownTitle).exists()).toBe(true); + expect(wrapper.findComponent(DropdownTitle).exists()).toBe(true); }); it('renders `dropdown-value` component', async () => { @@ -153,7 +153,7 @@ describe('LabelsSelectRoot', () => { }); await nextTick(); - const valueComp = wrapper.find(DropdownValue); + const valueComp = wrapper.findComponent(DropdownValue); expect(valueComp.exists()).toBe(true); expect(valueComp.text()).toBe('None'); @@ -163,14 +163,14 @@ describe('LabelsSelectRoot', () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownButton'); await nextTick(); - expect(wrapper.find(DropdownButton).exists()).toBe(true); + expect(wrapper.findComponent(DropdownButton).exists()).toBe(true); }); it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownContents'); await nextTick(); - expect(wrapper.find(DropdownContents).exists()).toBe(true); + expect(wrapper.findComponent(DropdownContents).exists()).toBe(true); }); describe('sets content direction based on viewport', () => { @@ -187,7 +187,7 @@ describe('LabelsSelectRoot', () => { wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); await nextTick(); - expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true); + expect(wrapper.findComponent(DropdownContents).props('renderOnTop')).toBe(true); }); it('does not set direction when inside of viewport', async () => { @@ -195,7 +195,7 @@ describe('LabelsSelectRoot', () => { wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); await nextTick(); - expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); + expect(wrapper.findComponent(DropdownContents).props('renderOnTop')).toBe(false); }); }, ); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js index 1b27a294b90..cad401e0013 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js @@ -131,6 +131,7 @@ describe('LabelsSelectRoot', () => { expect(findDropdownValue().exists()).toBe(true); expect(findDropdownValue().props('selectedLabels')).toEqual([ { + __typename: 'Label', color: '#330066', description: null, id: 'gid://gitlab/ProjectLabel/1', diff --git a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js b/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js index de3e1ccfb03..01958a144ed 100644 --- a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js @@ -30,19 +30,19 @@ describe('Todo Button', () => { it('renders GlButton', () => { createComponent(); - expect(wrapper.find(GlButton).exists()).toBe(true); + expect(wrapper.findComponent(GlButton).exists()).toBe(true); }); it('emits click event when clicked', () => { createComponent({}, mount); - wrapper.find(GlButton).trigger('click'); + wrapper.findComponent(GlButton).trigger('click'); - expect(wrapper.emitted().click).toBeTruthy(); + expect(wrapper.emitted().click).toHaveLength(1); }); it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => { createComponent({}, mount); - wrapper.find(GlButton).trigger('click'); + wrapper.findComponent(GlButton).trigger('click'); const dispatchedEvent = dispatchEventSpy.mock.calls[0][0]; expect(dispatchEventSpy).toHaveBeenCalledTimes(1); @@ -57,12 +57,12 @@ describe('Todo Button', () => { `('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => { createComponent({ isTodo }); - expect(wrapper.find(GlButton).text()).toBe(label); + expect(wrapper.findComponent(GlButton).text()).toBe(label); }); it('binds additional props to GlButton', () => { createComponent({ loading: true }); - expect(wrapper.find(GlButton).props('loading')).toBe(true); + expect(wrapper.findComponent(GlButton).props('loading')).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/source_editor_spec.js b/spec/frontend/vue_shared/components/source_editor_spec.js index dca4d60e23c..ca5b990bc29 100644 --- a/spec/frontend/vue_shared/components/source_editor_spec.js +++ b/spec/frontend/vue_shared/components/source_editor_spec.js @@ -3,6 +3,7 @@ import { nextTick } from 'vue'; import { EDITOR_READY_EVENT } from '~/editor/constants'; import Editor from '~/editor/source_editor'; import SourceEditor from '~/vue_shared/components/source_editor.vue'; +import * as helpers from 'jest/editor/helpers'; jest.mock('~/editor/source_editor'); @@ -13,6 +14,7 @@ describe('Source Editor component', () => { const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const fileName = 'lorem.txt'; const fileGlobalId = 'snippet_777'; + const useSpy = jest.fn(); const createInstanceMock = jest.fn().mockImplementation(() => { mockInstance = { onDidChangeModelContent: jest.fn(), @@ -20,6 +22,7 @@ describe('Source Editor component', () => { getValue: jest.fn(), setValue: jest.fn(), dispose: jest.fn(), + use: useSpy, }; return mockInstance; }); @@ -77,16 +80,33 @@ describe('Source Editor component', () => { }); it('initialises Source Editor instance', () => { - const el = wrapper.find({ ref: 'editor' }).element; + const el = wrapper.findComponent({ ref: 'editor' }).element; expect(createInstanceMock).toHaveBeenCalledWith({ el, blobPath: fileName, blobGlobalId: fileGlobalId, blobContent: value, - extensions: null, }); }); + it.each` + description | extensions | toBeCalled + ${'no extension when `undefined` is'} | ${undefined} | ${false} + ${'no extension when {} is'} | ${{}} | ${false} + ${'no extension when [] is'} | ${[]} | ${false} + ${'single extension'} | ${{ definition: helpers.SEClassExtension }} | ${true} + ${'single extension with options'} | ${{ definition: helpers.SEWithSetupExt, setupOptions: { foo: 'bar' } }} | ${true} + ${'multiple extensions'} | ${[{ definition: helpers.SEClassExtension }, { definition: helpers.SEWithSetupExt }]} | ${true} + ${'multiple extensions with options'} | ${[{ definition: helpers.SEClassExtension }, { definition: helpers.SEWithSetupExt, setupOptions: { foo: 'bar' } }]} | ${true} + `('installs $description passed as a prop', ({ extensions, toBeCalled }) => { + createComponent({ extensions }); + if (toBeCalled) { + expect(useSpy).toHaveBeenCalledWith(extensions); + } else { + expect(useSpy).not.toHaveBeenCalled(); + } + }); + it('reacts to the changes in fileName', () => { const newFileName = 'ipsum.txt'; @@ -112,7 +132,7 @@ describe('Source Editor component', () => { }); it('emits EDITOR_READY_EVENT event when the Source Editor is ready', async () => { - const el = wrapper.find({ ref: 'editor' }).element; + const el = wrapper.findComponent({ ref: 'editor' }).element; expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeUndefined(); await el.dispatchEvent(new Event(EDITOR_READY_EVENT)); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js index eb2eec92534..fd3ff9ce892 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js @@ -1,4 +1,3 @@ -import { GlLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue'; import { @@ -11,16 +10,26 @@ const DEFAULT_PROPS = { number: 2, content: '// Line content', language: 'javascript', + blamePath: 'blame/file.js', }; describe('Chunk Line component', () => { let wrapper; + const fileLineBlame = true; const createComponent = (props = {}) => { - wrapper = shallowMountExtended(ChunkLine, { propsData: { ...DEFAULT_PROPS, ...props } }); + wrapper = shallowMountExtended(ChunkLine, { + propsData: { ...DEFAULT_PROPS, ...props }, + provide: { + glFeatures: { + fileLineBlame, + }, + }, + }); }; - const findLink = () => wrapper.findComponent(GlLink); + const findLineLink = () => wrapper.find('.file-line-num'); + const findBlameLink = () => wrapper.find('.file-line-blame'); const findContent = () => wrapper.findByTestId('content'); const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper'); @@ -47,14 +56,22 @@ describe('Chunk Line component', () => { }); }); + it('renders a blame link', () => { + expect(findBlameLink().attributes()).toMatchObject({ + href: `${DEFAULT_PROPS.blamePath}#L${DEFAULT_PROPS.number}`, + }); + + expect(findBlameLink().text()).toBe(''); + }); + it('renders a line number', () => { - expect(findLink().attributes()).toMatchObject({ + expect(findLineLink().attributes()).toMatchObject({ 'data-line-number': `${DEFAULT_PROPS.number}`, - to: `#L${DEFAULT_PROPS.number}`, + href: `#L${DEFAULT_PROPS.number}`, id: `L${DEFAULT_PROPS.number}`, }); - expect(findLink().text()).toBe(DEFAULT_PROPS.number.toString()); + expect(findLineLink().text()).toBe(DEFAULT_PROPS.number.toString()); }); it('renders content', () => { diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js index 42c4f2eacb8..8dc3348acfa 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js @@ -10,6 +10,7 @@ const DEFAULT_PROPS = { startingFrom: 140, totalLines: 50, language: 'javascript', + blamePath: 'blame/file.js', }; describe('Chunk component', () => { @@ -76,6 +77,7 @@ describe('Chunk component', () => { number: DEFAULT_PROPS.startingFrom + 1, content: splitContent[0], language: DEFAULT_PROPS.language, + blamePath: DEFAULT_PROPS.blamePath, }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js index 3036ce43888..375b1307616 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js @@ -1,8 +1,10 @@ import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker'; +import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker'; import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies'; -import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT } from './mock_data'; +import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT, GEMSPEC_FILE_TYPE } from './mock_data'; jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker'); +jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker'); describe('Highlight.js plugin for linking dependencies', () => { const hljsResultMock = { value: 'test' }; @@ -11,4 +13,9 @@ describe('Highlight.js plugin for linking dependencies', () => { linkDependencies(hljsResultMock, PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT); expect(packageJsonLinker).toHaveBeenCalled(); }); + + it('calls gemspecLinker for gemspec file types', () => { + linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE); + expect(gemspecLinker).toHaveBeenCalled(); + }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js index 75659770e2c..aa874c9c081 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js @@ -1,2 +1,4 @@ export const PACKAGE_JSON_FILE_TYPE = 'package_json'; export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }'; + +export const GEMSPEC_FILE_TYPE = 'gemspec'; diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js index ee200747af9..8079d5ad99a 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js @@ -14,10 +14,11 @@ describe('createLink', () => { it('escapes the user-controlled content', () => { const unescapedXSS = '<script>XSS</script>'; - const escapedXSS = '&lt;script&gt;XSS&lt;/script&gt;'; + const escapedPackageName = '<script>XSS</script>'; + const escapedHref = '&lt;script&gt;XSS&lt;/script&gt;'; const href = `http://test.com/${unescapedXSS}`; const innerText = `testing${unescapedXSS}`; - const result = `<a href="http://test.com/${escapedXSS}" rel="nofollow noreferrer noopener">testing${escapedXSS}</a>`; + const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`; expect(createLink(href, innerText)).toBe(result); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js new file mode 100644 index 00000000000..3f74bfa117f --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js @@ -0,0 +1,14 @@ +import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker'; + +describe('Highlight.js plugin for linking gemspec dependencies', () => { + it('mutates the input value by wrapping dependency names in anchors', () => { + const inputValue = + 's.add_dependency(<span class="hljs-string">'rugged'</span>, <span class="hljs-string">'~> 0.24.0'</span>)'; + const outputValue = + 's.add_dependency(<span class="hljs-string linked">'<a href="https://rubygems.org/gems/rugged" rel="nofollow noreferrer noopener">rugged</a>'</span>, <span class="hljs-string">'~> 0.24.0'</span>)'; + const hljsResultMock = { value: inputValue }; + + const output = gemspecLinker(hljsResultMock); + expect(output).toBe(outputValue); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 2c03b7aa7d3..4fbc907a813 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -40,8 +40,9 @@ describe('Source Viewer component', () => { const chunk2 = generateContent('// Some source code 2', 70); const content = chunk1 + chunk2; const path = 'some/path.js'; + const blamePath = 'some/blame/path.js'; const fileType = 'javascript'; - const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, fileType }; + const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType }; const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`; const createComponent = async (blob = {}) => { diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js index 4965969bc3e..6b869db4058 100644 --- a/spec/frontend/vue_shared/components/split_button_spec.js +++ b/spec/frontend/vue_shared/components/split_button_spec.js @@ -26,8 +26,9 @@ describe('SplitButton', () => { }); }; - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownItem = (index = 0) => findDropdown().findAll(GlDropdownItem).at(index); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItem = (index = 0) => + findDropdown().findAllComponents(GlDropdownItem).at(index); const selectItem = async (index) => { findDropdownItem(index).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js index ed23a47c328..99de26ce2ae 100644 --- a/spec/frontend/vue_shared/components/table_pagination_spec.js +++ b/spec/frontend/vue_shared/components/table_pagination_spec.js @@ -50,7 +50,7 @@ describe('Pagination component', () => { change: spy, }); - expect(wrapper.find(GlPagination).exists()).toBe(true); + expect(wrapper.findComponent(GlPagination).exists()).toBe(true); }); it('renders if there is a prev page', () => { @@ -66,7 +66,7 @@ describe('Pagination component', () => { change: spy, }); - expect(wrapper.find(GlPagination).exists()).toBe(true); + expect(wrapper.findComponent(GlPagination).exists()).toBe(true); }); }); @@ -83,7 +83,7 @@ describe('Pagination component', () => { }, change: spy, }); - wrapper.find(GlPagination).vm.$emit('input', 3); + wrapper.findComponent(GlPagination).vm.$emit('input', 3); expect(spy).toHaveBeenCalledWith(3); }); }); diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js index 9e7e5c1263f..ca1f7996ad6 100644 --- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js +++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js @@ -68,7 +68,7 @@ describe('TooltipOnTruncate component', () => { }, ); - wrapper = parent.find(WrappedTooltipOnTruncate); + wrapper = parent.findComponent(WrappedTooltipOnTruncate); }; const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip')?.value; diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js index 21e9b401215..a063a5591e3 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js +++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js @@ -14,7 +14,7 @@ describe('Upload dropzone component', () => { const findDropzoneCard = () => wrapper.find('.upload-dropzone-card'); const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]'); - const findIcon = () => wrapper.find(GlIcon); + const findIcon = () => wrapper.findComponent(GlIcon); const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text(); const findFileInput = () => wrapper.find('input[type="file"]'); diff --git a/spec/frontend/vue_shared/components/user_access_role_badge_spec.js b/spec/frontend/vue_shared/components/user_access_role_badge_spec.js index 7f25f7c08e7..cea6fcac8c8 100644 --- a/spec/frontend/vue_shared/components/user_access_role_badge_spec.js +++ b/spec/frontend/vue_shared/components/user_access_role_badge_spec.js @@ -18,7 +18,7 @@ describe('UserAccessRoleBadge', () => { }, }); - const badge = wrapper.find(GlBadge); + const badge = wrapper.findComponent(GlBadge); expect(badge.exists()).toBe(true); expect(badge.html()).toContain('test slot content'); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js index 5e05b54cb8c..f87737ca86a 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js @@ -18,6 +18,8 @@ const PROVIDED_PROPS = { describe('User Avatar Image Component', () => { let wrapper; + const findAvatar = () => wrapper.findComponent(GlAvatar); + afterEach(() => { wrapper.destroy(); }); @@ -28,21 +30,14 @@ describe('User Avatar Image Component', () => { propsData: { ...PROVIDED_PROPS, }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: true, - }, - }, }); }); it('should render `GlAvatar` and provide correct properties to it', () => { - const avatar = wrapper.findComponent(GlAvatar); - - expect(avatar.attributes('data-src')).toBe( + expect(findAvatar().attributes('data-src')).toBe( `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, ); - expect(avatar.props()).toMatchObject({ + expect(findAvatar().props()).toMatchObject({ src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, alt: PROVIDED_PROPS.imgAlt, size: PROVIDED_PROPS.size, @@ -63,23 +58,28 @@ describe('User Avatar Image Component', () => { ...PROVIDED_PROPS, lazy: true, }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: true, - }, - }, }); }); it('should add lazy attributes', () => { - const avatar = wrapper.findComponent(GlAvatar); - - expect(avatar.classes()).toContain('lazy'); - expect(avatar.attributes()).toMatchObject({ + expect(findAvatar().classes()).toContain('lazy'); + expect(findAvatar().attributes()).toMatchObject({ src: placeholderImage, 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, }); }); + + it('should use maximum number when size is provided as an object', () => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + size: { default: 16, md: 64, lg: 24 }, + lazy: true, + }, + }); + + expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`); + }); }); describe('Initialization without src', () => { @@ -89,18 +89,11 @@ describe('User Avatar Image Component', () => { ...PROVIDED_PROPS, imgSrc: null, }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: true, - }, - }, }); }); it('should have default avatar image', () => { - const avatar = wrapper.findComponent(GlAvatar); - - expect(avatar.props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`); + expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js index 75d2a936b34..6ad2ef226c2 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -15,47 +15,37 @@ const PROVIDED_PROPS = { describe('User Avatar Image Component', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - - describe('when `glAvatarForAllUserAvatars` feature flag enabled', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: { - ...PROVIDED_PROPS, + const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + ...props, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars, }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: true, - }, - }, - }); + }, }); + }; - it('should render `UserAvatarImageNew` component', () => { - expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(true); - expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(false); - }); + afterEach(() => { + wrapper.destroy(); }); - describe('when `glAvatarForAllUserAvatars` feature flag disabled', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: { - ...PROVIDED_PROPS, - }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: false, - }, - }, + describe.each([ + [false, true, true], + [true, false, true], + [true, true, true], + [false, false, false], + ])( + 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s', + (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => { + it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => { + createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars }); + expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(isUsingNewVersion); + expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(!isUsingNewVersion); }); - }); - - it('should render `UserAvatarImageOld` component', () => { - expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(false); - expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(true); - }); - }); + }, + ); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js index 5ba80b31b99..f485a14cfea 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js @@ -54,6 +54,7 @@ describe('User Avatar Link Component', () => { size: defaultProps.imgSize, tooltipPlacement: defaultProps.tooltipPlacement, tooltipText: '', + enforceGlAvatar: false, }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js index 2d513c46e77..cf7a1025dba 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js @@ -54,6 +54,7 @@ describe('User Avatar Link Component', () => { size: defaultProps.imgSize, tooltipPlacement: defaultProps.tooltipPlacement, tooltipText: '', + enforceGlAvatar: false, }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js index b36b83d1fea..fd3f59008ec 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -15,47 +15,37 @@ const PROVIDED_PROPS = { describe('User Avatar Link Component', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - - describe('when `glAvatarForAllUserAvatars` feature flag enabled', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarLink, { - propsData: { - ...PROVIDED_PROPS, + const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => { + wrapper = shallowMount(UserAvatarLink, { + propsData: { + ...PROVIDED_PROPS, + ...props, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars, }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: true, - }, - }, - }); + }, }); + }; - it('should render `UserAvatarLinkNew` component', () => { - expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(true); - expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(false); - }); + afterEach(() => { + wrapper.destroy(); }); - describe('when `glAvatarForAllUserAvatars` feature flag disabled', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarLink, { - propsData: { - ...PROVIDED_PROPS, - }, - provide: { - glFeatures: { - glAvatarForAllUserAvatars: false, - }, - }, + describe.each([ + [false, true, true], + [true, false, true], + [true, true, true], + [false, false, false], + ])( + 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s', + (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => { + it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => { + createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars }); + expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(isUsingNewVersion); + expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(!isUsingNewVersion); }); - }); - - it('should render `UserAvatarLinkOld` component', () => { - expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(false); - expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(true); - }); - }); + }, + ); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 20ff0848cff..b9accbf0373 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -38,7 +38,7 @@ describe('UserAvatarList', () => { }; const clickButton = () => { - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); button.vm.$emit('click'); }; @@ -79,7 +79,7 @@ describe('UserAvatarList', () => { const items = createList(20); factory({ propsData: { items } }); - const links = wrapper.findAll(UserAvatarLink); + const links = wrapper.findAllComponents(UserAvatarLink); const linkProps = links.wrappers.map((x) => x.props()); expect(linkProps).toEqual( @@ -105,7 +105,7 @@ describe('UserAvatarList', () => { it('renders all avatars if length is <= breakpoint', () => { factory(); - const links = wrapper.findAll(UserAvatarLink); + const links = wrapper.findAllComponents(UserAvatarLink); expect(links.length).toEqual(props.items.length); }); @@ -113,7 +113,7 @@ describe('UserAvatarList', () => { it('does not show button', () => { factory(); - expect(wrapper.find(GlButton).exists()).toBe(false); + expect(wrapper.findComponent(GlButton).exists()).toBe(false); }); }); @@ -126,7 +126,7 @@ describe('UserAvatarList', () => { it('renders avatars up to breakpoint', () => { factory(); - const links = wrapper.findAll(UserAvatarLink); + const links = wrapper.findAllComponents(UserAvatarLink); expect(links.length).toEqual(TEST_BREAKPOINT); }); @@ -138,7 +138,7 @@ describe('UserAvatarList', () => { }); it('renders all avatars', () => { - const links = wrapper.findAll(UserAvatarLink); + const links = wrapper.findAllComponents(UserAvatarLink); expect(links.length).toEqual(props.items.length); }); @@ -147,7 +147,7 @@ describe('UserAvatarList', () => { clickButton(); await nextTick(); - const links = wrapper.findAll(UserAvatarLink); + const links = wrapper.findAllComponents(UserAvatarLink); expect(links.length).toEqual(TEST_BREAKPOINT); }); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index 9550368eefc..b7ce3e47cef 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -6,6 +6,7 @@ import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; import { followUser, unfollowUser } from '~/api/user_api'; +import { mockTracking } from 'helpers/tracking_helper'; jest.mock('~/flash'); jest.mock('~/api/user_api', () => ({ @@ -51,6 +52,18 @@ describe('User Popover Component', () => { const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time'); const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button'); + const itTracksToggleFollowButtonClick = (expectedLabel) => { + it('tracks click', async () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + await findToggleFollowButton().trigger('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { + label: expectedLabel, + }); + }); + }; + const createWrapper = (props = {}) => { wrapper = mountExtended(UserPopover, { propsData: { @@ -75,7 +88,7 @@ describe('User Popover Component', () => { }, }); - expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); }); }); @@ -89,7 +102,7 @@ describe('User Popover Component', () => { it('shows icon for location', () => { createWrapper(); - const iconEl = wrapper.find(GlIcon); + const iconEl = wrapper.findComponent(GlIcon); expect(iconEl.props('name')).toEqual('location'); }); @@ -102,8 +115,8 @@ describe('User Popover Component', () => { }); describe('job data', () => { - const findWorkInformation = () => wrapper.find({ ref: 'workInformation' }); - const findBio = () => wrapper.find({ ref: 'bio' }); + const findWorkInformation = () => wrapper.findComponent({ ref: 'workInformation' }); + const findBio = () => wrapper.findComponent({ ref: 'bio' }); const bio = 'My super interesting bio'; it('should show only bio if work information is not available', () => { @@ -159,7 +172,7 @@ describe('User Popover Component', () => { createWrapper({ user }); expect( - wrapper.findAll(GlIcon).filter((icon) => icon.props('name') === 'profile').length, + wrapper.findAllComponents(GlIcon).filter((icon) => icon.props('name') === 'profile').length, ).toEqual(1); }); @@ -172,7 +185,7 @@ describe('User Popover Component', () => { createWrapper({ user }); expect( - wrapper.findAll(GlIcon).filter((icon) => icon.props('name') === 'work').length, + wrapper.findAllComponents(GlIcon).filter((icon) => icon.props('name') === 'work').length, ).toEqual(1); }); }); @@ -338,9 +351,11 @@ describe('User Popover Component', () => { await axios.waitForAll(); expect(wrapper.emitted().follow.length).toBe(1); - expect(wrapper.emitted().unfollow).toBeFalsy(); + expect(wrapper.emitted().unfollow).toBeUndefined(); }); + itTracksToggleFollowButtonClick('follow_from_user_popover'); + describe('when an error occurs', () => { beforeEach(() => { followUser.mockRejectedValue({}); @@ -361,8 +376,8 @@ describe('User Popover Component', () => { it('emits no events', async () => { await axios.waitForAll(); - expect(wrapper.emitted().follow).toBe(undefined); - expect(wrapper.emitted().unfollow).toBe(undefined); + expect(wrapper.emitted().follow).toBeUndefined(); + expect(wrapper.emitted().unfollow).toBeUndefined(); }); }); }); @@ -388,6 +403,8 @@ describe('User Popover Component', () => { expect(wrapper.emitted().unfollow.length).toBe(1); }); + itTracksToggleFollowButtonClick('unfollow_from_user_popover'); + describe('when an error occurs', () => { beforeEach(async () => { unfollowUser.mockRejectedValue({}); @@ -406,8 +423,8 @@ describe('User Popover Component', () => { }); it('emits no events', () => { - expect(wrapper.emitted().follow).toBe(undefined); - expect(wrapper.emitted().unfollow).toBe(undefined); + expect(wrapper.emitted().follow).toBeUndefined(); + expect(wrapper.emitted().unfollow).toBeUndefined(); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index ec9128d5e38..4188adc72a1 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -9,6 +9,7 @@ import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphq import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; import { IssuableType } from '~/issues/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import { @@ -16,6 +17,8 @@ import { searchResponseOnMR, projectMembersResponse, participantsQueryResponse, + mockUser1, + mockUser2, } from 'jest/sidebar/mock_data'; const assignee = { @@ -45,9 +48,14 @@ describe('User select dropdown', () => { const findSearchField = () => wrapper.findComponent(GlSearchBoxByType); const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]'); const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]'); + const findSelectedParticipantByIndex = (index) => + findSelectedParticipants().at(index).findComponent(SidebarParticipant); const findUnselectedParticipants = () => wrapper.findAll('[data-testid="unselected-participant"]'); + const findUnselectedParticipantByIndex = (index) => + findUnselectedParticipants().at(index).findComponent(SidebarParticipant); const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]'); + const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]'); const findUnassignLink = () => wrapper.find('[data-testid="unassign"]'); const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]'); @@ -136,6 +144,93 @@ describe('User select dropdown', () => { expect(findCurrentUser().exists()).toBe(true); }); + it('does not render current user if user is not logged in', async () => { + createComponent({ + props: { + currentUser: {}, + }, + }); + await waitForPromises(); + + expect(findCurrentUser().exists()).toBe(false); + }); + + it('does not render issuable author if author is not passed as a prop', async () => { + createComponent(); + await waitForPromises(); + + expect(findIssuableAuthor().exists()).toBe(false); + }); + + describe('when issuable author is passed as a prop', () => { + it('moves issuable author on top of assigned list, if author is assigned', async () => { + createComponent({ + props: { + value: [assignee, mockUser2], + issuableAuthor: mockUser2, + }, + }); + await waitForPromises(); + + expect(findSelectedParticipantByIndex(0).props('user')).toEqual(mockUser2); + }); + + it('moves issuable author on top of assigned list after current user, if author and current user are assigned', async () => { + const currentUser = mockUser1; + const issuableAuthor = mockUser2; + + createComponent({ + props: { + value: [assignee, issuableAuthor, currentUser], + issuableAuthor, + currentUser, + }, + }); + await waitForPromises(); + + expect(findSelectedParticipantByIndex(0).props('user')).toEqual(currentUser); + expect(findSelectedParticipantByIndex(1).props('user')).toEqual(issuableAuthor); + }); + + it('moves issuable author on top of unassigned list, if author is unassigned project member', async () => { + createComponent({ + props: { + issuableAuthor: mockUser2, + }, + }); + await waitForPromises(); + + expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(mockUser2); + }); + + it('moves issuable author on top of unassigned list after current user, if author and current user are unassigned project members', async () => { + const currentUser = mockUser2; + const issuableAuthor = mockUser1; + + createComponent({ + props: { + issuableAuthor, + currentUser, + }, + }); + await waitForPromises(); + + expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(currentUser); + expect(findUnselectedParticipantByIndex(1).props('user')).toMatchObject(issuableAuthor); + }); + + it('displays author in a designated position if author is not assigned and not a project member', async () => { + createComponent({ + props: { + issuableAuthor: assignee, + }, + }); + await waitForPromises(); + + expect(findIssuableAuthor().exists()).toBe(true); + }); + }); + it('displays correct amount of selected users', async () => { createComponent({ props: { diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 040461f6be4..a0b868d1d52 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -3,7 +3,7 @@ import { nextTick } from 'vue'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; +import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue'; import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; import { stubComponent } from 'helpers/stub_component'; @@ -37,8 +37,8 @@ const ACTION_EDIT_CONFIRM_FORK = { const ACTION_WEB_IDE = { href: TEST_WEB_IDE_URL, key: 'webide', - secondaryText: 'Quickly and easily edit multiple files in your project.', - tooltip: '', + secondaryText: i18n.webIdeText, + tooltip: i18n.webIdeTooltip, text: 'Web IDE', attrs: { 'data-qa-selector': 'web_ide_button', @@ -108,8 +108,8 @@ describe('Web IDE link component', () => { wrapper.destroy(); }); - const findActionsButton = () => wrapper.find(ActionsButton); - const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + const findActionsButton = () => wrapper.findComponent(ActionsButton); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const findModal = () => wrapper.findComponent(GlModal); const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal); diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js index 81362edaf37..7b0f0f7e344 100644 --- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js +++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js @@ -51,11 +51,11 @@ describe('IssuableCreateRoot', () => { }); it('renders issuable-form component', () => { - expect(wrapper.find(IssuableForm).exists()).toBe(true); + expect(wrapper.findComponent(IssuableForm).exists()).toBe(true); }); it('renders contents for slot "actions" within issuable-form component', () => { - const buttonEl = wrapper.find(IssuableForm).find('button.js-issuable-save'); + const buttonEl = wrapper.findComponent(IssuableForm).find('button.js-issuable-save'); expect(buttonEl.exists()).toBe(true); expect(buttonEl.text()).toBe('Submit issuable'); diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js index cbfd05e7903..f98e7a678f4 100644 --- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js +++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js @@ -65,9 +65,9 @@ describe('IssuableForm', () => { expect(titleFieldEl.exists()).toBe(true); expect(titleFieldEl.find('label').text()).toBe('Title'); - expect(titleFieldEl.find(GlFormInput).exists()).toBe(true); - expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title'); - expect(titleFieldEl.find(GlFormInput).attributes('autofocus')).toBe('true'); + expect(titleFieldEl.findComponent(GlFormInput).exists()).toBe(true); + expect(titleFieldEl.findComponent(GlFormInput).attributes('placeholder')).toBe('Title'); + expect(titleFieldEl.findComponent(GlFormInput).attributes('autofocus')).toBe('true'); }); it('renders issuable description input field', () => { @@ -75,8 +75,8 @@ describe('IssuableForm', () => { expect(descriptionFieldEl.exists()).toBe(true); expect(descriptionFieldEl.find('label').text()).toBe('Description'); - expect(descriptionFieldEl.find(MarkdownField).exists()).toBe(true); - expect(descriptionFieldEl.find(MarkdownField).props()).toMatchObject({ + expect(descriptionFieldEl.findComponent(MarkdownField).exists()).toBe(true); + expect(descriptionFieldEl.findComponent(MarkdownField).props()).toMatchObject({ markdownPreviewPath: wrapper.vm.descriptionPreviewPath, markdownDocsPath: wrapper.vm.descriptionHelpPath, addSpacingClasses: false, @@ -94,8 +94,8 @@ describe('IssuableForm', () => { expect(labelsSelectEl.exists()).toBe(true); expect(labelsSelectEl.find('label').text()).toBe('Labels'); - expect(labelsSelectEl.find(LabelsSelect).exists()).toBe(true); - expect(labelsSelectEl.find(LabelsSelect).props()).toMatchObject({ + expect(labelsSelectEl.findComponent(LabelsSelect).exists()).toBe(true); + expect(labelsSelectEl.findComponent(LabelsSelect).props()).toMatchObject({ allowLabelEdit: true, allowLabelCreate: true, allowMultiselect: true, diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js index 80f14dffd08..f55d3156581 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js @@ -3,6 +3,7 @@ import { nextTick } from 'vue'; import { useFakeDate } from 'helpers/fake_date'; import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_helper'; import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import IssuableAssignees from '~/issuable/components/issue_assignees.vue'; import { mockIssuable, mockRegularLabel } from '../mock_data'; @@ -13,6 +14,7 @@ const createComponent = ({ issuable = mockIssuable, showCheckbox = true, slots = {}, + showWorkItemTypeIcon = false, } = {}) => shallowMount(IssuableItem, { propsData: { @@ -21,6 +23,7 @@ const createComponent = ({ issuable, showDiscussions: true, showCheckbox, + showWorkItemTypeIcon, }, slots, stubs: { @@ -40,6 +43,7 @@ describe('IssuableItem', () => { let wrapper; const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]'); + const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon); beforeEach(() => { gon.gitlab_url = MOCK_GITLAB_URL; @@ -273,9 +277,9 @@ describe('IssuableItem', () => { const titleEl = wrapper.find('[data-testid="issuable-title"]'); expect(titleEl.exists()).toBe(true); - expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref); - expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget); - expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title); + expect(titleEl.findComponent(GlLink).attributes('href')).toBe(expectedHref); + expect(titleEl.findComponent(GlLink).attributes('target')).toBe(expectedTarget); + expect(titleEl.findComponent(GlLink).text()).toBe(mockIssuable.title); }, ); @@ -286,8 +290,8 @@ describe('IssuableItem', () => { await nextTick(); - expect(wrapper.find(GlFormCheckbox).exists()).toBe(true); - expect(wrapper.find(GlFormCheckbox).attributes('checked')).not.toBeDefined(); + expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true); + expect(wrapper.findComponent(GlFormCheckbox).attributes('checked')).not.toBeDefined(); wrapper.setProps({ checked: true, @@ -295,7 +299,7 @@ describe('IssuableItem', () => { await nextTick(); - expect(wrapper.find(GlFormCheckbox).attributes('checked')).toBe('true'); + expect(wrapper.findComponent(GlFormCheckbox).attributes('checked')).toBe('true'); }); it('renders issuable title with `target` set as "_blank" when issuable.webUrl is external', async () => { @@ -308,9 +312,9 @@ describe('IssuableItem', () => { await nextTick(); - expect(wrapper.find('[data-testid="issuable-title"]').find(GlLink).attributes('target')).toBe( - '_blank', - ); + expect( + wrapper.find('[data-testid="issuable-title"]').findComponent(GlLink).attributes('target'), + ).toBe('_blank'); }); it('renders issuable confidential icon when issuable is confidential', async () => { @@ -323,7 +327,7 @@ describe('IssuableItem', () => { await nextTick(); - const confidentialEl = wrapper.find('[data-testid="issuable-title"]').find(GlIcon); + const confidentialEl = wrapper.find('[data-testid="issuable-title"]').findComponent(GlIcon); expect(confidentialEl.exists()).toBe(true); expect(confidentialEl.props('name')).toBe('eye-slash'); @@ -349,11 +353,23 @@ describe('IssuableItem', () => { wrapper = createComponent(); const taskStatus = wrapper.find('[data-testid="task-status"]'); - const expected = `${mockIssuable.taskCompletionStatus.completedCount} of ${mockIssuable.taskCompletionStatus.count} tasks completed`; + const expected = `${mockIssuable.taskCompletionStatus.completedCount} of ${mockIssuable.taskCompletionStatus.count} checklist items completed`; expect(taskStatus.text()).toBe(expected); }); + it('does not renders work item type icon by default', () => { + wrapper = createComponent(); + + expect(findWorkItemTypeIcon().exists()).toBe(false); + }); + + it('renders work item type icon when props passed', () => { + wrapper = createComponent({ showWorkItemTypeIcon: true }); + + expect(findWorkItemTypeIcon().props('workItemType')).toBe(mockIssuable.type); + }); + it('renders issuable reference', () => { wrapper = createComponent(); @@ -440,7 +456,7 @@ describe('IssuableItem', () => { it('renders gl-label component for each label present within `issuable` prop', () => { wrapper = createComponent(); - const labelsEl = wrapper.findAll(GlLabel); + const labelsEl = wrapper.findAllComponents(GlLabel); expect(labelsEl.exists()).toBe(true); expect(labelsEl).toHaveLength(mockLabels.length); @@ -476,18 +492,18 @@ describe('IssuableItem', () => { const discussionsEl = wrapper.find('[data-testid="issuable-discussions"]'); expect(discussionsEl.exists()).toBe(true); - expect(discussionsEl.find(GlLink).attributes()).toMatchObject({ + expect(discussionsEl.findComponent(GlLink).attributes()).toMatchObject({ title: 'Comments', href: `${mockIssuable.webUrl}#notes`, }); - expect(discussionsEl.find(GlIcon).props('name')).toBe('comments'); - expect(discussionsEl.find(GlLink).text()).toContain('2'); + expect(discussionsEl.findComponent(GlIcon).props('name')).toBe('comments'); + expect(discussionsEl.findComponent(GlLink).text()).toContain('2'); }); it('renders issuable-assignees component', () => { wrapper = createComponent(); - const assigneesEl = wrapper.find(IssuableAssignees); + const assigneesEl = wrapper.findComponent(IssuableAssignees); expect(assigneesEl.exists()).toBe(true); expect(assigneesEl.props()).toMatchObject({ diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js index 50e79dbe589..0c53f599d55 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js @@ -359,7 +359,7 @@ describe('IssuableListRoot', () => { findIssuableTabs().vm.$emit('click'); - expect(wrapper.emitted('click-tab')).toBeTruthy(); + expect(wrapper.emitted('click-tab')).toHaveLength(1); }); it('sets all issuables as checked when filtered-search-bar component emits `checked-input` event', () => { @@ -369,7 +369,7 @@ describe('IssuableListRoot', () => { searchEl.vm.$emit('checked-input', true); - expect(searchEl.emitted('checked-input')).toBeTruthy(); + expect(searchEl.emitted('checked-input')).toHaveLength(1); expect(searchEl.emitted('checked-input').length).toBe(1); expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({ @@ -384,9 +384,9 @@ describe('IssuableListRoot', () => { const searchEl = findFilteredSearchBar(); searchEl.vm.$emit('onFilter'); - expect(wrapper.emitted('filter')).toBeTruthy(); + expect(wrapper.emitted('filter')).toHaveLength(1); searchEl.vm.$emit('onSort'); - expect(wrapper.emitted('sort')).toBeTruthy(); + expect(wrapper.emitted('sort')).toHaveLength(1); }); it('sets an issuable as checked when issuable-item component emits `checked-input` event', () => { @@ -396,7 +396,7 @@ describe('IssuableListRoot', () => { issuableItem.vm.$emit('checked-input', true); - expect(issuableItem.emitted('checked-input')).toBeTruthy(); + expect(issuableItem.emitted('checked-input')).toHaveLength(1); expect(issuableItem.emitted('checked-input').length).toBe(1); expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({ @@ -425,7 +425,7 @@ describe('IssuableListRoot', () => { wrapper = createComponent({ data, props: { showPaginationControls: true } }); findGlPagination().vm.$emit('input'); - expect(wrapper.emitted('page-change')).toBeTruthy(); + expect(wrapper.emitted('page-change')).toHaveLength(1); }); it.each` diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js index 8640f4a2cd5..b67bd0f42fe 100644 --- a/spec/frontend/vue_shared/issuable/list/mock_data.js +++ b/spec/frontend/vue_shared/issuable/list/mock_data.js @@ -57,6 +57,7 @@ export const mockIssuable = { count: 2, completedCount: 1, }, + type: 'issue', }; export const mockIssuables = [ diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js index 7c582360637..39a76a51191 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js @@ -154,7 +154,7 @@ describe('IssuableBody', () => { describe('template', () => { it('renders issuable-title component', () => { - const titleEl = wrapper.find(IssuableTitle); + const titleEl = wrapper.findComponent(IssuableTitle); expect(titleEl.exists()).toBe(true); expect(titleEl.props()).toMatchObject({ @@ -165,7 +165,7 @@ describe('IssuableBody', () => { }); it('renders issuable-description component', () => { - const descriptionEl = wrapper.find(IssuableDescription); + const descriptionEl = wrapper.findComponent(IssuableDescription); expect(descriptionEl.exists()).toBe(true); expect(descriptionEl.props('issuable')).toEqual(issuableBodyProps.issuable); @@ -184,7 +184,7 @@ describe('IssuableBody', () => { await nextTick(); - const editFormEl = wrapper.find(IssuableEditForm); + const editFormEl = wrapper.findComponent(IssuableEditForm); expect(editFormEl.exists()).toBe(true); expect(editFormEl.props()).toMatchObject({ issuable: issuableBodyProps.issuable, @@ -198,7 +198,7 @@ describe('IssuableBody', () => { describe('events', () => { it('component emits `edit-issuable` event bubbled via issuable-title', () => { - const issuableTitle = wrapper.find(IssuableTitle); + const issuableTitle = wrapper.findComponent(IssuableTitle); issuableTitle.vm.$emit('edit-issuable'); @@ -223,7 +223,7 @@ describe('IssuableBody', () => { await nextTick(); - const issuableEditForm = wrapper.find(IssuableEditForm); + const issuableEditForm = wrapper.findComponent(IssuableEditForm); issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index d3e484cf913..d843da4da5b 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -124,7 +124,7 @@ describe('IssuableEditForm', () => { const titleInputEl = wrapper.find('[data-testid="title"]'); expect(titleInputEl.exists()).toBe(true); - expect(titleInputEl.find(GlFormInput).attributes()).toMatchObject({ + expect(titleInputEl.findComponent(GlFormInput).attributes()).toMatchObject({ 'aria-label': 'Title', placeholder: 'Title', }); @@ -134,7 +134,7 @@ describe('IssuableEditForm', () => { const descriptionEl = wrapper.find('[data-testid="description"]'); expect(descriptionEl.exists()).toBe(true); - expect(descriptionEl.find(MarkdownField).props()).toMatchObject({ + expect(descriptionEl.findComponent(MarkdownField).props()).toMatchObject({ markdownPreviewPath: issuableEditFormProps.descriptionPreviewPath, markdownDocsPath: issuableEditFormProps.descriptionHelpPath, enableAutocomplete: issuableEditFormProps.enableAutocomplete, @@ -161,7 +161,7 @@ describe('IssuableEditForm', () => { }; it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', async () => { - const titleInputEl = wrapper.find(GlFormInput); + const titleInputEl = wrapper.findComponent(GlFormInput); titleInputEl.vm.$emit('keydown', eventObj, 'title'); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js index e00bb184535..6a8b9ef77a9 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -86,7 +86,7 @@ describe('IssuableHeader', () => { const blockedEl = wrapper.findByTestId('blocked'); expect(blockedEl.exists()).toBe(true); - expect(blockedEl.find(GlIcon).props('name')).toBe('lock'); + expect(blockedEl.findComponent(GlIcon).props('name')).toBe('lock'); }); it('renders confidential icon when issuable is confidential', async () => { @@ -97,7 +97,7 @@ describe('IssuableHeader', () => { const confidentialEl = wrapper.findByTestId('confidential'); expect(confidentialEl.exists()).toBe(true); - expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash'); + expect(confidentialEl.findComponent(GlIcon).props('name')).toBe('eye-slash'); }); it('renders issuable author avatar', () => { @@ -113,19 +113,19 @@ describe('IssuableHeader', () => { const avatarEl = wrapper.findByTestId('avatar'); expect(avatarEl.exists()).toBe(true); expect(avatarEl.attributes()).toMatchObject(avatarElAttrs); - expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({ + expect(avatarEl.findComponent(GlAvatarLabeled).attributes()).toMatchObject({ size: '24', src: avatarUrl, label: name, }); - expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false); + expect(avatarEl.findComponent(GlAvatarLabeled).findComponent(GlIcon).exists()).toBe(false); }); it('renders task status text when `taskCompletionStatus` prop is defined', () => { createComponent(); expect(findTaskStatusEl().exists()).toBe(true); - expect(findTaskStatusEl().text()).toContain('0 of 5 tasks completed'); + expect(findTaskStatusEl().text()).toContain('0 of 5 checklist items completed'); }); it('does not render task status text when tasks count is 0', () => { @@ -172,7 +172,7 @@ describe('IssuableHeader', () => { ); const avatarEl = wrapper.findComponent(GlAvatarLabeled); - const icon = avatarEl.find(GlIcon); + const icon = avatarEl.findComponent(GlIcon); expect(icon.exists()).toBe(true); expect(icon.props('name')).toBe('external-link'); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js index f56064ed8e1..edfd55c8bb4 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js @@ -63,7 +63,7 @@ describe('IssuableShowRoot', () => { }); it('renders issuable-header component', () => { - const issuableHeader = wrapper.find(IssuableHeader); + const issuableHeader = wrapper.findComponent(IssuableHeader); expect(issuableHeader.exists()).toBe(true); expect(issuableHeader.props()).toMatchObject({ @@ -84,7 +84,7 @@ describe('IssuableShowRoot', () => { }); it('renders issuable-body component', () => { - const issuableBody = wrapper.find(IssuableBody); + const issuableBody = wrapper.findComponent(IssuableBody); expect(issuableBody.exists()).toBe(true); expect(issuableBody.props()).toMatchObject({ @@ -99,38 +99,38 @@ describe('IssuableShowRoot', () => { }); it('renders issuable-sidebar component', () => { - const issuableSidebar = wrapper.find(IssuableSidebar); + const issuableSidebar = wrapper.findComponent(IssuableSidebar); expect(issuableSidebar.exists()).toBe(true); }); describe('events', () => { it('component emits `edit-issuable` event bubbled via issuable-body', () => { - const issuableBody = wrapper.find(IssuableBody); + const issuableBody = wrapper.findComponent(IssuableBody); issuableBody.vm.$emit('edit-issuable'); - expect(wrapper.emitted('edit-issuable')).toBeTruthy(); + expect(wrapper.emitted('edit-issuable')).toHaveLength(1); }); it('component emits `task-list-update-success` event bubbled via issuable-body', () => { - const issuableBody = wrapper.find(IssuableBody); + const issuableBody = wrapper.findComponent(IssuableBody); const eventParam = { foo: 'bar', }; issuableBody.vm.$emit('task-list-update-success', eventParam); - expect(wrapper.emitted('task-list-update-success')).toBeTruthy(); + expect(wrapper.emitted('task-list-update-success')).toHaveLength(1); expect(wrapper.emitted('task-list-update-success')[0]).toEqual([eventParam]); }); it('component emits `task-list-update-failure` event bubbled via issuable-body', () => { - const issuableBody = wrapper.find(IssuableBody); + const issuableBody = wrapper.findComponent(IssuableBody); issuableBody.vm.$emit('task-list-update-failure'); - expect(wrapper.emitted('task-list-update-failure')).toBeTruthy(); + expect(wrapper.emitted('task-list-update-failure')).toHaveLength(1); }); it.each(['keydown-title', 'keydown-description'])( @@ -145,11 +145,11 @@ describe('IssuableShowRoot', () => { issuableDescription: 'foobar', }; - const issuableBody = wrapper.find(IssuableBody); + const issuableBody = wrapper.findComponent(IssuableBody); issuableBody.vm.$emit(eventName, eventObj, issuableMeta); - expect(wrapper.emitted(eventName)).toBeTruthy(); + expect(wrapper.emitted()).toHaveProperty(eventName); expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]); }, ); diff --git a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js index 4b75da0b126..5f2b13a79c9 100644 --- a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js +++ b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js @@ -12,8 +12,8 @@ describe('SecurityReportDownloadDropdown component', () => { }); }; - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js index 68a97103d3a..a9651cf8bac 100644 --- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js +++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js @@ -70,8 +70,8 @@ describe('Security reports app', () => { return createMockApollo(requestHandlers); }; - const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown); - const findHelpIconComponent = () => wrapper.find(HelpIcon); + const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); + const findHelpIconComponent = () => wrapper.findComponent(HelpIcon); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js index 945727cd664..de5a814d3e7 100644 --- a/spec/frontend/whats_new/components/app_spec.js +++ b/spec/frontend/whats_new/components/app_spec.js @@ -64,7 +64,7 @@ describe('App', () => { buildWrapper(); wrapper.vm.$store.state.features = [ - { title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 }, + { name: 'Whats New Drawer', documentation_link: 'www.url.com', release: 3.11 }, ]; wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT; await nextTick(); @@ -115,7 +115,7 @@ describe('App', () => { it('renders features when provided via ajax', () => { expect(actions.fetchItems).toHaveBeenCalled(); - expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer'); + expect(wrapper.find('[data-test-id="feature-name"]').text()).toBe('Whats New Drawer'); }); it('send an event when feature item is clicked', () => { diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js index b6627c257ff..099054bf8ca 100644 --- a/spec/frontend/whats_new/components/feature_spec.js +++ b/spec/frontend/whats_new/components/feature_spec.js @@ -6,14 +6,15 @@ describe("What's new single feature", () => { let wrapper; const exampleFeature = { - title: 'Compliance pipeline configurations', - body: + name: 'Compliance pipeline configurations', + description: '<p data-testid="body-content">We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding <a href="https://en.wikipedia.org/wiki/Compliance_(psychology)" target="_blank" rel="noopener noreferrer" onload="alert(xss)">compliance</a> framework.</p>', stage: 'Manage', 'self-managed': true, 'gitlab-com': true, - packages: ['Ultimate'], - url: 'https://docs.gitlab.com/ee/user/project/settings/#compliance-pipeline-configuration', + available_in: ['Ultimate'], + documentation_link: + 'https://docs.gitlab.com/ee/user/project/settings/#compliance-pipeline-configuration', image_url: 'https://img.youtube.com/vi/upLJ_equomw/hqdefault.jpg', published_at: '2021-04-22T00:00:00.000Z', release: '13.11', diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js index 79b76f3c061..c3cc2fbc556 100644 --- a/spec/frontend/work_items/components/item_state_spec.js +++ b/spec/frontend/work_items/components/item_state_spec.js @@ -1,3 +1,4 @@ +import { GlFormSelect } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants'; import ItemState from '~/work_items/components/item_state.vue'; @@ -6,6 +7,7 @@ describe('ItemState', () => { let wrapper; const findLabel = () => wrapper.find('label').text(); + const findFormSelect = () => wrapper.findComponent(GlFormSelect); const selectedValue = () => wrapper.find('option:checked').element.value; const clickOpen = () => wrapper.findAll('option').at(0).setSelected(); @@ -51,4 +53,18 @@ describe('ItemState', () => { expect(wrapper.emitted('changed')).toBeUndefined(); }); + + describe('form select disabled prop', () => { + describe.each` + description | disabled | value + ${'when not disabled'} | ${false} | ${undefined} + ${'when disabled'} | ${true} | ${'disabled'} + `('$description', ({ disabled, value }) => { + it(`renders form select component with disabled=${value}`, () => { + createComponent({ disabled }); + + expect(findFormSelect().attributes('disabled')).toBe(value); + }); + }); + }); }); diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index a55f448c9a2..de20369eb1b 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -37,7 +37,7 @@ describe('ItemTitle', () => { disabled: true, }); - expect(wrapper.classes()).toContain('gl-cursor-not-allowed'); + expect(wrapper.classes()).toContain('gl-cursor-text'); expect(findInputEl().attributes('contenteditable')).toBe('false'); }); diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js index 137a0a7326d..a1f1d47ab90 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -1,5 +1,5 @@ -import { GlDropdownItem, GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; describe('WorkItemActions component', () => { @@ -7,12 +7,19 @@ describe('WorkItemActions component', () => { let glModalDirective; const findModal = () => wrapper.findComponent(GlModal); - const findDeleteButton = () => wrapper.findComponent(GlDropdownItem); + const findConfidentialityToggleButton = () => + wrapper.findByTestId('confidentiality-toggle-action'); + const findDeleteButton = () => wrapper.findByTestId('delete-action'); - const createComponent = ({ canDelete = true } = {}) => { + const createComponent = ({ + canUpdate = true, + canDelete = true, + isConfidential = false, + isParentConfidential = false, + } = {}) => { glModalDirective = jest.fn(); - wrapper = shallowMount(WorkItemActions, { - propsData: { workItemId: '123', canDelete }, + wrapper = shallowMountExtended(WorkItemActions, { + propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential }, directives: { glModal: { bind(_, { value }) { @@ -34,27 +41,69 @@ describe('WorkItemActions component', () => { expect(findModal().props('visible')).toBe(false); }); - it('shows confirm modal when clicking Delete work item', () => { + it('renders dropdown actions', () => { createComponent(); - findDeleteButton().vm.$emit('click'); - - expect(glModalDirective).toHaveBeenCalled(); + expect(findConfidentialityToggleButton().exists()).toBe(true); + expect(findDeleteButton().exists()).toBe(true); }); - it('emits event when clicking OK button', () => { - createComponent(); + describe('toggle confidentiality action', () => { + it.each` + isConfidential | buttonText + ${true} | ${'Turn off confidentiality'} + ${false} | ${'Turn on confidentiality'} + `( + 'renders confidentiality toggle button with text "$buttonText"', + ({ isConfidential, buttonText }) => { + createComponent({ isConfidential }); + + expect(findConfidentialityToggleButton().text()).toBe(buttonText); + }, + ); + + it('emits `toggleWorkItemConfidentiality` event when clicked', () => { + createComponent(); - findModal().vm.$emit('ok'); + findConfidentialityToggleButton().vm.$emit('click'); - expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]); + expect(wrapper.emitted('toggleWorkItemConfidentiality')[0]).toEqual([true]); + }); + + it.each` + props | propName | value + ${{ isParentConfidential: true }} | ${'isParentConfidential'} | ${true} + ${{ canUpdate: false }} | ${'canUpdate'} | ${false} + `('does not render when $propName is $value', ({ props }) => { + createComponent(props); + + expect(findConfidentialityToggleButton().exists()).toBe(false); + }); }); - it('does not render when canDelete is false', () => { - createComponent({ - canDelete: false, + describe('delete action', () => { + it('shows confirm modal when clicked', () => { + createComponent(); + + findDeleteButton().vm.$emit('click'); + + expect(glModalDirective).toHaveBeenCalled(); + }); + + it('emits event when clicking OK button', () => { + createComponent(); + + findModal().vm.$emit('ok'); + + expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]); }); - expect(wrapper.html()).toBe(''); + it('does not render when canDelete is false', () => { + createComponent({ + canDelete: false, + }); + + expect(wrapper.findByTestId('delete-action').exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js index 299949a4baa..f0ef8aee7a9 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -5,14 +5,15 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mockTracking } from 'helpers/tracking_helper'; -import { stripTypenames } from 'helpers/graphql_helpers'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import { i18n, TASK_TYPE_NAME, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import { temporaryConfig, resolvers } from '~/work_items/graphql/provider'; +import { temporaryConfig } from '~/work_items/graphql/provider'; import { projectMembersResponseWithCurrentUser, mockAssignees, @@ -20,6 +21,7 @@ import { currentUserResponse, currentUserNullResponse, projectMembersResponseWithoutCurrentUser, + updateWorkItemMutationResponse, } from '../mock_data'; Vue.use(VueApollo); @@ -33,6 +35,7 @@ describe('WorkItemAssignees component', () => { const findAssigneeLinks = () => wrapper.findAllComponents(GlLink); const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger); const findEmptyState = () => wrapper.findByTestId('empty-state'); const findAssignSelfButton = () => wrapper.findByTestId('assign-self'); @@ -43,6 +46,9 @@ describe('WorkItemAssignees component', () => { .mockResolvedValue(projectMembersResponseWithCurrentUser); const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse); const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse); + const successUpdateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponse); const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); @@ -50,15 +56,18 @@ describe('WorkItemAssignees component', () => { assignees = mockAssignees, searchQueryHandler = successSearchQueryHandler, currentUserQueryHandler = successCurrentUserQueryHandler, + updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, allowsMultipleAssignees = true, + canInviteMembers = false, canUpdate = true, } = {}) => { const apolloProvider = createMockApollo( [ [userSearchQuery, searchQueryHandler], [currentUserQuery, currentUserQueryHandler], + [updateWorkItemMutation, updateWorkItemMutationHandler], ], - resolvers, + {}, { typePolicies: temporaryConfig.cacheConfig.typePolicies, }, @@ -82,6 +91,7 @@ describe('WorkItemAssignees component', () => { allowsMultipleAssignees, workItemType: TASK_TYPE_NAME, canUpdate, + canInviteMembers, }, attachTo: document.body, apolloProvider, @@ -120,15 +130,6 @@ describe('WorkItemAssignees component', () => { expect(findTokenSelector().element.contains(document.activeElement)).toBe(true); }); - it('calls a mutation on clicking outside the token selector', async () => { - createComponent(); - findTokenSelector().vm.$emit('input', [mockAssignees[0]]); - findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); - await waitForPromises(); - - expect(findTokenSelector().props('selectedTokens')).toEqual([mockAssignees[0]]); - }); - it('passes `false` to `viewOnly` token selector prop if user can update assignees', () => { createComponent(); @@ -141,6 +142,36 @@ describe('WorkItemAssignees component', () => { expect(findTokenSelector().props('viewOnly')).toBe(true); }); + describe('when clicking outside the token selector', () => { + function arrange(args) { + createComponent(args); + findTokenSelector().vm.$emit('input', [mockAssignees[0]]); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + } + + it('calls a mutation with correct variables', () => { + arrange({ assignees: [] }); + + expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + assigneesWidget: { assigneeIds: [mockAssignees[0].id] }, + id: 'gid://gitlab/WorkItem/1', + }, + }); + }); + + it('emits an error and resets assignees if mutation was rejected', async () => { + arrange({ updateWorkItemMutationHandler: errorHandler, assignees: [mockAssignees[1]] }); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); + expect(findTokenSelector().props('selectedTokens')).toEqual([ + { ...mockAssignees[1], class: expect.anything() }, + ]); + }); + }); + describe('when searching for users', () => { beforeEach(() => { createComponent(); @@ -204,7 +235,7 @@ describe('WorkItemAssignees component', () => { expect(findTokenSelector().props('dropdownItems')).toHaveLength(2); }); - it('should search for users with correct key after text input', async () => { + it('searches for users with correct key after text input', async () => { const searchKey = 'Hello'; findTokenSelector().vm.$emit('focus'); @@ -225,6 +256,18 @@ describe('WorkItemAssignees component', () => { expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]); }); + it('updates localAssignees when assignees prop is updated', async () => { + createComponent({ assignees: [] }); + + expect(findTokenSelector().props('selectedTokens')).toEqual([]); + + await wrapper.setProps({ assignees: [mockAssignees[0]] }); + + expect(findTokenSelector().props('selectedTokens')).toEqual([ + { ...mockAssignees[0], class: expect.anything() }, + ]); + }); + describe('when assigning to current user', () => { it('does not show `Assign myself` button if current user is loading', () => { createComponent(); @@ -261,23 +304,21 @@ describe('WorkItemAssignees component', () => { expect(findAssignSelfButton().exists()).toBe(true); }); - it('calls update work item assignees mutation with current user as a variable on button click', () => { - // TODO: replace this test as soon as we have a real mutation implemented - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementation(jest.fn()); - + it('calls update work item assignees mutation with current user as a variable on button click', async () => { + const { currentUser } = currentUserResponse.data; findTokenSelector().trigger('mouseover'); findAssignSelfButton().vm.$emit('click', new MouseEvent('click')); + await nextTick(); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - assignees: [stripTypenames(currentUserResponse.data.currentUser)], - id: workItemId, - }, + expect(findTokenSelector().props('selectedTokens')).toMatchObject([currentUser]); + expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + assigneesWidget: { + assigneeIds: [currentUser.id], }, - }), - ); + }, + }); }); }); @@ -286,9 +327,7 @@ describe('WorkItemAssignees component', () => { await waitForPromises(); expect(findTokenSelector().props('dropdownItems')[0]).toEqual( - expect.objectContaining({ - ...stripTypenames(currentUserResponse.data.currentUser), - }), + expect.objectContaining(currentUserResponse.data.currentUser), ); }); @@ -303,9 +342,10 @@ describe('WorkItemAssignees component', () => { }); it('adds current user to the top of dropdown items', () => { - expect(findTokenSelector().props('dropdownItems')[0]).toEqual( - stripTypenames(currentUserResponse.data.currentUser), - ); + expect(findTokenSelector().props('dropdownItems')[0]).toEqual({ + ...currentUserResponse.data.currentUser, + class: expect.anything(), + }); }); it('does not add current user if search is not empty', async () => { @@ -313,7 +353,7 @@ describe('WorkItemAssignees component', () => { await waitForPromises(); expect(findTokenSelector().props('dropdownItems')[0]).not.toEqual( - stripTypenames(currentUserResponse.data.currentUser), + currentUserResponse.data.currentUser, ); }); }); @@ -405,4 +445,18 @@ describe('WorkItemAssignees component', () => { }); }); }); + + describe('invite members', () => { + it('does not render `Invite members` link if user has no permission to invite members', () => { + createComponent(); + + expect(findInviteMembersTrigger().exists()).toBe(false); + }); + + it('renders `Invite members` link if user has a permission to invite members', () => { + createComponent({ canInviteMembers: true }); + + expect(findInviteMembersTrigger().exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js index 70b1261bdb7..01891012f99 100644 --- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -7,6 +7,13 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql'; +import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql'; +import { + deleteWorkItemFromTaskMutationErrorResponse, + deleteWorkItemFromTaskMutationResponse, + deleteWorkItemMutationErrorResponse, + deleteWorkItemResponse, +} from '../mock_data'; describe('WorkItemDetailModal component', () => { let wrapper; @@ -25,28 +32,38 @@ describe('WorkItemDetailModal component', () => { }, }; + const defaultPropsData = { + issueGid: 'gid://gitlab/WorkItem/1', + workItemId: 'gid://gitlab/WorkItem/2', + }; + const findModal = () => wrapper.findComponent(GlModal); const findAlert = () => wrapper.findComponent(GlAlert); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); - const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => { + const createComponent = ({ + lockVersion, + lineNumberStart, + lineNumberEnd, + error = false, + deleteWorkItemFromTaskMutationHandler = jest + .fn() + .mockResolvedValue(deleteWorkItemFromTaskMutationResponse), + deleteWorkItemMutationHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse), + } = {}) => { const apolloProvider = createMockApollo([ - [ - deleteWorkItemFromTaskMutation, - jest.fn().mockResolvedValue({ - data: { - workItemDeleteTask: { - workItem: { id: 123, descriptionHtml: 'updated work item desc' }, - errors: [], - }, - }, - }), - ], + [deleteWorkItemFromTaskMutation, deleteWorkItemFromTaskMutationHandler], + [deleteWorkItemMutation, deleteWorkItemMutationHandler], ]); wrapper = shallowMount(WorkItemDetailModal, { apolloProvider, - propsData: { workItemId, issueGid }, + propsData: { + ...defaultPropsData, + lockVersion, + lineNumberStart, + lineNumberEnd, + }, data() { return { error, @@ -67,8 +84,8 @@ describe('WorkItemDetailModal component', () => { expect(findWorkItemDetail().props()).toEqual({ isModal: true, - workItemId: '1', - workItemParentId: '2', + workItemId: defaultPropsData.workItemId, + workItemParentId: defaultPropsData.issueGid, }); }); @@ -109,16 +126,85 @@ describe('WorkItemDetailModal component', () => { }); describe('delete work item', () => { - it('emits workItemDeleted and closes modal', async () => { - createComponent(); - const newDesc = 'updated work item desc'; - - findWorkItemDetail().vm.$emit('deleteWorkItem'); - - await waitForPromises(); + describe('when there is task data', () => { + it('emits workItemDeleted and closes modal', async () => { + const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationResponse); + createComponent({ + lockVersion: 1, + lineNumberStart: '3', + lineNumberEnd: '3', + deleteWorkItemFromTaskMutationHandler: mutationMock, + }); + const newDesc = 'updated work item desc'; + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]); + expect(hideModal).toHaveBeenCalled(); + expect(mutationMock).toHaveBeenCalledWith({ + input: { + id: defaultPropsData.issueGid, + lockVersion: 1, + taskData: { id: defaultPropsData.workItemId, lineNumberEnd: 3, lineNumberStart: 3 }, + }, + }); + }); + + it.each` + errorType | mutationMock | errorMessage + ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationErrorResponse)} | ${'Error'} + ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'} + `( + 'shows an error message when there is $errorType', + async ({ mutationMock, errorMessage }) => { + createComponent({ + lockVersion: 1, + lineNumberStart: '3', + lineNumberEnd: '3', + deleteWorkItemFromTaskMutationHandler: mutationMock, + }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).toBeUndefined(); + expect(hideModal).not.toHaveBeenCalled(); + expect(findAlert().text()).toBe(errorMessage); + }, + ); + }); - expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]); - expect(hideModal).toHaveBeenCalled(); + describe('when there is no task data', () => { + it('emits workItemDeleted and closes modal', async () => { + const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse); + createComponent({ deleteWorkItemMutationHandler: mutationMock }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).toEqual([[defaultPropsData.workItemId]]); + expect(hideModal).toHaveBeenCalled(); + expect(mutationMock).toHaveBeenCalledWith({ input: { id: defaultPropsData.workItemId } }); + }); + + it.each` + errorType | mutationMock | errorMessage + ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'} + ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'} + `( + 'shows an error message when there is $errorType', + async ({ mutationMock, errorMessage }) => { + createComponent({ deleteWorkItemMutationHandler: mutationMock }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).toBeUndefined(); + expect(hideModal).not.toHaveBeenCalled(); + expect(findAlert().text()).toBe(errorMessage); + }, + ); }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index 93bf7286aa7..434c1db8a2c 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -1,13 +1,20 @@ import Vue from 'vue'; -import { GlForm, GlFormCombobox } from '@gitlab/ui'; +import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; +import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; +import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; -import { availableWorkItemsResponse, updateWorkItemMutationResponse } from '../../mock_data'; +import { + availableWorkItemsResponse, + projectWorkItemTypesQueryResponse, + createWorkItemMutationResponse, + updateWorkItemMutationResponse, +} from '../../mock_data'; Vue.use(VueApollo); @@ -15,14 +22,21 @@ describe('WorkItemLinksForm', () => { let wrapper; const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); + const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse); - const createComponent = async ({ listResponse = availableWorkItemsResponse } = {}) => { + const createComponent = async ({ + listResponse = availableWorkItemsResponse, + typesResponse = projectWorkItemTypesQueryResponse, + parentConfidential = false, + } = {}) => { wrapper = shallowMountExtended(WorkItemLinksForm, { apolloProvider: createMockApollo([ [projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)], + [projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)], [updateWorkItemMutation, updateMutationResolver], + [createWorkItemMutation, createMutationResolver], ]), - propsData: { issuableGid: 'gid://gitlab/WorkItem/1' }, + propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential }, provide: { projectPath: 'project/path', }, @@ -33,6 +47,7 @@ describe('WorkItemLinksForm', () => { const findForm = () => wrapper.findComponent(GlForm); const findCombobox = () => wrapper.findComponent(GlFormCombobox); + const findInput = () => wrapper.findComponent(GlFormInput); const findAddChildButton = () => wrapper.findByTestId('add-child-button'); beforeEach(async () => { @@ -47,19 +62,73 @@ describe('WorkItemLinksForm', () => { expect(findForm().exists()).toBe(true); }); - it('passes available work items as prop when typing in combobox', async () => { - findCombobox().vm.$emit('input', 'Task'); + it('creates child task in non confidential parent', async () => { + findInput().vm.$emit('input', 'Create task test'); + + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalledWith({ + input: { + title: 'Create task test', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: false, + }, + }); + }); + + it('creates child task in confidential parent', async () => { + await createComponent({ parentConfidential: true }); + + findInput().vm.$emit('input', 'Create confidential task'); - expect(findCombobox().exists()).toBe(true); - expect(findCombobox().props('tokenList').length).toBe(2); + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalledWith({ + input: { + title: 'Create confidential task', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: true, + }, + }); }); - it('selects and add child', async () => { + // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 + // eslint-disable-next-line jest/no-disabled-tests + it.skip('selects and add child', async () => { findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]); findAddChildButton().vm.$emit('click'); await waitForPromises(); expect(updateMutationResolver).toHaveBeenCalled(); }); + + // eslint-disable-next-line jest/no-disabled-tests + describe.skip('when typing in combobox', () => { + beforeEach(async () => { + findCombobox().vm.$emit('input', 'Task'); + await waitForPromises(); + await jest.runOnlyPendingTimers(); + }); + + it('passes available work items as prop', () => { + expect(findCombobox().exists()).toBe(true); + expect(findCombobox().props('tokenList').length).toBe(2); + }); + + it('passes action to create task', () => { + expect(findCombobox().props('actionList').length).toBe(1); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js index f8471b7f167..287ec022d3f 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js @@ -1,75 +1,24 @@ -import Vue from 'vue'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { cloneDeep } from 'lodash'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; + import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; -import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql'; -import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; -import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants'; -import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data'; - -Vue.use(VueApollo); - -const PARENT_ID = 'gid://gitlab/WorkItem/1'; -const WORK_ITEM_ID = 'gid://gitlab/WorkItem/3'; describe('WorkItemLinksMenu', () => { let wrapper; - let mockApollo; - - const $toast = { - show: jest.fn(), - }; - - const createComponent = async ({ - data = {}, - mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse), - } = {}) => { - mockApollo = createMockApollo([ - [getWorkItemLinksQuery, jest.fn().mockResolvedValue(workItemHierarchyResponse)], - [changeWorkItemParentMutation, mutationHandler], - ]); - - mockApollo.clients.defaultClient.cache.writeQuery({ - query: getWorkItemLinksQuery, - variables: { - id: PARENT_ID, - }, - data: workItemHierarchyResponse.data, - }); - wrapper = shallowMountExtended(WorkItemLinksMenu, { - data() { - return { - ...data, - }; - }, - propsData: { - workItemId: WORK_ITEM_ID, - parentWorkItemId: PARENT_ID, - }, - apolloProvider: mockApollo, - mocks: { - $toast, - }, - }); - - await waitForPromises(); + const createComponent = () => { + wrapper = shallowMountExtended(WorkItemLinksMenu); }; const findDropdown = () => wrapper.find(GlDropdown); const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem); beforeEach(async () => { - await createComponent(); + createComponent(); }); afterEach(() => { wrapper.destroy(); - mockApollo = null; }); it('renders dropdown and dropdown items', () => { @@ -77,65 +26,9 @@ describe('WorkItemLinksMenu', () => { expect(findRemoveDropdownItem().exists()).toBe(true); }); - it('calls correct mutation with correct variables', async () => { - const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse); - - createComponent({ mutationHandler }); - - findRemoveDropdownItem().vm.$emit('click'); - - await waitForPromises(); - - expect(mutationHandler).toHaveBeenCalledWith({ - id: WORK_ITEM_ID, - parentId: null, - }); - }); - - it('shows toast when mutation succeeds', async () => { - const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse); - - createComponent({ mutationHandler }); - - findRemoveDropdownItem().vm.$emit('click'); - - await waitForPromises(); - - expect($toast.show).toHaveBeenCalledWith('Child removed', { - action: { onClick: expect.anything(), text: 'Undo' }, - }); - }); - - it('updates the cache when mutation succeeds', async () => { - const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse); - - createComponent({ mutationHandler }); - - mockApollo.clients.defaultClient.cache.readQuery = jest.fn( - () => workItemHierarchyResponse.data, - ); - - mockApollo.clients.defaultClient.cache.writeQuery = jest.fn(); - + it('emits removeChild event on click Remove', () => { findRemoveDropdownItem().vm.$emit('click'); - await waitForPromises(); - - // Remove the work item from parent's children - const resp = cloneDeep(workItemHierarchyResponse); - const index = resp.data.workItem.widgets - .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) - .children.nodes.findIndex((child) => child.id === WORK_ITEM_ID); - resp.data.workItem.widgets - .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) - .children.nodes.splice(index, 1); - - expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.anything(), - variables: { id: PARENT_ID }, - data: resp.data, - }), - ); + expect(wrapper.emitted('removeChild')).toHaveLength(1); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index 2ec9b1ec0ac..00f508f1548 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -1,34 +1,85 @@ import Vue, { nextTick } from 'vue'; -import { GlBadge } from '@gitlab/ui'; +import { GlButton, GlIcon, GlAlert } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import SidebarEventHub from '~/sidebar/event_hub'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; -import { workItemHierarchyResponse, workItemHierarchyEmptyResponse } from '../../mock_data'; +import { + workItemHierarchyResponse, + workItemHierarchyEmptyResponse, + workItemHierarchyNoUpdatePermissionResponse, + changeWorkItemParentMutationResponse, + workItemQueryResponse, +} from '../../mock_data'; Vue.use(VueApollo); describe('WorkItemLinks', () => { let wrapper; + let mockApollo; + + const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; + + const $toast = { + show: jest.fn(), + }; + + const mutationChangeParentHandler = jest + .fn() + .mockResolvedValue(changeWorkItemParentMutationResponse); + + const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + + const findChildren = () => wrapper.findAll('[data-testid="links-child"]'); + + const createComponent = async ({ + data = {}, + fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse), + mutationHandler = mutationChangeParentHandler, + } = {}) => { + mockApollo = createMockApollo( + [ + [getWorkItemLinksQuery, fetchHandler], + [changeWorkItemParentMutation, mutationHandler], + [workItemQuery, childWorkItemQueryHandler], + ], + {}, + { addTypename: true }, + ); - const createComponent = async ({ response = workItemHierarchyResponse } = {}) => { wrapper = shallowMountExtended(WorkItemLinks, { - apolloProvider: createMockApollo([ - [getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)], - ]), + data() { + return { + ...data, + }; + }, + provide: { + projectPath: 'project/path', + }, propsData: { issuableId: 1 }, + apolloProvider: mockApollo, + mocks: { + $toast, + }, }); await waitForPromises(); }; + const findAlert = () => wrapper.findComponent(GlAlert); const findToggleButton = () => wrapper.findByTestId('toggle-links'); const findLinksBody = () => wrapper.findByTestId('links-body'); const findEmptyState = () => wrapper.findByTestId('links-empty'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); + const findFirstLinksMenu = () => wrapper.findByTestId('links-menu'); + const findChildrenCount = () => wrapper.findByTestId('children-count'); beforeEach(async () => { await createComponent(); @@ -36,6 +87,7 @@ describe('WorkItemLinks', () => { afterEach(() => { wrapper.destroy(); + mockApollo = null; }); it('is expanded by default', () => { @@ -43,7 +95,7 @@ describe('WorkItemLinks', () => { expect(findLinksBody().exists()).toBe(true); }); - it('expands on click toggle button', async () => { + it('collapses on click toggle button', async () => { findToggleButton().vm.$emit('click'); await nextTick(); @@ -67,7 +119,9 @@ describe('WorkItemLinks', () => { describe('when no child links', () => { beforeEach(async () => { - await createComponent({ response: workItemHierarchyEmptyResponse }); + await createComponent({ + fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyEmptyResponse), + }); }); it('displays empty state if there are no children', () => { @@ -78,9 +132,140 @@ describe('WorkItemLinks', () => { it('renders all hierarchy widget children', () => { expect(findLinksBody().exists()).toBe(true); + expect(findChildren()).toHaveLength(4); + expect(findFirstLinksMenu().exists()).toBe(true); + }); + + it('shows alert when list loading fails', async () => { + const errorMessage = 'Some error'; + await createComponent({ + fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)), + }); + + await nextTick(); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(errorMessage); + }); + + it('renders widget child icon and tooltip', () => { + expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m'); + expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close'); + }); + + it('renders confidentiality icon when child item is confidential', () => { const children = wrapper.findAll('[data-testid="links-child"]'); + const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]'); + + expect(confidentialIcon.exists()).toBe(true); + expect(confidentialIcon.props('name')).toBe('eye-slash'); + }); + + it('displays number if children', () => { + expect(findChildrenCount().exists()).toBe(true); + + expect(findChildrenCount().text()).toContain('4'); + }); + + it('refetches child items when `confidentialityUpdated` event is emitted on SidebarEventhub', async () => { + const fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse); + await createComponent({ + fetchHandler, + }); + await waitForPromises(); + + SidebarEventHub.$emit('confidentialityUpdated'); + await nextTick(); + + // First call is done on component mount. + // Second call is done on confidentialityUpdated event. + expect(fetchHandler).toHaveBeenCalledTimes(2); + }); + + describe('when no permission to update', () => { + beforeEach(async () => { + await createComponent({ + fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyNoUpdatePermissionResponse), + }); + }); - expect(children).toHaveLength(4); - expect(children.at(0).findComponent(GlBadge).text()).toBe('Open'); + it('does not display button to toggle Add form', () => { + expect(findToggleAddFormButton().exists()).toBe(false); + }); + + it('does not display link menu on children', () => { + expect(findFirstLinksMenu().exists()).toBe(false); + }); + }); + + describe('remove child', () => { + beforeEach(async () => { + await createComponent({ mutationHandler: mutationChangeParentHandler }); + }); + + it('calls correct mutation with correct variables', async () => { + findFirstLinksMenu().vm.$emit('removeChild'); + + await waitForPromises(); + + expect(mutationChangeParentHandler).toHaveBeenCalledWith({ + input: { + id: WORK_ITEM_ID, + hierarchyWidget: { + parentId: null, + }, + }, + }); + }); + + it('shows toast when mutation succeeds', async () => { + findFirstLinksMenu().vm.$emit('removeChild'); + + await waitForPromises(); + + expect($toast.show).toHaveBeenCalledWith('Child removed', { + action: { onClick: expect.anything(), text: 'Undo' }, + }); + }); + + it('renders correct number of children after removal', async () => { + expect(findChildren()).toHaveLength(4); + + findFirstLinksMenu().vm.$emit('removeChild'); + await waitForPromises(); + + expect(findChildren()).toHaveLength(3); + }); + }); + + describe('prefetching child items', () => { + beforeEach(async () => { + await createComponent(); + }); + + const findChildLink = () => findChildren().at(0).findComponent(GlButton); + + it('does not fetch the child work item before hovering work item links', () => { + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); + + it('fetches the child work item if link is hovered for 250+ ms', async () => { + findChildLink().vm.$emit('mouseover'); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemQueryHandler).toHaveBeenCalledWith({ + id: 'gid://gitlab/WorkItem/2', + }); + }); + + it('does not fetch the child work item if link is hovered for less than 250 ms', async () => { + findChildLink().vm.$emit('mouseover'); + jest.advanceTimersByTime(200); + findChildLink().vm.$emit('mouseout'); + await waitForPromises(); + + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js index b379d1fc846..6b23a6e4795 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -29,6 +29,7 @@ describe('WorkItemState component', () => { const createComponent = ({ state = STATE_OPEN, mutationHandler = mutationSuccessHandler, + canUpdate = true, } = {}) => { const { id, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemState, { @@ -39,6 +40,7 @@ describe('WorkItemState component', () => { state, workItemType, }, + canUpdate, }, }); }; @@ -53,6 +55,20 @@ describe('WorkItemState component', () => { expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state); }); + describe('item state disabled prop', () => { + describe.each` + description | canUpdate | value + ${'when cannot update'} | ${false} | ${true} + ${'when can update'} | ${true} | ${false} + `('$description', ({ canUpdate, value }) => { + it(`renders item state component with disabled=${value}`, () => { + createComponent({ canUpdate }); + + expect(findItemState().props('disabled')).toBe(value); + }); + }); + }); + describe('when updating the state', () => { it('calls a mutation', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index a48449bb636..c0d966abab8 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -20,7 +20,11 @@ describe('WorkItemTitle component', () => { const findItemTitle = () => wrapper.findComponent(ItemTitle); - const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => { + const createComponent = ({ + workItemParentId, + mutationHandler = mutationSuccessHandler, + canUpdate = true, + } = {}) => { const { id, title, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemTitle, { apolloProvider: createMockApollo([ @@ -32,6 +36,7 @@ describe('WorkItemTitle component', () => { workItemTitle: title, workItemType: workItemType.name, workItemParentId, + canUpdate, }, }); }; @@ -46,6 +51,20 @@ describe('WorkItemTitle component', () => { expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title); }); + describe('item title disabled prop', () => { + describe.each` + description | canUpdate | value + ${'when cannot update'} | ${false} | ${true} + ${'when can update'} | ${true} | ${false} + `('$description', ({ canUpdate, value }) => { + it(`renders item title component with disabled=${value}`, () => { + createComponent({ canUpdate }); + + expect(findItemTitle().props('disabled')).toBe(value); + }); + }); + }); + describe('when updating the title', () => { it('calls a mutation', () => { const title = 'new title!'; diff --git a/spec/frontend/work_items/components/work_item_type_icon_spec.js b/spec/frontend/work_items/components/work_item_type_icon_spec.js new file mode 100644 index 00000000000..85466578e18 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js @@ -0,0 +1,47 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; + +let wrapper; + +function createComponent(propsData) { + wrapper = shallowMount(WorkItemTypeIcon, { propsData }); +} + +describe('Work Item type component', () => { + const findIcon = () => wrapper.findComponent(GlIcon); + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + workItemType | workItemIconName | iconName | text + ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} + ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} + ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} + ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} + ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} + ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} + ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} + ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} + `( + 'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"', + ({ workItemType, workItemIconName, iconName, text }) => { + beforeEach(() => { + createComponent({ + workItemType, + workItemIconName, + }); + }); + + it(`renders icon with name '${iconName}'`, () => { + expect(findIcon().props('name')).toBe(iconName); + }); + + it(`renders correct text`, () => { + expect(wrapper.text()).toBe(text); + }); + }, + ); +}); diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js index c3bbea26cda..94bdb336deb 100644 --- a/spec/frontend/work_items/components/work_item_weight_spec.js +++ b/spec/frontend/work_items/components/work_item_weight_spec.js @@ -1,16 +1,21 @@ import { GlForm, GlFormInput } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { mockTracking } from 'helpers/tracking_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { __ } from '~/locale'; import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql'; +import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data'; describe('WorkItemWeight component', () => { + Vue.use(VueApollo); + let wrapper; - const mutateSpy = jest.fn(); const workItemId = 'gid://gitlab/WorkItem/1'; const workItemType = 'Task'; @@ -22,8 +27,10 @@ describe('WorkItemWeight component', () => { hasIssueWeightsFeature = true, isEditing = false, weight, + mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse), } = {}) => { wrapper = mountExtended(WorkItemWeight, { + apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), propsData: { canUpdate, weight, @@ -33,11 +40,6 @@ describe('WorkItemWeight component', () => { provide: { hasIssueWeightsFeature, }, - mocks: { - $apollo: { - mutate: mutateSpy, - }, - }, }); if (isEditing) { @@ -131,26 +133,73 @@ describe('WorkItemWeight component', () => { }); describe('when blurred', () => { - it('calls a mutation to update the weight', () => { - const weight = 0; - createComponent({ isEditing: true, weight }); + it('calls a mutation to update the weight when the input value is different', () => { + const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); + createComponent({ + isEditing: true, + weight: 0, + mutationHandler: mutationSpy, + canUpdate: true, + }); + + findInput().vm.$emit('blur', { target: { value: 1 } }); + + expect(mutationSpy).toHaveBeenCalledWith({ + input: { + id: workItemId, + weightWidget: { + weight: 1, + }, + }, + }); + }); + + it('does not call a mutation to update the weight when the input value is the same', () => { + const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); + createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true }); findInput().trigger('blur'); - expect(mutateSpy).toHaveBeenCalledWith({ - mutation: localUpdateWorkItemMutation, - variables: { - input: { - id: workItemId, - weight, + expect(mutationSpy).not.toHaveBeenCalledWith(); + }); + + it('emits an error when there is a GraphQL error', async () => { + const response = { + data: { + workItemUpdate: { + errors: ['Error!'], + workItem: {}, }, }, + }; + createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockResolvedValue(response), + canUpdate: true, + }); + + findInput().trigger('blur'); + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); + }); + + it('emits an error when there is a network error', async () => { + createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockRejectedValue(new Error()), + canUpdate: true, }); + + findInput().trigger('blur'); + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); }); it('tracks updating the weight', () => { const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - createComponent(); + createComponent({ canUpdate: true }); findInput().trigger('blur'); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 0359caf7116..d24ac2a9f93 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -25,10 +25,14 @@ export const workItemQueryResponse = { title: 'Test', state: 'OPEN', description: 'description', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', + iconName: 'issue-type-task', }, userPermissions: { deleteWorkItem: false, @@ -46,6 +50,7 @@ export const workItemQueryResponse = { __typename: 'WorkItemWidgetAssignees', type: 'ASSIGNEES', allowsMultipleAssignees: true, + canInviteMembers: true, assignees: { nodes: mockAssignees, }, @@ -57,13 +62,14 @@ export const workItemQueryResponse = { id: 'gid://gitlab/Issue/1', iid: '5', title: 'Parent title', + confidential: false, }, children: { - edges: [ + nodes: [ { - node: { - id: 'gid://gitlab/WorkItem/444', - }, + id: 'gid://gitlab/WorkItem/444', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, }, ], }, @@ -77,16 +83,21 @@ export const updateWorkItemMutationResponse = { data: { workItemUpdate: { __typename: 'WorkItemUpdatePayload', + errors: [], workItem: { __typename: 'WorkItem', id: 'gid://gitlab/WorkItem/1', title: 'Updated title', state: 'OPEN', description: 'description', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', + iconName: 'issue-type-task', }, userPermissions: { deleteWorkItem: false, @@ -95,24 +106,46 @@ export const updateWorkItemMutationResponse = { widgets: [ { children: { - edges: [ + nodes: [ { - node: 'gid://gitlab/WorkItem/444', + id: 'gid://gitlab/WorkItem/444', }, ], }, }, + { + __typename: 'WorkItemWidgetAssignees', + type: 'ASSIGNEES', + allowsMultipleAssignees: true, + canInviteMembers: true, + assignees: { + nodes: [mockAssignees[0]], + }, + }, ], }, }, }, }; +export const mockParent = { + parent: { + id: 'gid://gitlab/Issue/1', + iid: '5', + title: 'Parent title', + confidential: false, + }, +}; + export const workItemResponseFactory = ({ canUpdate = false, + canDelete = false, allowsMultipleAssignees = true, assigneesWidgetPresent = true, - parent = null, + weightWidgetPresent = true, + confidential = false, + canInviteMembers = false, + parent = mockParent.parent, } = {}) => ({ data: { workItem: { @@ -121,13 +154,17 @@ export const workItemResponseFactory = ({ title: 'Updated title', state: 'OPEN', description: 'description', + confidential, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', + iconName: 'issue-type-task', }, userPermissions: { - deleteWorkItem: false, + deleteWorkItem: canDelete, updateWorkItem: canUpdate, }, widgets: [ @@ -143,20 +180,28 @@ export const workItemResponseFactory = ({ __typename: 'WorkItemWidgetAssignees', type: 'ASSIGNEES', allowsMultipleAssignees, + canInviteMembers, assignees: { nodes: mockAssignees, }, } : { type: 'MOCK TYPE' }, + weightWidgetPresent + ? { + __typename: 'WorkItemWidgetWeight', + type: 'WEIGHT', + weight: 0, + } + : { type: 'MOCK TYPE' }, { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', children: { - edges: [ + nodes: [ { - node: { - id: 'gid://gitlab/WorkItem/444', - }, + id: 'gid://gitlab/WorkItem/444', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, }, ], }, @@ -203,10 +248,14 @@ export const createWorkItemMutationResponse = { title: 'Updated title', state: 'OPEN', description: 'description', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', + iconName: 'issue-type-task', }, userPermissions: { deleteWorkItem: false, @@ -214,6 +263,7 @@ export const createWorkItemMutationResponse = { }, widgets: [], }, + errors: [], }, }, }; @@ -229,10 +279,14 @@ export const createWorkItemFromTaskMutationResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Updated title', state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', + iconName: 'issue-type-task', }, userPermissions: { deleteWorkItem: false, @@ -252,11 +306,15 @@ export const createWorkItemFromTaskMutationResponse = { id: 'gid://gitlab/WorkItem/1000000', title: 'Updated title', state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, description: '', + confidential: false, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', + iconName: 'issue-type-task', }, userPermissions: { deleteWorkItem: false, @@ -284,6 +342,32 @@ export const deleteWorkItemFailureResponse = { ], }; +export const deleteWorkItemMutationErrorResponse = { + data: { + workItemDelete: { + errors: ['Error'], + }, + }, +}; + +export const deleteWorkItemFromTaskMutationResponse = { + data: { + workItemDeleteTask: { + workItem: { id: 123, descriptionHtml: 'updated work item desc' }, + errors: [], + }, + }, +}; + +export const deleteWorkItemFromTaskMutationErrorResponse = { + data: { + workItemDeleteTask: { + workItem: { id: 123, descriptionHtml: 'updated work item desc' }, + errors: ['Error'], + }, + }, +}; + export const workItemTitleSubscriptionResponse = { data: { issuableTitleUpdated: { @@ -302,6 +386,13 @@ export const workItemHierarchyEmptyResponse = { __typename: 'WorkItemType', }, title: 'New title', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + userPermissions: { + deleteWorkItem: false, + updateWorkItem: false, + }, + confidential: false, widgets: [ { type: 'DESCRIPTION', @@ -322,6 +413,54 @@ export const workItemHierarchyEmptyResponse = { }, }; +export const workItemHierarchyNoUpdatePermissionResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/1', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + __typename: 'WorkItemType', + }, + title: 'New title', + userPermissions: { + deleteWorkItem: false, + updateWorkItem: false, + }, + confidential: false, + widgets: [ + { + type: 'DESCRIPTION', + __typename: 'WorkItemWidgetDescription', + }, + { + type: 'HIERARCHY', + parent: null, + children: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'xyz', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', + }, + ], + __typename: 'WorkItemConnection', + }, + __typename: 'WorkItemWidgetHierarchy', + }, + ], + __typename: 'WorkItem', + }, + }, +}; + export const workItemHierarchyResponse = { data: { workItem: { @@ -331,6 +470,11 @@ export const workItemHierarchyResponse = { __typename: 'WorkItemType', }, title: 'New title', + userPermissions: { + deleteWorkItem: true, + updateWorkItem: true, + }, + confidential: false, widgets: [ { type: 'DESCRIPTION', @@ -349,6 +493,9 @@ export const workItemHierarchyResponse = { }, title: 'xyz', state: 'OPEN', + confidential: true, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, __typename: 'WorkItem', }, { @@ -359,6 +506,9 @@ export const workItemHierarchyResponse = { }, title: 'abc', state: 'CLOSED', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: '2022-08-12T13:07:52Z', __typename: 'WorkItem', }, { @@ -369,6 +519,9 @@ export const workItemHierarchyResponse = { }, title: 'bar', state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, __typename: 'WorkItem', }, { @@ -379,6 +532,9 @@ export const workItemHierarchyResponse = { }, title: 'foobar', state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, __typename: 'WorkItem', }, ], @@ -396,14 +552,34 @@ export const changeWorkItemParentMutationResponse = { data: { workItemUpdate: { workItem: { - id: 'gid://gitlab/WorkItem/2', + __typename: 'WorkItem', workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/1', + name: 'Issue', + iconName: 'issue-type-issue', }, - title: 'Foo', + userPermissions: { + deleteWorkItem: true, + updateWorkItem: true, + }, + description: null, + id: 'gid://gitlab/WorkItem/2', state: 'OPEN', - __typename: 'WorkItem', + title: 'Foo', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + widgets: [ + { + __typename: 'WorkItemWidgetHierarchy', + type: 'HIERARCHY', + parent: null, + children: { + nodes: [], + }, + }, + ], }, errors: [], __typename: 'WorkItemUpdatePayload', @@ -423,6 +599,7 @@ export const availableWorkItemsResponse = { id: 'gid://gitlab/WorkItem/458', title: 'Task 1', state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', }, }, { @@ -430,6 +607,7 @@ export const availableWorkItemsResponse = { id: 'gid://gitlab/WorkItem/459', title: 'Task 2', state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', }, }, ], @@ -551,11 +729,3 @@ export const projectLabelsResponse = { }, }, }; - -export const mockParent = { - parent: { - id: 'gid://gitlab/Issue/1', - iid: '5', - title: 'Parent title', - }, -}; diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js index 43869468ad0..823981df880 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -1,11 +1,12 @@ -import { GlAlert, GlSkeletonLoader, GlButton } from '@gitlab/ui'; +import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; +import WorkItemActions from '~/work_items/components/work_item_actions.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; import WorkItemState from '~/work_items/components/work_item_state.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; @@ -16,6 +17,8 @@ import WorkItemInformation from '~/work_items/components/work_item_information.v import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import { temporaryConfig } from '~/work_items/graphql/provider'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { @@ -30,12 +33,19 @@ describe('WorkItemDetail component', () => { Vue.use(VueApollo); - const workItemQueryResponse = workItemResponseFactory(); + const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true }); + const workItemQueryResponseWithoutParent = workItemResponseFactory({ + parent: null, + canUpdate: true, + canDelete: true, + }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); const findAlert = () => wrapper.findComponent(GlAlert); const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findWorkItemActions = () => wrapper.findComponent(WorkItemActions); const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); const findWorkItemState = () => wrapper.findComponent(WorkItemState); const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription); @@ -51,17 +61,21 @@ describe('WorkItemDetail component', () => { const createComponent = ({ isModal = false, + updateInProgress = false, workItemId = workItemQueryResponse.data.workItem.id, handler = successHandler, subscriptionHandler = initialSubscriptionHandler, + confidentialityMock = [updateWorkItemMutation, jest.fn()], workItemsMvc2Enabled = false, includeWidgets = false, + error = undefined, } = {}) => { wrapper = shallowMount(WorkItemDetail, { apolloProvider: createMockApollo( [ [workItemQuery, handler], [workItemTitleSubscription, subscriptionHandler], + confidentialityMock, ], {}, { @@ -69,6 +83,12 @@ describe('WorkItemDetail component', () => { }, ), propsData: { isModal, workItemId }, + data() { + return { + updateInProgress, + error, + }; + }, provide: { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, @@ -146,6 +166,148 @@ describe('WorkItemDetail component', () => { }); }); + describe('confidentiality', () => { + const errorMessage = 'Mutation failed'; + const confidentialWorkItem = workItemResponseFactory({ + confidential: true, + }); + + // Mocks for work item without parent + const withoutParentExpectedInputVars = { + id: workItemQueryResponse.data.workItem.id, + confidential: true, + }; + const toggleConfidentialityWithoutParentHandler = jest.fn().mockResolvedValue({ + data: { + workItemUpdate: { + workItem: confidentialWorkItem.data.workItem, + errors: [], + }, + }, + }); + const withoutParentHandlerMock = jest + .fn() + .mockResolvedValue(workItemQueryResponseWithoutParent); + const confidentialityWithoutParentMock = [ + updateWorkItemMutation, + toggleConfidentialityWithoutParentHandler, + ]; + const confidentialityWithoutParentFailureMock = [ + updateWorkItemMutation, + jest.fn().mockRejectedValue(new Error(errorMessage)), + ]; + + // Mocks for work item with parent + const withParentExpectedInputVars = { + id: mockParent.parent.id, + taskData: { id: workItemQueryResponse.data.workItem.id, confidential: true }, + }; + const toggleConfidentialityWithParentHandler = jest.fn().mockResolvedValue({ + data: { + workItemUpdate: { + workItem: { + id: confidentialWorkItem.data.workItem.id, + descriptionHtml: confidentialWorkItem.data.workItem.description, + }, + task: { + workItem: confidentialWorkItem.data.workItem, + confidential: true, + }, + errors: [], + }, + }, + }); + const confidentialityWithParentMock = [ + updateWorkItemTaskMutation, + toggleConfidentialityWithParentHandler, + ]; + const confidentialityWithParentFailureMock = [ + updateWorkItemTaskMutation, + jest.fn().mockRejectedValue(new Error(errorMessage)), + ]; + + describe.each` + context | handlerMock | confidentialityMock | confidentialityFailureMock | inputVariables + ${'no parent'} | ${withoutParentHandlerMock} | ${confidentialityWithoutParentMock} | ${confidentialityWithoutParentFailureMock} | ${withoutParentExpectedInputVars} + ${'parent'} | ${successHandler} | ${confidentialityWithParentMock} | ${confidentialityWithParentFailureMock} | ${withParentExpectedInputVars} + `( + 'when work item has $context', + ({ handlerMock, confidentialityMock, confidentialityFailureMock, inputVariables }) => { + it('renders confidential badge when work item is confidential', async () => { + createComponent({ + handler: jest.fn().mockResolvedValue(confidentialWorkItem), + confidentialityMock, + }); + + await waitForPromises(); + + const confidentialBadge = wrapper.findComponent(GlBadge); + expect(confidentialBadge.exists()).toBe(true); + expect(confidentialBadge.props()).toMatchObject({ + variant: 'warning', + icon: 'eye-slash', + }); + expect(confidentialBadge.attributes('title')).toBe( + 'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.', + ); + expect(confidentialBadge.text()).toBe('Confidential'); + }); + + it('renders gl-loading-icon while update mutation is in progress', async () => { + createComponent({ + handler: handlerMock, + confidentialityMock, + }); + + await waitForPromises(); + + findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); + + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('emits workItemUpdated and shows confidentiality badge when mutation is successful', async () => { + createComponent({ + handler: handlerMock, + confidentialityMock, + }); + + await waitForPromises(); + + findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); + await waitForPromises(); + + expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]); + expect(confidentialityMock[1]).toHaveBeenCalledWith({ + input: inputVariables, + }); + expect(findLoadingIcon().exists()).toBe(false); + }); + + it('shows alert message when mutation fails', async () => { + createComponent({ + handler: handlerMock, + confidentialityMock: confidentialityFailureMock, + }); + + await waitForPromises(); + findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); + await waitForPromises(); + + expect(wrapper.emitted('workItemUpdated')).toBeFalsy(); + + await nextTick(); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(errorMessage); + expect(findLoadingIcon().exists()).toBe(false); + }); + }, + ); + }); + describe('description', () => { it('does not show description widget if loading description fails', () => { createComponent(); @@ -169,7 +331,7 @@ describe('WorkItemDetail component', () => { }); it('does not show secondary breadcrumbs if there is not a parent', async () => { - createComponent(); + createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) }); await waitForPromises(); @@ -177,7 +339,7 @@ describe('WorkItemDetail component', () => { }); it('shows work item type if there is not a parent', async () => { - createComponent(); + createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) }); await waitForPromises(); expect(findWorkItemType().exists()).toBe(true); @@ -276,34 +438,29 @@ describe('WorkItemDetail component', () => { }); describe('weight widget', () => { - describe('when work_items_mvc_2 feature flag is enabled', () => { - describe.each` - description | includeWidgets | exists - ${'when widget is returned from API'} | ${true} | ${true} - ${'when widget is not returned from API'} | ${false} | ${false} - `('$description', ({ includeWidgets, exists }) => { - it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => { - createComponent({ includeWidgets, workItemsMvc2Enabled: true }); - await waitForPromises(); + describe.each` + description | weightWidgetPresent | exists + ${'when widget is returned from API'} | ${true} | ${true} + ${'when widget is not returned from API'} | ${false} | ${false} + `('$description', ({ weightWidgetPresent, exists }) => { + it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => { + const response = workItemResponseFactory({ weightWidgetPresent }); + const handler = jest.fn().mockResolvedValue(response); + createComponent({ handler }); + await waitForPromises(); - expect(findWorkItemWeight().exists()).toBe(exists); - }); + expect(findWorkItemWeight().exists()).toBe(exists); }); }); - describe('when work_items_mvc_2 feature flag is disabled', () => { - describe.each` - description | includeWidgets | exists - ${'when widget is returned from API'} | ${true} | ${false} - ${'when widget is not returned from API'} | ${false} | ${false} - `('$description', ({ includeWidgets, exists }) => { - it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => { - createComponent({ includeWidgets, workItemsMvc2Enabled: false }); - await waitForPromises(); + it('shows an error message when it emits an `error` event', async () => { + createComponent({ workItemsMvc2Enabled: true }); + await waitForPromises(); - expect(findWorkItemWeight().exists()).toBe(exists); - }); - }); + findWorkItemWeight().vm.$emit('error', i18n.updateError); + await waitForPromises(); + + expect(findAlert().text()).toBe(i18n.updateError); }); }); diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js index 092e9c90553..1426fbfab80 100644 --- a/spec/frontend/work_items_hierarchy/components/app_spec.js +++ b/spec/frontend/work_items_hierarchy/components/app_spec.js @@ -1,19 +1,17 @@ -import { nextTick } from 'vue'; -import { createLocalVue, mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import { GlBanner } from '@gitlab/ui'; import App from '~/work_items_hierarchy/components/app.vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); describe('WorkItemsHierarchy App', () => { let wrapper; const createComponent = (props = {}, data = {}) => { wrapper = extendedWrapper( mount(App, { - localVue, provide: { illustrationPath: '/foo.svg', licensePlan: 'free', diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js index 74774e38d6b..67420e7fc2a 100644 --- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js +++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js @@ -1,4 +1,5 @@ -import { createLocalVue, mount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { GlBadge } from '@gitlab/ui'; import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue'; @@ -6,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import RESPONSE from '~/work_items_hierarchy/static_response'; import { workItemTypes } from '~/work_items_hierarchy/constants'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); describe('WorkItemsHierarchy Hierarchy', () => { let wrapper; @@ -32,7 +32,6 @@ describe('WorkItemsHierarchy Hierarchy', () => { const createComponent = (props = {}) => { wrapper = extendedWrapper( mount(Hierarchy, { - localVue, propsData: { workItemTypes: props.workItemTypes, ...props, |