diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-20 15:19:03 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-20 15:19:03 +0000 |
commit | 14bd84b61276ef29b97d23642d698de769bacfd2 (patch) | |
tree | f9eba90140c1bd874211dea17750a0d422c04080 /spec/frontend | |
parent | 891c388697b2db0d8ee0c8358a9bdbf6dc56d581 (diff) | |
download | gitlab-ce-14bd84b61276ef29b97d23642d698de769bacfd2.tar.gz |
Add latest changes from gitlab-org/gitlab@15-10-stable-eev15.10.0-rc42
Diffstat (limited to 'spec/frontend')
1634 files changed, 18131 insertions, 11415 deletions
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 45639f4c948..200f539fb3e 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -12,6 +12,7 @@ settings: jest: jestConfigFile: 'jest.config.js' rules: + '@gitlab/vtu-no-explicit-wrapper-destroy': error jest/expect-expect: - off - assertFunctionNames: diff --git a/spec/frontend/__helpers__/create_mock_source_editor_extension.js b/spec/frontend/__helpers__/create_mock_source_editor_extension.js new file mode 100644 index 00000000000..fa529604d6f --- /dev/null +++ b/spec/frontend/__helpers__/create_mock_source_editor_extension.js @@ -0,0 +1,12 @@ +export const createMockSourceEditorExtension = (ActualExtension) => { + const { extensionName } = ActualExtension; + const providedKeys = Object.keys(new ActualExtension().provides()); + + const mockedMethods = Object.fromEntries(providedKeys.map((key) => [key, jest.fn()])); + const MockExtension = function MockExtension() {}; + MockExtension.extensionName = extensionName; + MockExtension.mockedMethods = mockedMethods; + MockExtension.prototype.provides = jest.fn().mockReturnValue(mockedMethods); + + return MockExtension; +}; diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js index d5044be88d7..7e8dd463d28 100644 --- a/spec/frontend/__helpers__/experimentation_helper.js +++ b/spec/frontend/__helpers__/experimentation_helper.js @@ -2,16 +2,9 @@ import { merge } from 'lodash'; // This helper is for specs that use `gitlab/experimentation` module export function withGonExperiment(experimentKey, value = true) { - let origGon; - beforeEach(() => { - origGon = window.gon; window.gon = merge({}, window.gon || {}, { experiments: { [experimentKey]: value } }); }); - - afterEach(() => { - window.gon = origGon; - }); } // The following helper is for specs that use `gitlab-experiment` utilities, diff --git a/spec/frontend/__helpers__/gon_helper.js b/spec/frontend/__helpers__/gon_helper.js new file mode 100644 index 00000000000..51d5ece5fc1 --- /dev/null +++ b/spec/frontend/__helpers__/gon_helper.js @@ -0,0 +1,5 @@ +export const createGon = (IS_EE) => { + return { + ee: IS_EE, + }; +}; diff --git a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js index 54d397d0997..8b6cdedfd9f 100644 --- a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js +++ b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js @@ -12,10 +12,6 @@ describe('keepAlive', () => { wrapper = mount(keepAlive(component)); }); - afterEach(() => { - wrapper.destroy(); - }); - it('converts a component to a keep-alive component', async () => { const { element } = wrapper.findComponent(component); diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js index 2fe9fe89a90..0217835b2a3 100644 --- a/spec/frontend/__helpers__/shared_test_setup.js +++ b/spec/frontend/__helpers__/shared_test_setup.js @@ -1,10 +1,12 @@ /* Common setup for both unit and integration test environments */ +import { ReadableStream, WritableStream } from 'node:stream/web'; import * as jqueryMatchers from 'custom-jquery-matchers'; import Vue from 'vue'; import { enableAutoDestroy } from '@vue/test-utils'; import 'jquery'; import Translate from '~/vue_shared/translate'; import setWindowLocation from './set_window_location_helper'; +import { createGon } from './gon_helper'; import { setGlobalDateToFakeDate } from './fake_date'; import { TEST_HOST } from './test_constants'; import * as customMatchers from './matchers'; @@ -13,6 +15,9 @@ import './dom_shims'; import './jquery'; import '~/commons/bootstrap'; +global.ReadableStream = ReadableStream; +global.WritableStream = WritableStream; + enableAutoDestroy(afterEach); // This module has some fairly decent visual test coverage in it's own repository. @@ -67,8 +72,13 @@ beforeEach(() => { // eslint-disable-next-line jest/no-standalone-expect expect.hasAssertions(); - // Reset the mocked window.location. This ensures tests don't interfere with - // each other, and removes the need to tidy up if it was changed for a given - // test. + // Reset globals: This ensures tests don't interfere with + // each other, and removes the need to tidy up if it was + // changed for a given test. + + // Reset the mocked window.location setWindowLocation(TEST_HOST); + + // Reset window.gon object + window.gon = createGon(window.IS_EE); }); diff --git a/spec/frontend/__helpers__/vue_mock_directive.js b/spec/frontend/__helpers__/vue_mock_directive.js index e952f258c4d..e7a2aa7f10d 100644 --- a/spec/frontend/__helpers__/vue_mock_directive.js +++ b/spec/frontend/__helpers__/vue_mock_directive.js @@ -2,7 +2,7 @@ export const getKey = (name) => `$_gl_jest_${name}`; export const getBinding = (el, name) => el[getKey(name)]; -const writeBindingToElement = (el, { name, value, arg, modifiers }) => { +const writeBindingToElement = (el, name, { value, arg, modifiers }) => { el[getKey(name)] = { value, arg, @@ -10,16 +10,24 @@ const writeBindingToElement = (el, { name, value, arg, modifiers }) => { }; }; -export const createMockDirective = () => ({ - bind(el, binding) { - writeBindingToElement(el, binding); - }, +export const createMockDirective = (name) => { + if (!name) { + throw new Error( + 'Vue 3 no longer passes the name of the directive to its hooks, an explicit name is required', + ); + } - update(el, binding) { - writeBindingToElement(el, binding); - }, + return { + bind(el, binding) { + writeBindingToElement(el, name, binding); + }, - unbind(el, { name }) { - delete el[getKey(name)]; - }, -}); + update(el, binding) { + writeBindingToElement(el, name, binding); + }, + + unbind(el) { + delete el[getKey(name)]; + }, + }; +}; diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js index bdd5a0a9034..94164814879 100644 --- a/spec/frontend/__helpers__/vuex_action_helper.js +++ b/spec/frontend/__helpers__/vuex_action_helper.js @@ -78,6 +78,8 @@ export default ( } actions.push(dispatchedAction); + + return Promise.resolve(); }; const validateResults = () => { diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js index 4bd21ff150a..64081ca11a3 100644 --- a/spec/frontend/__helpers__/vuex_action_helper_spec.js +++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js @@ -83,6 +83,20 @@ describe.each([testActionFn, testActionFnWithOptionsArg])( }); }); + describe('given an async action (chaining off a dispatch)', () => { + it('mocks dispatch accurately', () => { + const asyncAction = ({ commit, dispatch }) => { + return dispatch('ACTION').then(() => { + commit('MUTATION'); + }); + }; + + assertion = { actions: [{ type: 'ACTION' }], mutations: [{ type: 'MUTATION' }] }; + + return testAction(asyncAction, null, {}, assertion.mutations, assertion.actions); + }); + }); + describe('given an async action (returning a promise)', () => { const data = { FOO: 'BAR' }; diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js index 4d893bcd0bd..c51f37db384 100644 --- a/spec/frontend/__mocks__/@gitlab/ui.js +++ b/spec/frontend/__mocks__/@gitlab/ui.js @@ -13,13 +13,18 @@ export * from '@gitlab/ui'; * are imported internally in `@gitlab/ui`. */ -jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({ +/* eslint-disable global-require */ + +jest.mock('@gitlab/ui/src/directives/tooltip.js', () => ({ GlTooltipDirective: { bind() {}, }, })); +jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => + require('@gitlab/ui/src/directives/tooltip'), +); -jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({ +jest.mock('@gitlab/ui/src/components/base/tooltip/tooltip.vue', () => ({ props: ['target', 'id', 'triggers', 'placement', 'container', 'boundary', 'disabled', 'show'], render(h) { return h( @@ -33,7 +38,11 @@ jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({ }, })); -jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({ +jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => + require('@gitlab/ui/src/components/base/tooltip/tooltip.vue'), +); + +jest.mock('@gitlab/ui/src/components/base/popover/popover.vue', () => ({ props: { cssClasses: { type: Array, @@ -65,3 +74,6 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({ ); }, })); +jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => + require('@gitlab/ui/src/components/base/popover/popover.vue'), +); diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js index d4fe2ce5406..15f806fc31a 100644 --- a/spec/frontend/__mocks__/lodash/debounce.js +++ b/spec/frontend/__mocks__/lodash/debounce.js @@ -9,9 +9,22 @@ // Further reference: https://github.com/facebook/jest/issues/3465 export default (fn) => { - const debouncedFn = jest.fn().mockImplementation(fn); - debouncedFn.cancel = jest.fn(); - debouncedFn.flush = jest.fn().mockImplementation(() => { + let id; + const debouncedFn = jest.fn(function run(...args) { + // this is calculated in runtime so beforeAll hook works in tests + const timeout = global.JEST_DEBOUNCE_THROTTLE_TIMEOUT; + if (timeout) { + id = setTimeout(() => { + fn.apply(this, args); + }, timeout); + } else { + fn.apply(this, args); + } + }); + debouncedFn.cancel = jest.fn(() => { + clearTimeout(id); + }); + debouncedFn.flush = jest.fn(() => { const errorMessage = "The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'."; diff --git a/spec/frontend/__mocks__/lodash/throttle.js b/spec/frontend/__mocks__/lodash/throttle.js index e8a82654c78..b1014662918 100644 --- a/spec/frontend/__mocks__/lodash/throttle.js +++ b/spec/frontend/__mocks__/lodash/throttle.js @@ -1,4 +1,4 @@ // Similar to `lodash/debounce`, `lodash/throttle` also causes flaky specs. // See `./debounce.js` for more details. -export default (fn) => fn; +export { default } from './debounce'; diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js index ec20088c443..5de5f495f01 100644 --- a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js +++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js @@ -33,10 +33,6 @@ describe('AbuseCategorySelector', () => { createComponent({ showDrawer: true }); }); - afterEach(() => { - wrapper.destroy(); - }); - const findDrawer = () => wrapper.findComponent(GlDrawer); const findTitle = () => wrapper.findByTestId('category-drawer-title'); 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 491d2a0e323..6605faadc17 100644 --- a/spec/frontend/access_tokens/components/expires_at_field_spec.js +++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js @@ -25,10 +25,6 @@ describe('~/access_tokens/components/expires_at_field', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should render datepicker with input info', () => { createComponent(); diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js index e4313bdfa26..fb92cc34ce9 100644 --- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js +++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js @@ -4,12 +4,12 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue'; import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from '~/access_tokens/components/constants'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_INFO } from '~/alert'; import { __, sprintf } from '~/locale'; import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('~/access_tokens/components/new_access_token_app', () => { let wrapper; @@ -52,7 +52,6 @@ describe('~/access_tokens/components/new_access_token_app', () => { afterEach(() => { resetHTMLFixture(); - wrapper.destroy(); createAlert.mockClear(); }); diff --git a/spec/frontend/access_tokens/components/token_spec.js b/spec/frontend/access_tokens/components/token_spec.js index 1af21aaa8cd..f62f7d72e3b 100644 --- a/spec/frontend/access_tokens/components/token_spec.js +++ b/spec/frontend/access_tokens/components/token_spec.js @@ -23,10 +23,6 @@ describe('Token', () => { wrapper = mountExtended(Token, { propsData: defaultPropsData, slots: defaultSlots }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders title slot', () => { createComponent(); diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js index d7acfbb47eb..6e7dee6a2cc 100644 --- a/spec/frontend/access_tokens/components/tokens_app_spec.js +++ b/spec/frontend/access_tokens/components/tokens_app_spec.js @@ -54,10 +54,6 @@ describe('TokensApp', () => { expect(container.props()).toMatchObject(expectedProps); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders all enabled tokens', () => { createComponent(); 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 1d57473943b..5e96da9af7e 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 @@ -55,10 +55,6 @@ describe('AddContextCommitsModal', () => { wrapper = createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders modal with 2 tabs', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js index f679576182f..975f115c4bb 100644 --- a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js +++ b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js @@ -26,10 +26,6 @@ describe('ReviewTabContainer', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('shows loading icon when commits are being loaded', () => { createWrapper({ isLoading: true }); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js index 27c8d760a96..3863eee3795 100644 --- a/spec/frontend/add_context_commits_modal/store/actions_spec.js +++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js @@ -31,10 +31,10 @@ describe('AddContextCommitsModalStoreActions', () => { short_id: 'abcdef', committed_date: '2020-06-12', }; - gon.api_version = 'v4'; let mock; beforeEach(() => { + gon.api_version = 'v4'; mock = new MockAdapter(axios); }); diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js new file mode 100644 index 00000000000..d32fa25d238 --- /dev/null +++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js @@ -0,0 +1,43 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { mockAbuseReports } from '../mock_data'; + +describe('AbuseReportRow', () => { + let wrapper; + const mockAbuseReport = mockAbuseReports[0]; + + const findListItem = () => wrapper.findComponent(ListItem); + const findTitle = () => wrapper.findByTestId('title'); + const findUpdatedAt = () => wrapper.findByTestId('updated-at'); + + const createComponent = () => { + wrapper = shallowMountExtended(AbuseReportRow, { + propsData: { + report: mockAbuseReport, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('renders a ListItem', () => { + expect(findListItem().exists()).toBe(true); + }); + + it('displays correctly formatted title', () => { + const { reporter, reportedUser, category } = mockAbuseReport; + expect(findTitle().text()).toMatchInterpolatedText( + `${reportedUser.name} reported for ${category} by ${reporter.name}`, + ); + }); + + it('displays correctly formatted updated at', () => { + expect(findUpdatedAt().text()).toMatchInterpolatedText( + `Updated ${getTimeago().format(mockAbuseReport.updatedAt)}`, + ); + }); +}); diff --git a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js new file mode 100644 index 00000000000..9efab8403a0 --- /dev/null +++ b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js @@ -0,0 +1,214 @@ +import { shallowMount } from '@vue/test-utils'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { redirectTo, updateHistory } from '~/lib/utils/url_utility'; +import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue'; +import { + FILTERED_SEARCH_TOKENS, + FILTERED_SEARCH_TOKEN_USER, + FILTERED_SEARCH_TOKEN_REPORTER, + FILTERED_SEARCH_TOKEN_STATUS, + FILTERED_SEARCH_TOKEN_CATEGORY, + DEFAULT_SORT, + SORT_OPTIONS, +} from '~/admin/abuse_reports/constants'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils'; + +jest.mock('~/lib/utils/url_utility', () => { + const urlUtility = jest.requireActual('~/lib/utils/url_utility'); + + return { + __esModule: true, + ...urlUtility, + redirectTo: jest.fn(), + updateHistory: jest.fn(), + }; +}); + +describe('AbuseReportsFilteredSearchBar', () => { + let wrapper; + + const CATEGORIES = ['spam', 'phishing']; + + const createComponent = () => { + wrapper = shallowMount(AbuseReportsFilteredSearchBar, { + provide: { categories: CATEGORIES }, + }); + }; + + const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar); + + beforeEach(() => { + setWindowLocation('https://localhost'); + }); + + it('passes correct props to `FilteredSearchBar` component', () => { + createComponent(); + + const categoryToken = buildFilteredSearchCategoryToken(CATEGORIES); + + expect(findFilteredSearchBar().props()).toMatchObject({ + namespace: 'abuse_reports', + recentSearchesStorageKey: 'abuse_reports', + searchInputPlaceholder: 'Filter reports', + tokens: [...FILTERED_SEARCH_TOKENS, categoryToken], + initialSortBy: DEFAULT_SORT, + sortOptions: SORT_OPTIONS, + }); + }); + + it('sets status=open query when there is no initial status query', () => { + createComponent(); + + expect(updateHistory).toHaveBeenCalledWith({ + url: 'https://localhost/?status=open', + replace: true, + }); + + expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([ + { + type: FILTERED_SEARCH_TOKEN_STATUS.type, + value: { data: 'open', operator: '=' }, + }, + ]); + }); + + it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => { + setWindowLocation('?status=closed&user=mr_abuser&reporter=ms_nitch'); + + createComponent(); + + expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([ + { + type: FILTERED_SEARCH_TOKEN_USER.type, + value: { data: 'mr_abuser', operator: '=' }, + }, + { + type: FILTERED_SEARCH_TOKEN_REPORTER.type, + value: { data: 'ms_nitch', operator: '=' }, + }, + { + type: FILTERED_SEARCH_TOKEN_STATUS.type, + value: { data: 'closed', operator: '=' }, + }, + ]); + }); + + describe('initial sort', () => { + it.each( + SORT_OPTIONS.flatMap(({ sortDirection: { descending, ascending } }) => [ + descending, + ascending, + ]), + )( + 'parses sort=%s query and passes it to `FilteredSearchBar` component as initialSortBy', + (sortBy) => { + setWindowLocation(`?sort=${sortBy}`); + + createComponent(); + + expect(findFilteredSearchBar().props('initialSortBy')).toEqual(sortBy); + }, + ); + + it(`uses ${DEFAULT_SORT} as initialSortBy when sort query param is invalid`, () => { + setWindowLocation(`?sort=unknown`); + + createComponent(); + + expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT); + }); + }); + + describe('onFilter', () => { + const USER_FILTER_TOKEN = { + type: FILTERED_SEARCH_TOKEN_USER.type, + value: { data: 'mr_abuser', operator: '=' }, + }; + const REPORTER_FILTER_TOKEN = { + type: FILTERED_SEARCH_TOKEN_REPORTER.type, + value: { data: 'ms_nitch', operator: '=' }, + }; + const STATUS_FILTER_TOKEN = { + type: FILTERED_SEARCH_TOKEN_STATUS.type, + value: { data: 'open', operator: '=' }, + }; + const CATEGORY_FILTER_TOKEN = { + type: FILTERED_SEARCH_TOKEN_CATEGORY.type, + value: { data: 'spam', operator: '=' }, + }; + + const createComponentAndFilter = (filterTokens, initialLocation) => { + if (initialLocation) { + setWindowLocation(initialLocation); + } + + createComponent(); + + findFilteredSearchBar().vm.$emit('onFilter', filterTokens); + }; + + it.each([USER_FILTER_TOKEN, REPORTER_FILTER_TOKEN, STATUS_FILTER_TOKEN, CATEGORY_FILTER_TOKEN])( + 'redirects with $type query param', + (filterToken) => { + createComponentAndFilter([filterToken]); + const { type, value } = filterToken; + expect(redirectTo).toHaveBeenCalledWith(`https://localhost/?${type}=${value.data}`); + }, + ); + + it('ignores search query param', () => { + const searchFilterToken = { type: FILTERED_SEARCH_TERM, value: { data: 'ignored' } }; + createComponentAndFilter([USER_FILTER_TOKEN, searchFilterToken]); + expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser'); + }); + + it('redirects without page query param', () => { + createComponentAndFilter([USER_FILTER_TOKEN], '?page=2'); + expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser'); + }); + + it('redirects with existing sort query param', () => { + createComponentAndFilter([USER_FILTER_TOKEN], `?sort=${DEFAULT_SORT}`); + expect(redirectTo).toHaveBeenCalledWith( + `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT}`, + ); + }); + }); + + describe('onSort', () => { + const SORT_VALUE = 'updated_at_asc'; + const EXISTING_QUERY = 'status=closed&user=mr_abuser'; + + const createComponentAndSort = (initialLocation) => { + setWindowLocation(initialLocation); + createComponent(); + findFilteredSearchBar().vm.$emit('onSort', SORT_VALUE); + }; + + it('redirects to URL with existing query params and the sort query param', () => { + createComponentAndSort(`?${EXISTING_QUERY}`); + + expect(redirectTo).toHaveBeenCalledWith( + `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`, + ); + }); + + it('redirects without page query param', () => { + createComponentAndSort(`?${EXISTING_QUERY}&page=2`); + + expect(redirectTo).toHaveBeenCalledWith( + `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`, + ); + }); + + it('redirects with existing sort query param replaced with the new one', () => { + createComponentAndSort(`?${EXISTING_QUERY}&sort=created_at_desc`); + + expect(redirectTo).toHaveBeenCalledWith( + `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`, + ); + }); + }); +}); diff --git a/spec/frontend/admin/abuse_reports/components/app_spec.js b/spec/frontend/admin/abuse_reports/components/app_spec.js new file mode 100644 index 00000000000..41728baaf33 --- /dev/null +++ b/spec/frontend/admin/abuse_reports/components/app_spec.js @@ -0,0 +1,104 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlEmptyState, GlPagination } from '@gitlab/ui'; +import { queryToObject, objectToQuery } from '~/lib/utils/url_utility'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import AbuseReportsApp from '~/admin/abuse_reports/components/app.vue'; +import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue'; +import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue'; +import { mockAbuseReports } from '../mock_data'; + +describe('AbuseReportsApp', () => { + let wrapper; + + const findFilteredSearchBar = () => wrapper.findComponent(AbuseReportsFilteredSearchBar); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findAbuseReportRows = () => wrapper.findAllComponents(AbuseReportRow); + const findPagination = () => wrapper.findComponent(GlPagination); + + const createComponent = (props = {}) => { + wrapper = shallowMount(AbuseReportsApp, { + propsData: { + abuseReports: mockAbuseReports, + pagination: { currentPage: 1, perPage: 20, totalItems: mockAbuseReports.length }, + ...props, + }, + }); + }; + + it('renders AbuseReportsFilteredSearchBar', () => { + createComponent(); + + expect(findFilteredSearchBar().exists()).toBe(true); + }); + + it('renders one AbuseReportRow for each abuse report', () => { + createComponent(); + + expect(findEmptyState().exists()).toBe(false); + expect(findAbuseReportRows().length).toBe(mockAbuseReports.length); + }); + + it('renders empty state when there are no reports', () => { + createComponent({ + abuseReports: [], + pagination: { currentPage: 1, perPage: 20, totalItems: 0 }, + }); + + expect(findEmptyState().exists()).toBe(true); + }); + + describe('pagination', () => { + const pagination = { + currentPage: 1, + perPage: 1, + totalItems: mockAbuseReports.length, + }; + + it('renders GlPagination with the correct props when needed', () => { + createComponent({ pagination }); + + expect(findPagination().exists()).toBe(true); + expect(findPagination().props()).toMatchObject({ + value: pagination.currentPage, + perPage: pagination.perPage, + totalItems: pagination.totalItems, + prevText: 'Prev', + nextText: 'Next', + labelNextPage: 'Go to next page', + labelPrevPage: 'Go to previous page', + align: 'center', + }); + }); + + it('does not render GlPagination when not needed', () => { + createComponent({ pagination: { currentPage: 1, perPage: 2, totalItems: 2 } }); + + expect(findPagination().exists()).toBe(false); + }); + + describe('linkGen prop', () => { + const existingQuery = { + user: 'mr_okay', + status: 'closed', + }; + const expectedGeneratedQuery = { + ...existingQuery, + page: '2', + }; + + beforeEach(() => { + setWindowLocation(`https://localhost?${objectToQuery(existingQuery)}`); + }); + + it('generates the correct page URL', () => { + createComponent({ pagination }); + + const linkGen = findPagination().props('linkGen'); + const generatedUrl = linkGen(expectedGeneratedQuery.page); + const [, generatedQuery] = generatedUrl.split('?'); + + expect(queryToObject(generatedQuery)).toMatchObject(expectedGeneratedQuery); + }); + }); + }); +}); diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js new file mode 100644 index 00000000000..778f055eb82 --- /dev/null +++ b/spec/frontend/admin/abuse_reports/mock_data.js @@ -0,0 +1,14 @@ +export const mockAbuseReports = [ + { + category: 'spam', + updatedAt: '2022-12-07T06:45:39.977Z', + reporter: { name: 'Ms. Admin' }, + reportedUser: { name: 'Mr. Abuser' }, + }, + { + category: 'phishing', + updatedAt: '2022-12-07T06:45:39.977Z', + reporter: { name: 'Ms. Reporter' }, + reportedUser: { name: 'Mr. Phisher' }, + }, +]; diff --git a/spec/frontend/admin/abuse_reports/utils_spec.js b/spec/frontend/admin/abuse_reports/utils_spec.js new file mode 100644 index 00000000000..17f0b9acb26 --- /dev/null +++ b/spec/frontend/admin/abuse_reports/utils_spec.js @@ -0,0 +1,13 @@ +import { FILTERED_SEARCH_TOKEN_CATEGORY } from '~/admin/abuse_reports/constants'; +import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils'; + +describe('buildFilteredSearchCategoryToken', () => { + it('adds correctly formatted options to FILTERED_SEARCH_TOKEN_CATEGORY', () => { + const categories = ['tuxedo', 'tabby']; + + expect(buildFilteredSearchCategoryToken(categories)).toMatchObject({ + ...FILTERED_SEARCH_TOKEN_CATEGORY, + options: categories.map((c) => ({ value: c, title: c })), + }); + }); +}); diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js index c9a899ab78b..06f9ffeffcd 100644 --- a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js +++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js @@ -19,10 +19,6 @@ describe('DevopsScoreCallout', () => { const findBanner = () => wrapper.findComponent(GlBanner); - afterEach(() => { - wrapper.destroy(); - }); - describe('with no cookie set', () => { beforeEach(() => { utils.setCookie = jest.fn(); diff --git a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js index 2db997942a7..969844f981c 100644 --- a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js +++ b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js @@ -29,10 +29,6 @@ describe('Form component', () => { wrapper = mountFn(SettingsForm, { propsData }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Enable inactive project deletion', () => { it('has the checkbox', () => { createComponent(); diff --git a/spec/frontend/admin/application_settings/network_outbound_spec.js b/spec/frontend/admin/application_settings/network_outbound_spec.js new file mode 100644 index 00000000000..2c06a3fd67f --- /dev/null +++ b/spec/frontend/admin/application_settings/network_outbound_spec.js @@ -0,0 +1,70 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; + +import initNetworkOutbound from '~/admin/application_settings/network_outbound'; + +describe('initNetworkOutbound', () => { + const findAllowCheckboxes = () => document.querySelectorAll('.js-allow-local-requests'); + const findDenyCheckbox = () => document.querySelector('.js-deny-all-requests'); + const findWarningBanner = () => document.querySelector('.js-deny-all-requests-warning'); + const clickDenyCheckbox = () => { + findDenyCheckbox().click(); + }; + + const createFixture = (denyAll = false) => { + setHTMLFixture(` + <input class="js-deny-all-requests" type="checkbox" name="application_setting[deny_all_requests_except_allowed]" ${ + denyAll ? 'checked="checked"' : '' + }/> + <div class="js-deny-all-requests-warning ${denyAll ? '' : 'gl-display-none'}"></div> + <input class="js-allow-local-requests" type="checkbox" name="application_setting[allow_local_requests_from_web_hooks_and_services]" /> + <input class="js-allow-local-requests" type="checkbox" name="application_setting[allow_local_requests_from_system_hooks]" /> + `); + }; + + afterEach(() => { + resetHTMLFixture(); + }); + + describe('when the checkbox is not checked', () => { + beforeEach(() => { + createFixture(); + initNetworkOutbound(); + }); + + it('shows banner and disables allow checkboxes on change', () => { + expect(findDenyCheckbox().checked).toBe(false); + expect(findWarningBanner().classList).toContain('gl-display-none'); + + clickDenyCheckbox(); + + expect(findDenyCheckbox().checked).toBe(true); + expect(findWarningBanner().classList).not.toContain('gl-display-none'); + const allowCheckboxes = findAllowCheckboxes(); + allowCheckboxes.forEach((checkbox) => { + expect(checkbox.checked).toBe(false); + expect(checkbox.disabled).toBe(true); + }); + }); + }); + + describe('when the checkbox is checked', () => { + beforeEach(() => { + createFixture(true); + initNetworkOutbound(); + }); + + it('hides banner and enables allow checkboxes on change', () => { + expect(findDenyCheckbox().checked).toBe(true); + expect(findWarningBanner().classList).not.toContain('gl-display-none'); + + clickDenyCheckbox(); + + expect(findDenyCheckbox().checked).toBe(false); + expect(findWarningBanner().classList).toContain('gl-display-none'); + const allowCheckboxes = findAllowCheckboxes(); + allowCheckboxes.forEach((checkbox) => { + expect(checkbox.disabled).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js index 1a400a101b5..315c38a2bbc 100644 --- a/spec/frontend/admin/applications/components/delete_application_spec.js +++ b/spec/frontend/admin/applications/components/delete_application_spec.js @@ -31,7 +31,6 @@ describe('DeleteApplication', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js index 212f4c0842c..d7b319a3d5e 100644 --- a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js +++ b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js @@ -26,10 +26,6 @@ describe('BackgroundMigrationsDatabaseListbox', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); describe('template always', () => { diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js index d69bf4a22bf..50d8eeb563d 100644 --- a/spec/frontend/admin/broadcast_messages/components/base_spec.js +++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js @@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { redirectTo } from '~/lib/utils/url_utility'; @@ -12,7 +12,7 @@ import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vu import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue'; import { generateMockMessages, MOCK_MESSAGES } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); describe('BroadcastMessagesBase', () => { @@ -41,7 +41,6 @@ describe('BroadcastMessagesBase', () => { afterEach(() => { axiosMock.restore(); - wrapper.destroy(); }); it('renders the table and pagination when there are existing messages', () => { diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js index 36c0ac303ba..292575c984b 100644 --- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js +++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import { GlBroadcastMessage, GlForm } from '@gitlab/ui'; import AxiosMockAdapter from 'axios-mock-adapter'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status'; import MessageForm from '~/admin/broadcast_messages/components/message_form.vue'; @@ -15,7 +15,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('MessageForm', () => { let wrapper; diff --git a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js index 349fab03853..432bfefeb18 100644 --- a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js +++ b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js @@ -21,10 +21,6 @@ describe('MessagesTable', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders a table row for each message', () => { createComponent(); diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js index 4d4a2caedde..a05654a1d25 100644 --- a/spec/frontend/admin/deploy_keys/components/table_spec.js +++ b/spec/frontend/admin/deploy_keys/components/table_spec.js @@ -9,10 +9,10 @@ import { stubComponent } from 'helpers/stub_component'; import DeployKeysTable from '~/admin/deploy_keys/components/table.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import Api, { DEFAULT_PER_PAGE } from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; jest.mock('~/api'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); describe('DeployKeysTable', () => { @@ -91,10 +91,6 @@ describe('DeployKeysTable', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders page title', () => { createComponent(); @@ -242,7 +238,7 @@ describe('DeployKeysTable', () => { itRendersTheEmptyState(); - it('displays flash', () => { + it('displays alert', () => { expect(createAlert).toHaveBeenCalledWith({ message: DeployKeysTable.i18n.apiErrorMessage, captureError: true, 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 eecc21e206b..9e55716cc30 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js @@ -28,10 +28,6 @@ describe('Signup Form', () => { const findCheckboxLabel = () => findByTestId('label'); const findHelpText = () => findByTestId('helpText'); - afterEach(() => { - wrapper.destroy(); - }); - describe('Signup Checkbox', () => { beforeEach(() => { mountComponent(); 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 f2a951bcc76..9192fc12401 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js @@ -40,8 +40,6 @@ describe('Signup Form', () => { const findModal = () => wrapper.findComponent(GlModal); afterEach(() => { - wrapper.destroy(); - formSubmitSpy = null; }); diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js index 4c362a31068..60e46cddd7e 100644 --- a/spec/frontend/admin/statistics_panel/components/app_spec.js +++ b/spec/frontend/admin/statistics_panel/components/app_spec.js @@ -30,10 +30,6 @@ describe('Admin statistics app', () => { store = createStore(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findStats = (idx) => wrapper.findAll('.js-stats').at(idx); describe('template', () => { diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js index 97d257c682c..c069203d046 100644 --- a/spec/frontend/admin/topics/components/remove_avatar_spec.js +++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js @@ -20,7 +20,7 @@ describe('RemoveAvatar', () => { name, }, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, stubs: { GlSprintf, @@ -36,10 +36,6 @@ describe('RemoveAvatar', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('the button component', () => { it('displays the remove button', () => { const button = findButton(); diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js index 738cbd88c4c..113a0e3d404 100644 --- a/spec/frontend/admin/topics/components/topic_select_spec.js +++ b/spec/frontend/admin/topics/components/topic_select_spec.js @@ -59,7 +59,6 @@ describe('TopicSelect', () => { } afterEach(() => { - wrapper.destroy(); jest.clearAllMocks(); }); diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js index 8e9652332c1..4aeaa5356b4 100644 --- a/spec/frontend/admin/users/components/actions/actions_spec.js +++ b/spec/frontend/admin/users/components/actions/actions_spec.js @@ -22,11 +22,6 @@ describe('Action components', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('CONFIRMATION_ACTIONS', () => { it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', (action) => { initComponent({ diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js index 913732aae42..d40089edc82 100644 --- a/spec/frontend/admin/users/components/app_spec.js +++ b/spec/frontend/admin/users/components/app_spec.js @@ -17,11 +17,6 @@ describe('AdminUsersApp component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when initialized', () => { beforeEach(() => { initComponent(); 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 2e892e292d7..efb951f4ad2 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 @@ -73,11 +73,6 @@ describe('Delete user modal', () => { formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders modal with form included', () => { createComponent(); expect(findForm().element).toMatchSnapshot(); diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js index 1b080b05c95..1a2cc3e5c34 100644 --- a/spec/frontend/admin/users/components/user_actions_spec.js +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -32,16 +32,11 @@ describe('AdminUserActions component', () => { showButtonLabels, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('edit button', () => { describe('when the user has an edit action attached', () => { beforeEach(() => { diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js index 94fac875fbe..02e648d2b77 100644 --- a/spec/frontend/admin/users/components/user_avatar_spec.js +++ b/spec/frontend/admin/users/components/user_avatar_spec.js @@ -26,7 +26,7 @@ describe('AdminUserAvatar component', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { GlAvatarLabeled, @@ -34,11 +34,6 @@ describe('AdminUserAvatar component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when initialized', () => { beforeEach(() => { initComponent(); diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js index 73be33d5a9d..19c1cd38a50 100644 --- a/spec/frontend/admin/users/components/user_date_spec.js +++ b/spec/frontend/admin/users/components/user_date_spec.js @@ -17,11 +17,6 @@ describe('FormatDate component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` date | dateFormat | output ${mockDate} | ${undefined} | ${'Nov 13, 2020'} diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js index a0aec347b6b..6f658fd2e59 100644 --- a/spec/frontend/admin/users/components/users_table_spec.js +++ b/spec/frontend/admin/users/components/users_table_spec.js @@ -10,12 +10,12 @@ import AdminUserActions from '~/admin/users/components/user_actions.vue'; import AdminUserAvatar from '~/admin/users/components/user_avatar.vue'; import AdminUsersTable from '~/admin/users/components/users_table.vue'; import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import AdminUserDate from '~/vue_shared/components/user_date.vue'; import { users, paths, createGroupCountResponse } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -57,11 +57,6 @@ describe('AdminUsersTable component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when there are users', () => { beforeEach(() => { initComponent(); @@ -134,7 +129,7 @@ describe('AdminUsersTable component', () => { await waitForPromises(); }); - it('creates a flash message and captures the error', () => { + it('creates an alert message and captures the error', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'Could not load user group counts. Please refresh the page to try again.', captureError: true, diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js index b51858d5129..d8a94ee5e1d 100644 --- a/spec/frontend/admin/users/index_spec.js +++ b/spec/frontend/admin/users/index_spec.js @@ -19,8 +19,6 @@ describe('initAdminUsersApp', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; el = null; }); @@ -47,8 +45,6 @@ describe('initAdminUserActions', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; el = null; }); diff --git a/spec/frontend/airflow/dags/components/dags_spec.js b/spec/frontend/airflow/dags/components/dags_spec.js deleted file mode 100644 index f9cf4fc87af..00000000000 --- a/spec/frontend/airflow/dags/components/dags_spec.js +++ /dev/null @@ -1,115 +0,0 @@ -import { GlAlert, GlPagination, GlTableLite } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { TEST_HOST } from 'helpers/test_constants'; -import AirflowDags from '~/airflow/dags/components/dags.vue'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import { mockDags } from './mock_data'; - -describe('AirflowDags', () => { - let wrapper; - - const createWrapper = ( - dags = [], - pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 }, - ) => { - wrapper = mountExtended(AirflowDags, { - propsData: { - dags, - pagination, - }, - }); - }; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findEmptyState = () => wrapper.findByText('There are no DAGs to show'); - const findPagination = () => wrapper.findComponent(GlPagination); - - describe('default (no dags)', () => { - beforeEach(() => { - createWrapper(); - }); - - it('shows incubation warning', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('shows empty state', () => { - expect(findEmptyState().exists()).toBe(true); - }); - - it('does not show pagination', () => { - expect(findPagination().exists()).toBe(false); - }); - }); - - describe('with dags', () => { - const createWrapperWithDags = (pagination = {}) => { - createWrapper(mockDags, { - page: 1, - isLastPage: false, - per_page: 2, - totalItems: 5, - ...pagination, - }); - }; - - const findDagsData = () => { - return wrapper - .findComponent(GlTableLite) - .findAll('tbody tr') - .wrappers.map((tr) => { - return tr.findAll('td').wrappers.map((td) => { - const timeAgo = td.findComponent(TimeAgo); - - if (timeAgo.exists()) { - return { - type: 'time', - value: timeAgo.props('time'), - }; - } - - return { - type: 'text', - value: td.text(), - }; - }); - }); - }; - - it('renders the table of Dags with data', () => { - createWrapperWithDags(); - - expect(findDagsData()).toEqual( - mockDags.map((x) => [ - { type: 'text', value: x.dag_name }, - { type: 'text', value: x.schedule }, - { type: 'time', value: x.next_run }, - { type: 'text', value: String(x.is_active) }, - { type: 'text', value: String(x.is_paused) }, - { type: 'text', value: x.fileloc }, - ]), - ); - }); - - describe('Pagination behaviour', () => { - it.each` - pagination | expected - ${{}} | ${{ value: 1, prevPage: null, nextPage: 2 }} - ${{ page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: 3 }} - ${{ isLastPage: true, page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: null }} - `('with $pagination, sets pagination props', ({ pagination, expected }) => { - createWrapperWithDags({ ...pagination }); - - expect(findPagination().props()).toMatchObject(expected); - }); - - it('generates link for each page', () => { - createWrapperWithDags(); - - const generateLink = findPagination().props('linkGen'); - - expect(generateLink(3)).toBe(`${TEST_HOST}/?page=3`); - }); - }); - }); -}); diff --git a/spec/frontend/airflow/dags/components/mock_data.js b/spec/frontend/airflow/dags/components/mock_data.js deleted file mode 100644 index 9547282517d..00000000000 --- a/spec/frontend/airflow/dags/components/mock_data.js +++ /dev/null @@ -1,67 +0,0 @@ -export const mockDags = [ - { - id: 1, - project_id: 7, - created_at: '2023-01-05T14:07:02.975Z', - updated_at: '2023-01-05T14:07:02.975Z', - has_import_errors: false, - is_active: false, - is_paused: true, - next_run: '2023-01-05T14:07:02.975Z', - dag_name: 'Dag number 1', - schedule: 'Manual', - fileloc: '/opt/dag.py', - }, - { - id: 2, - project_id: 7, - created_at: '2023-01-05T14:07:02.975Z', - updated_at: '2023-01-05T14:07:02.975Z', - has_import_errors: false, - is_active: false, - is_paused: true, - next_run: '2023-01-05T14:07:02.975Z', - dag_name: 'Dag number 2', - schedule: 'Manual', - fileloc: '/opt/dag.py', - }, - { - id: 3, - project_id: 7, - created_at: '2023-01-05T14:07:02.975Z', - updated_at: '2023-01-05T14:07:02.975Z', - has_import_errors: false, - is_active: false, - is_paused: true, - next_run: '2023-01-05T14:07:02.975Z', - dag_name: 'Dag number 3', - schedule: 'Manual', - fileloc: '/opt/dag.py', - }, - { - id: 4, - project_id: 7, - created_at: '2023-01-05T14:07:02.975Z', - updated_at: '2023-01-05T14:07:02.975Z', - has_import_errors: false, - is_active: false, - is_paused: true, - next_run: '2023-01-05T14:07:02.975Z', - dag_name: 'Dag number 4', - schedule: 'Manual', - fileloc: '/opt/dag.py', - }, - { - id: 5, - project_id: 7, - created_at: '2023-01-05T14:07:02.975Z', - updated_at: '2023-01-05T14:07:02.975Z', - has_import_errors: false, - is_active: false, - is_paused: true, - next_run: '2023-01-05T14:07:02.975Z', - dag_name: 'Dag number 5', - schedule: 'Manual', - fileloc: '/opt/dag.py', - }, -]; diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js index 7fb4f2d2463..3f709d8c9f5 100644 --- a/spec/frontend/alert_management/components/alert_management_table_spec.js +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -68,7 +68,7 @@ describe('AlertManagementTable', () => { }, stubs, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }), ); diff --git a/spec/frontend/flash_spec.js b/spec/frontend/alert_spec.js index 17d6cea23df..1ae8373016b 100644 --- a/spec/frontend/flash_spec.js +++ b/spec/frontend/alert_spec.js @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/browser'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { createAlert, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_WARNING } from '~/alert'; jest.mock('@sentry/browser'); diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap index 0e402e61bcc..4a60d605cae 100644 --- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -51,36 +51,23 @@ exports[`Alert integration settings form default state should match the default </gl-link-stub> </label> - <gl-dropdown-stub + <gl-collapsible-listbox-stub block="true" category="primary" - clearalltext="Clear all" - clearalltextclass="gl-px-5" data-qa-selector="incident_templates_dropdown" headertext="" - hideheaderborder="true" - highlighteditemstitle="Selected" - highlighteditemstitleclass="gl-px-5" + icon="" id="alert-integration-settings-issue-template" + items="[object Object]" + noresultstext="No results found" + placement="left" + resetbuttonlabel="" + searchplaceholder="Search" + selected="selecte_tmpl" size="medium" - text="selecte_tmpl" + toggletext="" variant="default" - > - <gl-dropdown-item-stub - avatarurl="" - data-qa-selector="incident_templates_item" - iconcolor="" - iconname="" - iconrightarialabel="" - iconrightname="" - ischeckitem="true" - secondarytext="" - > - - No template selected - - </gl-dropdown-item-stub> - </gl-dropdown-stub> + /> </gl-form-group-stub> <gl-form-group-stub diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js index a15c78cc456..67d8619f157 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js @@ -30,7 +30,7 @@ import { INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR, DELETE_INTEGRATION_ERROR, } from '~/alerts_settings/utils/error_messages'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; import { @@ -48,7 +48,7 @@ import { } from './mocks/apollo_mock'; import mockIntegrations from './mocks/integrations.json'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('AlertsSettingsWrapper', () => { let wrapper; @@ -128,10 +128,6 @@ describe('AlertsSettingsWrapper', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { beforeEach(() => { createComponent({ @@ -478,7 +474,7 @@ describe('AlertsSettingsWrapper', () => { expect(destroyIntegrationHandler).toHaveBeenCalled(); }); - it('displays flash if mutation had a recoverable error', async () => { + it('displays alert if mutation had a recoverable error', async () => { createComponentWithApollo({ destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors), }); @@ -489,7 +485,7 @@ describe('AlertsSettingsWrapper', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'Houston, we have a problem' }); }); - it('displays flash if mutation had a non-recoverable error', async () => { + it('displays alert if mutation had a non-recoverable error', async () => { createComponentWithApollo({ destroyHandler: jest.fn().mockRejectedValue('Error'), }); diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/components/activity_chart_spec.js index c26407f5c1d..4f8126aaacf 100644 --- a/spec/frontend/analytics/components/activity_chart_spec.js +++ b/spec/frontend/analytics/components/activity_chart_spec.js @@ -13,11 +13,6 @@ describe('Activity Chart Bundle', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findChart = () => wrapper.findComponent(GlColumnChart); const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]'); diff --git a/spec/frontend/analytics/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/base_spec.js index 58588ff49ce..033916eabcd 100644 --- a/spec/frontend/analytics/cycle_analytics/base_spec.js +++ b/spec/frontend/analytics/cycle_analytics/base_spec.js @@ -31,13 +31,15 @@ Vue.use(Vuex); let wrapper; -const { id: groupId, path: groupPath } = currentGroup; +const { path } = currentGroup; +const groupPath = `groups/${path}`; const defaultState = { currentGroup, createdBefore, createdAfter, stageCounts, - endpoints: { fullPath, groupId, groupPath }, + groupPath, + namespace: { fullPath }, }; function createStore({ initialState = {}, initialGetters = {} }) { @@ -93,11 +95,6 @@ describe('Value stream analytics component', () => { wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents, pagination } }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders the path navigation component', () => { expect(findPathNavigation().exists()).toBe(true); }); @@ -139,7 +136,6 @@ describe('Value stream analytics component', () => { it('passes the paths to the filter bar', () => { expect(findFilters().props()).toEqual({ - groupId, groupPath, endDate: createdBefore, hasDateRangeFilter: true, @@ -157,6 +153,10 @@ describe('Value stream analytics component', () => { expect(findPagination().exists()).toBe(true); }); + it('does not render a link to the value streams dashboard', () => { + expect(findOverviewMetrics().props('dashboardsPath')).toBeNull(); + }); + describe('with `cycleAnalyticsForGroups=true` license', () => { beforeEach(() => { wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } }); @@ -167,6 +167,23 @@ describe('Value stream analytics component', () => { }); }); + describe('with `groupAnalyticsDashboardsPage=true` and `groupLevelAnalyticsDashboard=true` license', () => { + beforeEach(() => { + wrapper = createComponent({ + initialState: { + features: { groupAnalyticsDashboardsPage: true, groupLevelAnalyticsDashboard: true }, + }, + }); + }); + + it('renders a link to the value streams dashboard', () => { + expect(findOverviewMetrics().props('dashboardsPath')).toBeDefined(); + expect(findOverviewMetrics().props('dashboardsPath')).toBe( + '/groups/foo/-/analytics/dashboards/value_streams_dashboard?query=full/path/to/foo', + ); + }); + }); + describe('isLoading = true', () => { beforeEach(() => { wrapper = createComponent({ diff --git a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js index 2b26b202882..da7824adbf9 100644 --- a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js +++ b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js @@ -98,7 +98,6 @@ describe('Filter bar', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js index 9be92bb92bc..6dd7e2e6223 100644 --- a/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js +++ b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js @@ -16,10 +16,6 @@ describe('Formatted Stage Count', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it.each` stageCount | expectedOutput ${null} | ${'-'} diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js index f820f755400..216e07844b8 100644 --- a/spec/frontend/analytics/cycle_analytics/mock_data.js +++ b/spec/frontend/analytics/cycle_analytics/mock_data.js @@ -219,6 +219,8 @@ export const group = { }; export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true }); +export const groupNamespace = { id: currentGroup.id, fullPath: `groups/${currentGroup.path}` }; +export const projectNamespace = { fullPath: 'some/cool/path' }; export const selectedProjects = [ { diff --git a/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js index 107e62035c3..9a598ee0ad1 100644 --- a/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js +++ b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js @@ -50,8 +50,6 @@ describe('Project PathNavigation', () => { afterEach(() => { unmockTracking(); - wrapper.destroy(); - wrapper = null; }); describe('displays correctly', () => { diff --git a/spec/frontend/analytics/cycle_analytics/stage_table_spec.js b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js index cfccce7eae9..fbc63a80de8 100644 --- a/spec/frontend/analytics/cycle_analytics/stage_table_spec.js +++ b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js @@ -51,10 +51,6 @@ function createComponent(props = {}, shallow = false) { } describe('StageTable', () => { - afterEach(() => { - wrapper.destroy(); - }); - describe('is loaded with data', () => { beforeEach(() => { wrapper = createComponent(); @@ -258,7 +254,6 @@ describe('StageTable', () => { afterEach(() => { unmockTracking(); - wrapper.destroy(); }); it('will display the pagination component', () => { @@ -305,7 +300,6 @@ describe('StageTable', () => { afterEach(() => { unmockTracking(); - wrapper.destroy(); }); it('can sort the end event or duration', () => { diff --git a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js index 3030fca126b..b2ce8596c22 100644 --- a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js +++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js @@ -13,21 +13,13 @@ import { createdBefore, initialPaginationState, reviewEvents, + projectNamespace as namespace, } from '../mock_data'; -const { id: groupId, path: groupPath } = currentGroup; -const mockMilestonesPath = 'mock-milestones.json'; -const mockLabelsPath = 'mock-labels.json'; -const mockRequestPath = 'some/cool/path'; +const { path: groupPath } = currentGroup; +const mockMilestonesPath = `/${namespace.fullPath}/-/milestones.json`; +const mockLabelsPath = `/${namespace.fullPath}/-/labels.json`; const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams'; -const mockEndpoints = { - fullPath: mockFullPath, - requestPath: mockRequestPath, - labelsPath: mockLabelsPath, - milestonesPath: mockMilestonesPath, - groupId, - groupPath, -}; const mockSetDateActionCommit = { payload: { createdAfter, createdBefore }, type: 'SET_DATE_RANGE', @@ -35,6 +27,7 @@ const mockSetDateActionCommit = { const defaultState = { ...getters, + namespace, selectedValueStream, createdAfter, createdBefore, @@ -81,7 +74,8 @@ describe('Project Value Stream Analytics actions', () => { const selectedAssigneeList = ['Assignee 1', 'Assignee 2']; const selectedLabelList = ['Label 1', 'Label 2']; const payload = { - endpoints: mockEndpoints, + namespace, + groupPath, selectedAuthor, selectedMilestone, selectedAssigneeList, @@ -92,7 +86,7 @@ describe('Project Value Stream Analytics actions', () => { groupEndpoint: 'foo', labelsEndpoint: mockLabelsPath, milestonesEndpoint: mockMilestonesPath, - projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams', + projectEndpoint: namespace.fullPath, }; it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => { @@ -193,7 +187,6 @@ describe('Project Value Stream Analytics actions', () => { beforeEach(() => { state = { ...defaultState, - endpoints: mockEndpoints, selectedStage, }; mock = new MockAdapter(axios); @@ -219,7 +212,6 @@ describe('Project Value Stream Analytics actions', () => { beforeEach(() => { state = { ...defaultState, - endpoints: mockEndpoints, selectedStage, }; mock = new MockAdapter(axios); @@ -243,7 +235,6 @@ describe('Project Value Stream Analytics actions', () => { beforeEach(() => { state = { ...defaultState, - endpoints: mockEndpoints, selectedStage, }; mock = new MockAdapter(axios); @@ -265,9 +256,7 @@ describe('Project Value Stream Analytics actions', () => { const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/; beforeEach(() => { - state = { - endpoints: mockEndpoints, - }; + state = { namespace }; mock = new MockAdapter(axios); mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_OK); }); @@ -333,7 +322,7 @@ describe('Project Value Stream Analytics actions', () => { beforeEach(() => { state = { - endpoints: mockEndpoints, + namespace, selectedValueStream, }; mock = new MockAdapter(axios); diff --git a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js index 567fac81e1f..70b7454f4a0 100644 --- a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js +++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js @@ -17,12 +17,14 @@ import { rawStageCounts, stageCounts, initialPaginationState as pagination, + projectNamespace as mockNamespace, } from '../mock_data'; let state; const rawEvents = rawIssueEvents.events; const convertedEvents = issueEvents.events; -const mockRequestPath = 'fake/request/path'; +const mockGroupPath = 'groups/path'; +const mockFeatures = { some: 'feature' }; const mockCreatedAfter = '2020-06-18'; const mockCreatedBefore = '2020-07-18'; @@ -64,19 +66,22 @@ describe('Project Value Stream Analytics mutations', () => { const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore }; const mockInitialPayload = { - endpoints: { requestPath: mockRequestPath }, currentGroup: { title: 'cool-group' }, id: 1337, + groupPath: mockGroupPath, + namespace: mockNamespace, + features: mockFeatures, ...mockSetDatePayload, }; const mockInitializedObj = { - endpoints: { requestPath: mockRequestPath }, ...mockSetDatePayload, }; it.each` mutation | stateKey | value - ${types.INITIALIZE_VSA} | ${'endpoints'} | ${{ requestPath: mockRequestPath }} + ${types.INITIALIZE_VSA} | ${'features'} | ${mockFeatures} + ${types.INITIALIZE_VSA} | ${'namespace'} | ${mockNamespace} + ${types.INITIALIZE_VSA} | ${'groupPath'} | ${mockGroupPath} ${types.INITIALIZE_VSA} | ${'createdAfter'} | ${mockCreatedAfter} ${types.INITIALIZE_VSA} | ${'createdBefore'} | ${mockCreatedBefore} `('$mutation will set $stateKey', ({ mutation, stateKey, value }) => { diff --git a/spec/frontend/analytics/cycle_analytics/total_time_spec.js b/spec/frontend/analytics/cycle_analytics/total_time_spec.js index 47ee7aad8c4..6597b6fa3d5 100644 --- a/spec/frontend/analytics/cycle_analytics/total_time_spec.js +++ b/spec/frontend/analytics/cycle_analytics/total_time_spec.js @@ -10,10 +10,6 @@ describe('TotalTime', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with a valid time object', () => { it.each` time diff --git a/spec/frontend/analytics/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js index fe412bf7498..e6d17edcadc 100644 --- a/spec/frontend/analytics/cycle_analytics/utils_spec.js +++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js @@ -91,9 +91,9 @@ describe('Value stream analytics utils', () => { const projectId = '5'; const createdAfter = '2021-09-01'; const createdBefore = '2021-11-06'; - const groupId = '146'; const groupPath = 'fake-group'; - const fullPath = 'fake-group/fake-project'; + const namespaceName = 'Fake project'; + const namespaceFullPath = 'fake-group/fake-project'; const labelsPath = '/fake-group/fake-project/-/labels.json'; const milestonesPath = '/fake-group/fake-project/-/milestones.json'; const requestPath = '/fake-group/fake-project/-/value_stream_analytics'; @@ -102,11 +102,11 @@ describe('Value stream analytics utils', () => { projectId, createdBefore, createdAfter, - fullPath, + namespaceName, + namespaceFullPath, requestPath, labelsPath, milestonesPath, - groupId, groupPath, }; @@ -124,14 +124,13 @@ describe('Value stream analytics utils', () => { expect(res.createdAfter).toEqual(new Date(createdAfter)); }); + it('sets the namespace', () => { + expect(res.namespace.name).toBe(namespaceName); + expect(res.namespace.fullPath).toBe(namespaceFullPath); + }); + it('sets the endpoints', () => { - const { endpoints } = res; - expect(endpoints.fullPath).toBe(fullPath); - expect(endpoints.requestPath).toBe(requestPath); - expect(endpoints.labelsPath).toBe(labelsPath); - expect(endpoints.milestonesPath).toBe(milestonesPath); - expect(endpoints.groupId).toBe(parseInt(groupId, 10)); - expect(endpoints.groupPath).toBe(groupPath); + expect(res.groupPath).toBe(`groups/${groupPath}`); }); it('returns null when there is no stage', () => { @@ -164,7 +163,7 @@ describe('Value stream analytics utils', () => { ...rawData, gon: { licensed_features: fakeFeatures }, }); - expect(res.features).toEqual(fakeFeatures); + expect(res.features).toMatchObject(fakeFeatures); }); }); }); diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js index 4f333e95d89..160f6ce0563 100644 --- a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js +++ b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js @@ -34,11 +34,6 @@ describe('ValueStreamFilters', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('will render the filter bar', () => { expect(findFilterBar().exists()).toBe(true); }); diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js index 948dc5c9be2..6a64737bc80 100644 --- a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js +++ b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js @@ -8,10 +8,11 @@ import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api'; import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants'; import { prepareTimeMetricsData } from '~/analytics/shared/utils'; import MetricTile from '~/analytics/shared/components/metric_tile.vue'; -import { createAlert } from '~/flash'; +import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue'; +import { createAlert } from '~/alert'; import { group } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('ValueStreamMetrics', () => { let wrapper; @@ -37,6 +38,7 @@ describe('ValueStreamMetrics', () => { }); }; + const findVSDLink = () => wrapper.findComponent(ValueStreamsDashboardLink); const findMetrics = () => wrapper.findAllComponents(MetricTile); const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group'); @@ -48,10 +50,6 @@ describe('ValueStreamMetrics', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with successful requests', () => { beforeEach(() => { mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData }); @@ -168,6 +166,25 @@ describe('ValueStreamMetrics', () => { }); }); + describe('Value Streams Dashboard Link', () => { + it('will render when a dashboardsPath is set', async () => { + wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS, dashboardsPath: 'fake-group-path' }); + await waitForPromises(); + + const vsdLink = findVSDLink(); + + expect(vsdLink.exists()).toBe(true); + expect(vsdLink.props()).toEqual({ requestPath: 'fake-group-path' }); + }); + + it('does not render without a dashboardsPath', async () => { + wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS }); + await waitForPromises(); + + expect(findVSDLink().exists()).toBe(false); + }); + }); + describe('with a request failing', () => { beforeEach(async () => { mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue(); diff --git a/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js index c62bfb11f7b..70bfce41c82 100644 --- a/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js +++ b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js @@ -6,10 +6,6 @@ import ServicePingDisabled from '~/analytics/devops_reports/components/service_p describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - const createWrapper = ({ isAdmin = false } = {}) => { wrapper = mountExtended(ServicePingDisabled, { provide: { diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js index 562e86529ee..5f0847e0db6 100644 --- a/spec/frontend/analytics/shared/components/daterange_spec.js +++ b/spec/frontend/analytics/shared/components/daterange_spec.js @@ -22,10 +22,6 @@ describe('Daterange component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker); const findDateRangeIndicator = () => wrapper.findByTestId('daterange-picker-indicator'); @@ -90,18 +86,19 @@ describe('Daterange component', () => { }); describe('set', () => { - it('emits the change event with an object containing startDate and endDate', () => { + it('emits the change event with an object containing startDate and endDate', async () => { const startDate = new Date('2019-10-01'); const endDate = new Date('2019-10-05'); - wrapper.vm.dateRange = { startDate, endDate }; - expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]); + await findDaterangePicker().vm.$emit('input', { startDate, endDate }); + + expect(wrapper.emitted('change')).toEqual([[{ startDate, endDate }]]); }); }); describe('get', () => { - it("returns value of dateRange from state's startDate and endDate", () => { - expect(wrapper.vm.dateRange).toEqual({ + it("datepicker to have default of dateRange from state's startDate and endDate", () => { + expect(findDaterangePicker().props('value')).toEqual({ startDate: defaultProps.startDate, endDate: defaultProps.endDate, }); diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js index e0bfff3e664..d7e6606cdc6 100644 --- a/spec/frontend/analytics/shared/components/metric_popover_spec.js +++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js @@ -34,10 +34,6 @@ describe('MetricPopover', () => { const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon); const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the metric label', () => { wrapper = createComponent({ metric: MOCK_METRIC }); expect(findMetricLabel().text()).toBe(MOCK_METRIC.label); diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js index 980dfad9eb0..00e82cff0f0 100644 --- a/spec/frontend/analytics/shared/components/metric_tile_spec.js +++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js @@ -21,10 +21,6 @@ describe('MetricTile', () => { const findSingleStat = () => wrapper.findComponent(GlSingleStat); const findPopover = () => wrapper.findComponent(MetricPopover); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { describe('links', () => { it('when the metric has links, it redirects the user on click', () => { diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js index 3871fd530d8..d2cbe0d39e4 100644 --- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -70,10 +70,6 @@ describe('ProjectsDropdownFilter component', () => { return waitForPromises(); }; - afterEach(() => { - wrapper.destroy(); - }); - const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items'); const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items'); const findClearAllButton = () => wrapper.findByText('Clear all'); diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js index b48e2d971b5..24af7b836d5 100644 --- a/spec/frontend/analytics/shared/utils_spec.js +++ b/spec/frontend/analytics/shared/utils_spec.js @@ -5,6 +5,7 @@ import { extractPaginationQueryParameters, getDataZoomOption, prepareTimeMetricsData, + generateValueStreamsDashboardLink, } from '~/analytics/shared/utils'; import { slugify } from '~/lib/utils/text_utility'; import { objectToQuery } from '~/lib/utils/url_utility'; @@ -212,3 +213,30 @@ describe('prepareTimeMetricsData', () => { ]); }); }); + +describe('generateValueStreamsDashboardLink', () => { + it.each` + groupPath | projectPaths | result + ${''} | ${[]} | ${''} + ${'groups/fake-group'} | ${[]} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard'} + ${'groups/fake-group'} | ${['fake-path/project_1']} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard?query=fake-path/project_1'} + ${'groups/fake-group'} | ${['fake-path/project_1', 'fake-path/project_2']} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard?query=fake-path/project_1,fake-path/project_2'} + `( + 'generates the dashboard link when groupPath=$groupPath and projectPaths=$projectPaths', + ({ groupPath, projectPaths, result }) => { + expect(generateValueStreamsDashboardLink(groupPath, projectPaths)).toBe(result); + }, + ); + + describe('with a relative url rool set', () => { + beforeEach(() => { + gon.relative_url_root = '/foobar'; + }); + + it('with includes a relative path if one is set', () => { + expect(generateValueStreamsDashboardLink('groups/fake-path', ['project_1'])).toBe( + '/foobar/groups/fake-path/-/analytics/dashboards/value_streams_dashboard?query=project_1', + ); + }); + }); +}); diff --git a/spec/frontend/analytics/usage_trends/components/app_spec.js b/spec/frontend/analytics/usage_trends/components/app_spec.js index c732dc22322..f9338661ebf 100644 --- a/spec/frontend/analytics/usage_trends/components/app_spec.js +++ b/spec/frontend/analytics/usage_trends/components/app_spec.js @@ -15,11 +15,6 @@ describe('UsageTrendsApp', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('displays the usage counts component', () => { expect(wrapper.findComponent(UsageCounts).exists()).toBe(true); }); diff --git a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js index f4cbc56be5c..a71ce090955 100644 --- a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js +++ b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js @@ -26,10 +26,6 @@ describe('UsageCounts', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat); diff --git a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js index ad6089f74b5..322d05e663a 100644 --- a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js @@ -45,11 +45,6 @@ describe('UsageTrendsCountChart', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findLoader = () => wrapper.findComponent(ChartSkeletonLoader); const findChart = () => wrapper.findComponent(GlLineChart); const findAlert = () => wrapper.findComponent(GlAlert); diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js index e7abd4d4323..20836d7cc70 100644 --- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js @@ -42,11 +42,6 @@ describe('UsersChart', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findLoader = () => wrapper.findComponent(ChartSkeletonLoader); const findAlert = () => wrapper.findComponent(GlAlert); const findChart = () => wrapper.findComponent(GlAreaChart); diff --git a/spec/frontend/api/alert_management_alerts_api_spec.js b/spec/frontend/api/alert_management_alerts_api_spec.js index 507f659a170..86052a05b76 100644 --- a/spec/frontend/api/alert_management_alerts_api_spec.js +++ b/spec/frontend/api/alert_management_alerts_api_spec.js @@ -9,7 +9,6 @@ import { describe('~/api/alert_management_alerts_api.js', () => { let mock; - let originalGon; const projectId = 1; const alertIid = 2; @@ -19,13 +18,11 @@ describe('~/api/alert_management_alerts_api.js', () => { beforeEach(() => { mock = new MockAdapter(axios); - originalGon = window.gon; window.gon = { api_version: 'v4' }; }); afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('fetchAlertMetricImages', () => { diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js index 0315db02cf2..642edb33624 100644 --- a/spec/frontend/api/groups_api_spec.js +++ b/spec/frontend/api/groups_api_spec.js @@ -10,23 +10,18 @@ const mockUrlRoot = '/gitlab'; const mockGroupId = '99'; 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 }; + window.gon = { + api_version: mockApiVersion, + relative_url_root: mockUrlRoot, + }; }); afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('updateGroup', () => { diff --git a/spec/frontend/api/packages_api_spec.js b/spec/frontend/api/packages_api_spec.js index 5f517bcf358..37c4b926ec2 100644 --- a/spec/frontend/api/packages_api_spec.js +++ b/spec/frontend/api/packages_api_spec.js @@ -6,22 +6,18 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; describe('Api', () => { const dummyApiVersion = 'v3000'; const dummyUrlRoot = '/gitlab'; - const dummyGon = { - api_version: dummyApiVersion, - relative_url_root: dummyUrlRoot, - }; - let originalGon; let mock; beforeEach(() => { mock = new MockAdapter(axios); - originalGon = window.gon; - window.gon = { ...dummyGon }; + window.gon = { + api_version: dummyApiVersion, + relative_url_root: dummyUrlRoot, + }; }); afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('packages', () => { diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js index 2d4ed39dad0..2de56fae0c2 100644 --- a/spec/frontend/api/projects_api_spec.js +++ b/spec/frontend/api/projects_api_spec.js @@ -7,7 +7,6 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; describe('~/api/projects_api.js', () => { let mock; - let originalGon; const projectId = 1; const setfullPathProjectSearch = (value) => { @@ -17,13 +16,11 @@ describe('~/api/projects_api.js', () => { beforeEach(() => { mock = new MockAdapter(axios); - originalGon = window.gon; window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } }; }); afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('getProjects', () => { diff --git a/spec/frontend/api/tags_api_spec.js b/spec/frontend/api/tags_api_spec.js index af3533f52b7..0a1177d4f60 100644 --- a/spec/frontend/api/tags_api_spec.js +++ b/spec/frontend/api/tags_api_spec.js @@ -5,20 +5,17 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; describe('~/api/tags_api.js', () => { let mock; - let originalGon; const projectId = 1; beforeEach(() => { mock = new MockAdapter(axios); - originalGon = window.gon; window.gon = { api_version: 'v7' }; }); afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('getTag', () => { diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js index 4d0252aad23..6636d77a09b 100644 --- a/spec/frontend/api/user_api_spec.js +++ b/spec/frontend/api/user_api_spec.js @@ -12,19 +12,16 @@ import { timeRanges } from '~/vue_shared/constants'; describe('~/api/user_api', () => { let axiosMock; - let originalGon; beforeEach(() => { axiosMock = new MockAdapter(axios); - originalGon = window.gon; window.gon = { api_version: 'v4' }; }); afterEach(() => { axiosMock.restore(); axiosMock.resetHistory(); - window.gon = originalGon; }); describe('followUser', () => { diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 6fd106502c4..4ef37311e51 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -10,27 +10,22 @@ import { HTTP_STATUS_OK, } from '~/lib/utils/http_status'; -jest.mock('~/flash'); - describe('Api', () => { const dummyApiVersion = 'v3000'; const dummyUrlRoot = '/gitlab'; - const dummyGon = { - api_version: dummyApiVersion, - relative_url_root: dummyUrlRoot, - }; - let originalGon; + let mock; beforeEach(() => { mock = new MockAdapter(axios); - originalGon = window.gon; - window.gon = { ...dummyGon }; + window.gon = { + api_version: dummyApiVersion, + relative_url_root: dummyUrlRoot, + }; }); afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('buildUrl', () => { @@ -1423,7 +1418,7 @@ describe('Api', () => { describe('when service data increment counter is called with feature flag disabled', () => { beforeEach(() => { - gon.features = { ...gon.features, usageDataApi: false }; + gon.features = { usageDataApi: false }; }); it('returns null', () => { @@ -1437,7 +1432,7 @@ describe('Api', () => { describe('when service data increment counter is called', () => { beforeEach(() => { - gon.features = { ...gon.features, usageDataApi: true }; + gon.features = { usageDataApi: true }; }); it('resolves the Promise', () => { @@ -1468,7 +1463,7 @@ describe('Api', () => { describe('when service data increment unique users is called with feature flag disabled', () => { beforeEach(() => { - gon.features = { ...gon.features, usageDataApi: false }; + gon.features = { usageDataApi: false }; }); it('returns null and does not call the endpoint', () => { @@ -1483,7 +1478,7 @@ describe('Api', () => { describe('when service data increment unique users is called', () => { beforeEach(() => { - gon.features = { ...gon.features, usageDataApi: true }; + gon.features = { usageDataApi: true }; }); it('resolves the Promise', () => { @@ -1500,7 +1495,7 @@ describe('Api', () => { describe('when user is not set and feature flag enabled', () => { beforeEach(() => { - gon.features = { ...gon.features, usageDataApi: true }; + gon.features = { usageDataApi: true }; }); it('returns null and does not call the endpoint', () => { diff --git a/spec/frontend/approvals/mock_data.js b/spec/frontend/approvals/mock_data.js new file mode 100644 index 00000000000..e0e90c09791 --- /dev/null +++ b/spec/frontend/approvals/mock_data.js @@ -0,0 +1,10 @@ +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; + +export const createCanApproveResponse = () => { + const response = JSON.parse(JSON.stringify(approvedByCurrentUser)); + response.data.project.mergeRequest.userPermissions.canApprove = true; + response.data.project.mergeRequest.approved = false; + response.data.project.mergeRequest.approvedBy.nodes = []; + + return response; +}; diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js index 2a7156bf480..268772ed4c0 100644 --- a/spec/frontend/artifacts/components/artifact_row_spec.js +++ b/spec/frontend/artifacts/components/artifact_row_spec.js @@ -1,9 +1,10 @@ -import { GlBadge, GlButton, GlFriendlyWrap } from '@gitlab/ui'; +import { GlBadge, GlButton, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui'; import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import ArtifactRow from '~/artifacts/components/artifact_row.vue'; +import { BULK_DELETE_FEATURE_FLAG } from '~/artifacts/constants'; describe('ArtifactRow component', () => { let wrapper; @@ -15,23 +16,21 @@ describe('ArtifactRow component', () => { const findSize = () => wrapper.findByTestId('job-artifact-row-size'); const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button'); const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button'); + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); - const createComponent = ({ canDestroyArtifacts = true } = {}) => { + const createComponent = ({ canDestroyArtifacts = true, glFeatures = {} } = {}) => { wrapper = shallowMountExtended(ArtifactRow, { propsData: { artifact, + isSelected: false, isLoading: false, isLastRow: false, }, - provide: { canDestroyArtifacts }, + provide: { canDestroyArtifacts, glFeatures }, stubs: { GlBadge, GlButton, GlFriendlyWrap }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('artifact details', () => { beforeEach(async () => { createComponent(); @@ -77,4 +76,30 @@ describe('ArtifactRow component', () => { expect(wrapper.emitted('delete')).toBeDefined(); }); }); + + describe('bulk delete checkbox', () => { + describe('with permission and feature flag enabled', () => { + beforeEach(() => { + createComponent({ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true } }); + }); + + it('emits selectArtifact when toggled', () => { + findCheckbox().vm.$emit('input', true); + + expect(wrapper.emitted('selectArtifact')).toStrictEqual([[artifact, true]]); + }); + }); + + it('is not shown without permission', () => { + createComponent({ canDestroyArtifacts: false }); + + expect(findCheckbox().exists()).toBe(false); + }); + + it('is not shown with feature flag disabled', () => { + createComponent(); + + expect(findCheckbox().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js new file mode 100644 index 00000000000..876906b2c3c --- /dev/null +++ b/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js @@ -0,0 +1,96 @@ +import { GlSprintf, GlModal } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue'; +import bulkDestroyArtifactsMutation from '~/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql'; + +Vue.use(VueApollo); + +describe('ArtifactsBulkDelete component', () => { + let wrapper; + let requestHandlers; + + const projectId = '123'; + const selectedArtifacts = [ + mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0].id, + mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[1].id, + ]; + + const findText = () => wrapper.findComponent(GlSprintf).text(); + const findDeleteButton = () => wrapper.findByTestId('bulk-delete-delete-button'); + const findClearButton = () => wrapper.findByTestId('bulk-delete-clear-button'); + const findModal = () => wrapper.findComponent(GlModal); + + const createComponent = ({ + handlers = { + bulkDestroyArtifactsMutation: jest.fn(), + }, + } = {}) => { + requestHandlers = handlers; + wrapper = mountExtended(ArtifactsBulkDelete, { + apolloProvider: createMockApollo([ + [bulkDestroyArtifactsMutation, requestHandlers.bulkDestroyArtifactsMutation], + ]), + propsData: { + selectedArtifacts, + queryVariables: {}, + isLoading: false, + isLastRow: false, + }, + provide: { projectId }, + }); + }; + + describe('selected artifacts box', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('displays selected artifacts count', () => { + expect(findText()).toContain(String(selectedArtifacts.length)); + }); + + it('opens the confirmation modal when the delete button is clicked', async () => { + expect(findModal().props('visible')).toBe(false); + + findDeleteButton().trigger('click'); + await waitForPromises(); + + expect(findModal().props('visible')).toBe(true); + }); + + it('emits clearSelectedArtifacts event when the clear button is clicked', () => { + findClearButton().trigger('click'); + + expect(wrapper.emitted('clearSelectedArtifacts')).toBeDefined(); + }); + }); + + describe('bulk delete confirmation modal', () => { + beforeEach(async () => { + createComponent(); + findDeleteButton().trigger('click'); + await waitForPromises(); + }); + + it('calls the bulk delete mutation with the selected artifacts on confirm', () => { + findModal().vm.$emit('primary'); + + expect(requestHandlers.bulkDestroyArtifactsMutation).toHaveBeenCalledWith({ + projectId: `gid://gitlab/Project/${projectId}`, + ids: selectedArtifacts, + }); + }); + + it('does not call the bulk delete mutation on cancel', () => { + findModal().vm.$emit('cancel'); + + expect(requestHandlers.bulkDestroyArtifactsMutation).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js index d006e0285d2..6bf3498f9b0 100644 --- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js +++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js @@ -10,9 +10,9 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql'; import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; -jest.mock('~/flash'); +jest.mock('~/alert'); const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0]; const refetchArtifacts = jest.fn(); @@ -25,11 +25,12 @@ describe('ArtifactsTableRowDetails component', () => { const findModal = () => wrapper.findComponent(GlModal); - const createComponent = ( + const createComponent = ({ handlers = { destroyArtifactMutation: jest.fn(), }, - ) => { + selectedArtifacts = [], + } = {}) => { requestHandlers = handlers; wrapper = mountExtended(ArtifactsTableRowDetails, { apolloProvider: createMockApollo([ @@ -37,6 +38,7 @@ describe('ArtifactsTableRowDetails component', () => { ]), propsData: { artifacts, + selectedArtifacts, refetchArtifacts, queryVariables: {}, }, @@ -47,10 +49,6 @@ describe('ArtifactsTableRowDetails component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('passes correct props', () => { beforeEach(() => { createComponent(); @@ -92,7 +90,7 @@ describe('ArtifactsTableRowDetails component', () => { }); }); - it('displays a flash message and refetches artifacts when the mutation fails', async () => { + it('displays an alert message and refetches artifacts when the mutation fails', async () => { createComponent({ destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')), }); @@ -120,4 +118,20 @@ describe('ArtifactsTableRowDetails component', () => { expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled(); }); }); + + describe('bulk delete selection', () => { + it('is not selected for unselected artifact', async () => { + createComponent(); + await waitForPromises(); + + expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(false); + }); + + it('is selected for selected artifacts', async () => { + createComponent({ selectedArtifacts: [artifacts.nodes[0].id] }); + await waitForPromises(); + + expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(true); + }); + }); }); diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/artifacts/components/feedback_banner_spec.js index 3421486020a..af9599daefa 100644 --- a/spec/frontend/artifacts/components/feedback_banner_spec.js +++ b/spec/frontend/artifacts/components/feedback_banner_spec.js @@ -32,10 +32,6 @@ describe('Artifacts management feedback banner', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('is displayed with the correct props', () => { createComponent(); diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js index dbe4598f599..40f3c9633ab 100644 --- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js @@ -1,4 +1,12 @@ -import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } from '@gitlab/ui'; +import { + GlLoadingIcon, + GlTable, + GlLink, + GlBadge, + GlPagination, + GlModal, + GlFormCheckbox, +} from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; @@ -8,15 +16,22 @@ import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue'; import FeedbackBanner from '~/artifacts/components/feedback_banner.vue'; import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue'; import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue'; +import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants'; +import { + ARCHIVE_FILE_TYPE, + JOBS_PER_PAGE, + I18N_FETCH_ERROR, + INITIAL_CURRENT_PAGE, + BULK_DELETE_FEATURE_FLAG, +} from '~/artifacts/constants'; import { totalArtifactsSizeForJob } from '~/artifacts/utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -24,6 +39,8 @@ describe('JobArtifactsTable component', () => { let wrapper; let requestHandlers; + const mockToastShow = jest.fn(); + const findBanner = () => wrapper.findComponent(FeedbackBanner); const findLoadingState = () => wrapper.findComponent(GlLoadingIcon); @@ -55,6 +72,11 @@ describe('JobArtifactsTable component', () => { const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button'); const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button'); + // first checkbox is a "select all", this finder should get the first job checkbox + const findJobCheckbox = () => wrapper.findAllComponents(GlFormCheckbox).at(1); + const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox); + const findBulkDelete = () => wrapper.findComponent(ArtifactsBulkDelete); + const findPagination = () => wrapper.findComponent(GlPagination); const setPage = async (page) => { findPagination().vm.$emit('input', page); @@ -69,7 +91,14 @@ describe('JobArtifactsTable component', () => { ]; } const getJobArtifactsResponseThatPaginates = { - data: { project: { jobs: { nodes: enoughJobsToPaginate } } }, + data: { + project: { + jobs: { + nodes: enoughJobsToPaginate, + pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo, hasNextPage: true }, + }, + }, + }, }; const job = getJobArtifactsResponse.data.project.jobs.nodes[0]; @@ -77,13 +106,14 @@ describe('JobArtifactsTable component', () => { (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE, ); - const createComponent = ( + const createComponent = ({ handlers = { getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse), }, data = {}, canDestroyArtifacts = true, - ) => { + glFeatures = {}, + } = {}) => { requestHandlers = handlers; wrapper = mountExtended(JobArtifactsTable, { apolloProvider: createMockApollo([ @@ -91,8 +121,15 @@ describe('JobArtifactsTable component', () => { ]), provide: { projectPath: 'project/path', + projectId: 'gid://projects/id', canDestroyArtifacts, artifactsManagementFeedbackImagePath: 'banner/image/path', + glFeatures, + }, + mocks: { + $toast: { + show: mockToastShow, + }, }, data() { return data; @@ -100,10 +137,6 @@ describe('JobArtifactsTable component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders feedback banner', () => { createComponent(); @@ -118,7 +151,9 @@ describe('JobArtifactsTable component', () => { it('on error, shows an alert', async () => { createComponent({ - getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')), + handlers: { + getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')), + }, }); await waitForPromises(); @@ -259,10 +294,10 @@ describe('JobArtifactsTable component', () => { archive: { downloadPath: null }, }; - createComponent( - { getJobArtifactsQuery: jest.fn() }, - { jobArtifacts: [jobWithoutDownloadPath] }, - ); + createComponent({ + handlers: { getJobArtifactsQuery: jest.fn() }, + data: { jobArtifacts: [jobWithoutDownloadPath] }, + }); await waitForPromises(); @@ -285,10 +320,10 @@ describe('JobArtifactsTable component', () => { browseArtifactsPath: null, }; - createComponent( - { getJobArtifactsQuery: jest.fn() }, - { jobArtifacts: [jobWithoutBrowsePath] }, - ); + createComponent({ + handlers: { getJobArtifactsQuery: jest.fn() }, + data: { jobArtifacts: [jobWithoutBrowsePath] }, + }); await waitForPromises(); @@ -298,7 +333,7 @@ describe('JobArtifactsTable component', () => { describe('delete button', () => { it('does not show when user does not have permission', async () => { - createComponent({}, {}, false); + createComponent({ canDestroyArtifacts: false }); await waitForPromises(); @@ -314,50 +349,125 @@ describe('JobArtifactsTable component', () => { }); }); + describe('bulk delete', () => { + describe('with permission and feature flag enabled', () => { + beforeEach(async () => { + createComponent({ + canDestroyArtifacts: true, + glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true }, + }); + + await waitForPromises(); + }); + + it('shows selected artifacts when a job is checked', async () => { + expect(findBulkDelete().exists()).toBe(false); + + await findJobCheckbox().vm.$emit('input', true); + + expect(findBulkDelete().exists()).toBe(true); + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual( + job.artifacts.nodes.map((node) => node.id), + ); + }); + + it('disappears when selected artifacts are cleared', async () => { + await findJobCheckbox().vm.$emit('input', true); + + expect(findBulkDelete().exists()).toBe(true); + + await findBulkDelete().vm.$emit('clearSelectedArtifacts'); + + expect(findBulkDelete().exists()).toBe(false); + }); + + it('shows a toast when artifacts are deleted', async () => { + const count = job.artifacts.nodes.length; + + await findJobCheckbox().vm.$emit('input', true); + findBulkDelete().vm.$emit('deleted', count); + + expect(mockToastShow).toHaveBeenCalledWith(`${count} selected artifacts deleted`); + }); + }); + + it('shows no checkboxes without permission', async () => { + createComponent({ + canDestroyArtifacts: false, + glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true }, + }); + + await waitForPromises(); + + expect(findAnyCheckbox().exists()).toBe(false); + }); + + it('shows no checkboxes with feature flag disabled', async () => { + createComponent({ + canDestroyArtifacts: true, + glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false }, + }); + + await waitForPromises(); + + expect(findAnyCheckbox().exists()).toBe(false); + }); + }); + describe('pagination', () => { - const { pageInfo } = getJobArtifactsResponse.data.project.jobs; + const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs; + const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates); beforeEach(async () => { - createComponent( - { - getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates), + createComponent({ + handlers: { + getJobArtifactsQuery: query, }, - { - count: enoughJobsToPaginate.length, - pageInfo, - }, - ); + data: { pageInfo }, + }); await waitForPromises(); }); it('renders pagination and passes page props', () => { - expect(findPagination().exists()).toBe(true); expect(findPagination().props()).toMatchObject({ - value: wrapper.vm.pagination.currentPage, - prevPage: wrapper.vm.prevPage, - nextPage: wrapper.vm.nextPage, + value: INITIAL_CURRENT_PAGE, + prevPage: Number(pageInfo.hasPreviousPage), + nextPage: Number(pageInfo.hasNextPage), + }); + + expect(query).toHaveBeenCalledWith({ + projectPath: 'project/path', + firstPageSize: JOBS_PER_PAGE, + lastPageSize: null, + nextPageCursor: '', + prevPageCursor: '', }); }); - it('updates query variables when going to previous page', () => { - return setPage(1).then(() => { - expect(wrapper.vm.queryVariables).toMatchObject({ - projectPath: 'project/path', - nextPageCursor: undefined, - prevPageCursor: pageInfo.startCursor, - }); + it('updates query variables when going to previous page', async () => { + await setPage(1); + + expect(query).toHaveBeenLastCalledWith({ + projectPath: 'project/path', + firstPageSize: null, + lastPageSize: JOBS_PER_PAGE, + prevPageCursor: pageInfo.startCursor, }); + expect(findPagination().props('value')).toEqual(1); }); - it('updates query variables when going to next page', () => { - return setPage(2).then(() => { - expect(wrapper.vm.queryVariables).toMatchObject({ - lastPageSize: null, - nextPageCursor: pageInfo.endCursor, - prevPageCursor: '', - }); + it('updates query variables when going to next page', async () => { + await setPage(2); + + expect(query).toHaveBeenLastCalledWith({ + projectPath: 'project/path', + firstPageSize: JOBS_PER_PAGE, + lastPageSize: null, + prevPageCursor: '', + nextPageCursor: pageInfo.endCursor, }); + expect(findPagination().props('value')).toEqual(2); }); }); }); diff --git a/spec/frontend/artifacts/components/job_checkbox_spec.js b/spec/frontend/artifacts/components/job_checkbox_spec.js new file mode 100644 index 00000000000..95cc548b8c8 --- /dev/null +++ b/spec/frontend/artifacts/components/job_checkbox_spec.js @@ -0,0 +1,71 @@ +import { GlFormCheckbox } from '@gitlab/ui'; +import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import JobCheckbox from '~/artifacts/components/job_checkbox.vue'; + +describe('JobCheckbox component', () => { + let wrapper; + + const mockArtifactNodes = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes; + const mockSelectedArtifacts = [mockArtifactNodes[0], mockArtifactNodes[1]]; + const mockUnselectedArtifacts = [mockArtifactNodes[2]]; + + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); + + const createComponent = ({ + hasArtifacts = true, + selectedArtifacts = mockSelectedArtifacts, + unselectedArtifacts = mockUnselectedArtifacts, + } = {}) => { + wrapper = shallowMountExtended(JobCheckbox, { + propsData: { + hasArtifacts, + selectedArtifacts, + unselectedArtifacts, + }, + mocks: { GlFormCheckbox }, + }); + }; + + it('is disabled when the job has no artifacts', () => { + createComponent({ hasArtifacts: false }); + + expect(findCheckbox().attributes('disabled')).toBe('true'); + }); + + describe('when some artifacts are selected', () => { + beforeEach(() => { + createComponent(); + }); + + it('is indeterminate', () => { + expect(findCheckbox().attributes('indeterminate')).toBe('true'); + expect(findCheckbox().attributes('checked')).toBeUndefined(); + }); + + it('selects the unselected artifacts on click', () => { + findCheckbox().vm.$emit('input', true); + + expect(wrapper.emitted('selectArtifact')).toMatchObject([[mockUnselectedArtifacts[0], true]]); + }); + }); + + describe('when all artifacts are selected', () => { + beforeEach(() => { + createComponent({ unselectedArtifacts: [] }); + }); + + it('is checked', () => { + expect(findCheckbox().attributes('checked')).toBe('true'); + }); + + it('deselects the selected artifacts on click', () => { + findCheckbox().vm.$emit('input', false); + + expect(wrapper.emitted('selectArtifact')).toMatchObject([ + [mockSelectedArtifacts[0], false], + [mockSelectedArtifacts[1], false], + ]); + }); + }); +}); diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js index ca94acfa444..efdebe5f3b0 100644 --- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js +++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js @@ -78,8 +78,6 @@ describe('Keep latest artifact checkbox', () => { }; afterEach(() => { - wrapper.destroy(); - wrapper = null; apolloProvider = null; }); diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js deleted file mode 100644 index 3ae7fcf1c49..00000000000 --- a/spec/frontend/authentication/u2f/authenticate_spec.js +++ /dev/null @@ -1,104 +0,0 @@ -import $ from 'jquery'; -import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import U2FAuthenticate from '~/authentication/u2f/authenticate'; -import 'vendor/u2f'; -import MockU2FDevice from './mock_u2f_device'; - -describe('U2FAuthenticate', () => { - let u2fDevice; - let container; - let component; - - beforeEach(() => { - loadHTMLFixture('u2f/authenticate.html'); - u2fDevice = new MockU2FDevice(); - container = $('#js-authenticate-token-2fa'); - component = new U2FAuthenticate( - container, - '#js-login-token-2fa-form', - { - sign_requests: [], - }, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form'), - ); - }); - - afterEach(() => { - resetHTMLFixture(); - }); - - describe('with u2f unavailable', () => { - let oldu2f; - - beforeEach(() => { - jest.spyOn(component, 'switchToFallbackUI').mockImplementation(() => {}); - oldu2f = window.u2f; - window.u2f = null; - }); - - afterEach(() => { - window.u2f = oldu2f; - }); - - it('falls back to normal 2fa', async () => { - await component.start(); - expect(component.switchToFallbackUI).toHaveBeenCalled(); - }); - }); - - describe('with u2f available', () => { - beforeEach(() => { - // bypass automatic form submission within renderAuthenticated - jest.spyOn(component, 'renderAuthenticated').mockReturnValue(true); - u2fDevice = new MockU2FDevice(); - - return component.start(); - }); - - it('allows authenticating via a U2F device', () => { - const inProgressMessage = container.find('p'); - - expect(inProgressMessage.text()).toContain('Trying to communicate with your device'); - u2fDevice.respondToAuthenticateRequest({ - deviceData: 'this is data from the device', - }); - - expect(component.renderAuthenticated).toHaveBeenCalledWith( - '{"deviceData":"this is data from the device"}', - ); - }); - - describe('errors', () => { - it('displays an error message', () => { - const setupButton = container.find('#js-login-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToAuthenticateRequest({ - errorCode: 'error!', - }); - const errorMessage = container.find('p'); - - expect(errorMessage.text()).toContain('There was a problem communicating with your device'); - }); - - it('allows retrying authentication after an error', () => { - let setupButton = container.find('#js-login-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToAuthenticateRequest({ - errorCode: 'error!', - }); - const retryButton = container.find('#js-token-2fa-try-again'); - retryButton.trigger('click'); - setupButton = container.find('#js-login-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToAuthenticateRequest({ - deviceData: 'this is data from the device', - }); - - expect(component.renderAuthenticated).toHaveBeenCalledWith( - '{"deviceData":"this is data from the device"}', - ); - }); - }); - }); -}); diff --git a/spec/frontend/authentication/u2f/mock_u2f_device.js b/spec/frontend/authentication/u2f/mock_u2f_device.js deleted file mode 100644 index ec8425a4e3e..00000000000 --- a/spec/frontend/authentication/u2f/mock_u2f_device.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-unused-expressions */ - -export default class MockU2FDevice { - constructor() { - this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this); - this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this); - window.u2f || (window.u2f = {}); - window.u2f.register = (appId, registerRequests, signRequests, callback) => { - this.registerCallback = callback; - }; - window.u2f.sign = (appId, challenges, signRequests, callback) => { - this.authenticateCallback = callback; - }; - } - - respondToRegisterRequest(params) { - return this.registerCallback(params); - } - - respondToAuthenticateRequest(params) { - return this.authenticateCallback(params); - } -} diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js deleted file mode 100644 index 23d1e5c7dee..00000000000 --- a/spec/frontend/authentication/u2f/register_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import $ from 'jquery'; -import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { trimText } from 'helpers/text_helper'; -import U2FRegister from '~/authentication/u2f/register'; -import 'vendor/u2f'; -import MockU2FDevice from './mock_u2f_device'; - -describe('U2FRegister', () => { - let u2fDevice; - let container; - let component; - - beforeEach(() => { - loadHTMLFixture('u2f/register.html'); - u2fDevice = new MockU2FDevice(); - container = $('#js-register-token-2fa'); - component = new U2FRegister(container, {}); - return component.start(); - }); - - afterEach(() => { - resetHTMLFixture(); - }); - - it('allows registering a U2F device', () => { - const setupButton = container.find('#js-setup-token-2fa-device'); - - expect(trimText(setupButton.text())).toBe('Set up new device'); - setupButton.trigger('click'); - const inProgressMessage = container.children('p'); - - expect(inProgressMessage.text()).toContain('Trying to communicate with your device'); - u2fDevice.respondToRegisterRequest({ - deviceData: 'this is data from the device', - }); - const registeredMessage = container.find('p'); - const deviceResponse = container.find('#js-device-response'); - - expect(registeredMessage.text()).toContain('Your device was successfully set up!'); - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); - }); - - describe('errors', () => { - it("doesn't allow the same device to be registered twice (for the same user", () => { - const setupButton = container.find('#js-setup-token-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToRegisterRequest({ - errorCode: 4, - }); - const errorMessage = container.find('p'); - - expect(errorMessage.text()).toContain('already been registered with us'); - }); - - it('displays an error message for other errors', () => { - const setupButton = container.find('#js-setup-token-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToRegisterRequest({ - errorCode: 'error!', - }); - const errorMessage = container.find('p'); - - expect(errorMessage.text()).toContain('There was a problem communicating with your device'); - }); - - it('allows retrying registration after an error', () => { - let setupButton = container.find('#js-setup-token-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToRegisterRequest({ - errorCode: 'error!', - }); - const retryButton = container.find('#js-token-2fa-try-again'); - retryButton.trigger('click'); - setupButton = container.find('#js-setup-token-2fa-device'); - setupButton.trigger('click'); - u2fDevice.respondToRegisterRequest({ - deviceData: 'this is data from the device', - }); - const registeredMessage = container.find('p'); - - expect(registeredMessage.text()).toContain('Your device was successfully set up!'); - }); - }); -}); diff --git a/spec/frontend/authentication/u2f/util_spec.js b/spec/frontend/authentication/u2f/util_spec.js deleted file mode 100644 index 67fd4c73243..00000000000 --- a/spec/frontend/authentication/u2f/util_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { canInjectU2fApi } from '~/authentication/u2f/util'; - -describe('U2F Utils', () => { - describe('canInjectU2fApi', () => { - it('returns false for Chrome < 41', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36'; - - expect(canInjectU2fApi(userAgent)).toBe(false); - }); - - it('returns true for Chrome >= 41', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'; - - expect(canInjectU2fApi(userAgent)).toBe(true); - }); - - it('returns false for Opera < 40', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25'; - - expect(canInjectU2fApi(userAgent)).toBe(false); - }); - - it('returns true for Opera >= 40', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991'; - - expect(canInjectU2fApi(userAgent)).toBe(true); - }); - - it('returns false for Safari', () => { - const userAgent = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4'; - - expect(canInjectU2fApi(userAgent)).toBe(false); - }); - - it('returns false for Chrome on Android', () => { - const userAgent = - 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36'; - - expect(canInjectU2fApi(userAgent)).toBe(false); - }); - - it('returns false for Chrome on iOS', () => { - const userAgent = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; - - expect(canInjectU2fApi(userAgent)).toBe(false); - }); - - it('returns false for Safari on iOS', () => { - const userAgent = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1'; - - expect(canInjectU2fApi(userAgent)).toBe(false); - }); - }); -}); diff --git a/spec/frontend/authentication/webauthn/components/registration_spec.js b/spec/frontend/authentication/webauthn/components/registration_spec.js new file mode 100644 index 00000000000..1221626db7d --- /dev/null +++ b/spec/frontend/authentication/webauthn/components/registration_spec.js @@ -0,0 +1,255 @@ +import { nextTick } from 'vue'; +import { GlAlert, GlButton, GlForm, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import Registration from '~/authentication/webauthn/components/registration.vue'; +import { + I18N_BUTTON_REGISTER, + I18N_BUTTON_SETUP, + I18N_BUTTON_TRY_AGAIN, + I18N_ERROR_HTTP, + I18N_ERROR_UNSUPPORTED_BROWSER, + I18N_INFO_TEXT, + I18N_STATUS_SUCCESS, + I18N_STATUS_WAITING, + STATE_ERROR, + STATE_READY, + STATE_SUCCESS, + STATE_UNSUPPORTED, + STATE_WAITING, + WEBAUTHN_REGISTER, +} from '~/authentication/webauthn/constants'; +import * as WebAuthnUtils from '~/authentication/webauthn/util'; +import WebAuthnError from '~/authentication/webauthn/error'; + +const csrfToken = 'mock-csrf-token'; +jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken })); +jest.mock('~/authentication/webauthn/util'); +jest.mock('~/authentication/webauthn/error'); + +describe('Registration', () => { + const initialError = null; + const passwordRequired = true; + const targetPath = '/-/profile/two_factor_auth/create_webauthn'; + let wrapper; + + const createComponent = (provide = {}) => { + wrapper = shallowMountExtended(Registration, { + provide: { initialError, passwordRequired, targetPath, ...provide }, + }); + }; + + const findButton = () => wrapper.findComponent(GlButton); + + describe(`when ${STATE_UNSUPPORTED} state`, () => { + it('shows an error if using unsecure scheme (HTTP)', () => { + // `supported` function returns false for HTTP because `navigator.credentials` is undefined. + WebAuthnUtils.supported.mockReturnValue(false); + WebAuthnUtils.isHTTPS.mockReturnValue(false); + createComponent(); + + const alert = wrapper.findComponent(GlAlert); + expect(alert.props('variant')).toBe('danger'); + expect(alert.text()).toBe(I18N_ERROR_HTTP); + }); + + it('shows an error if using unsupported browser', () => { + WebAuthnUtils.supported.mockReturnValue(false); + WebAuthnUtils.isHTTPS.mockReturnValue(true); + createComponent(); + + const alert = wrapper.findComponent(GlAlert); + expect(alert.props('variant')).toBe('danger'); + expect(alert.text()).toBe(I18N_ERROR_UNSUPPORTED_BROWSER); + }); + }); + + describe('when scheme or browser are supported', () => { + const mockCreate = jest.fn(); + + const clickSetupDeviceButton = () => { + findButton().vm.$emit('click'); + return nextTick(); + }; + + const setupDevice = () => { + clickSetupDeviceButton(); + return waitForPromises(); + }; + + beforeEach(() => { + WebAuthnUtils.isHTTPS.mockReturnValue(true); + WebAuthnUtils.supported.mockReturnValue(true); + global.navigator.credentials = { create: mockCreate }; + gon.webauthn = { options: {} }; + }); + + afterEach(() => { + global.navigator.credentials = undefined; + }); + + describe(`when ${STATE_READY} state`, () => { + it('shows button and explanation text', () => { + createComponent(); + + expect(findButton().text()).toBe(I18N_BUTTON_SETUP); + expect(wrapper.text()).toContain(I18N_INFO_TEXT); + }); + }); + + describe(`when ${STATE_WAITING} state`, () => { + it('shows loading icon and message after pressing the button', async () => { + createComponent(); + + await clickSetupDeviceButton(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.text()).toContain(I18N_STATUS_WAITING); + }); + }); + + describe(`when ${STATE_SUCCESS} state`, () => { + const credentials = 1; + + const findCurrentPasswordInput = () => wrapper.findByTestId('current-password-input'); + const findDeviceNameInput = () => wrapper.findByTestId('device-name-input'); + + beforeEach(() => { + mockCreate.mockResolvedValueOnce(true); + WebAuthnUtils.convertCreateResponse.mockReturnValue(credentials); + }); + + describe('registration form', () => { + it('has correct action', async () => { + createComponent(); + + await setupDevice(); + + expect(wrapper.findComponent(GlForm).attributes('action')).toBe(targetPath); + }); + + describe('when password is required', () => { + it('shows device name and password fields', async () => { + createComponent(); + + await setupDevice(); + + expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS); + + // Visible inputs + expect(findCurrentPasswordInput().attributes('name')).toBe('current_password'); + expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]'); + + // Hidden inputs + expect( + wrapper + .find('input[name="device_registration[device_response]"]') + .attributes('value'), + ).toBe(`${credentials}`); + expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe( + csrfToken, + ); + + expect(findButton().text()).toBe(I18N_BUTTON_REGISTER); + }); + + it('enables the register device button when device name and password are filled', async () => { + createComponent(); + + await setupDevice(); + + expect(findButton().props('disabled')).toBe(true); + + // Visible inputs + findCurrentPasswordInput().vm.$emit('input', 'my current password'); + findDeviceNameInput().vm.$emit('input', 'my device name'); + await nextTick(); + + expect(findButton().props('disabled')).toBe(false); + }); + }); + + describe('when password is not required', () => { + it('shows a device name field', async () => { + createComponent({ passwordRequired: false }); + + await setupDevice(); + + expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS); + + // Visible inputs + expect(findCurrentPasswordInput().exists()).toBe(false); + expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]'); + + // Hidden inputs + expect( + wrapper + .find('input[name="device_registration[device_response]"]') + .attributes('value'), + ).toBe(`${credentials}`); + expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe( + csrfToken, + ); + + expect(findButton().text()).toBe(I18N_BUTTON_REGISTER); + }); + + it('enables the register device button when device name is filled', async () => { + createComponent({ passwordRequired: false }); + + await setupDevice(); + + expect(findButton().props('disabled')).toBe(true); + + findDeviceNameInput().vm.$emit('input', 'my device name'); + await nextTick(); + + expect(findButton().props('disabled')).toBe(false); + }); + }); + }); + }); + + describe(`when ${STATE_ERROR} state`, () => { + it('shows an initial error message and a retry button', async () => { + const myError = 'my error'; + createComponent({ initialError: myError }); + + const alert = wrapper.findComponent(GlAlert); + expect(alert.props()).toMatchObject({ + variant: 'danger', + secondaryButtonText: I18N_BUTTON_TRY_AGAIN, + }); + expect(alert.text()).toContain(myError); + }); + + it('shows an error message and a retry button', async () => { + createComponent(); + const error = new Error(); + mockCreate.mockRejectedValueOnce(error); + + await setupDevice(); + + expect(WebAuthnError).toHaveBeenCalledWith(error, WEBAUTHN_REGISTER); + expect(wrapper.findComponent(GlAlert).props()).toMatchObject({ + variant: 'danger', + secondaryButtonText: I18N_BUTTON_TRY_AGAIN, + }); + }); + + it('recovers after an error (error to success state)', async () => { + createComponent(); + mockCreate.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(true); + + await setupDevice(); + + expect(wrapper.findComponent(GlAlert).props('variant')).toBe('danger'); + + wrapper.findComponent(GlAlert).vm.$emit('secondaryAction'); + await waitForPromises(); + + expect(wrapper.findComponent(GlAlert).props('variant')).toBe('info'); + }); + }); + }); +}); diff --git a/spec/frontend/authentication/webauthn/error_spec.js b/spec/frontend/authentication/webauthn/error_spec.js index 9b71f77dde2..b979173edc6 100644 --- a/spec/frontend/authentication/webauthn/error_spec.js +++ b/spec/frontend/authentication/webauthn/error_spec.js @@ -1,16 +1,17 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import WebAuthnError from '~/authentication/webauthn/error'; +import { WEBAUTHN_AUTHENTICATE, WEBAUTHN_REGISTER } from '~/authentication/webauthn/constants'; describe('WebAuthnError', () => { it.each([ [ 'NotSupportedError', 'Your device is not compatible with GitLab. Please try another device', - 'authenticate', + WEBAUTHN_AUTHENTICATE, ], - ['InvalidStateError', 'This device has not been registered with us.', 'authenticate'], - ['InvalidStateError', 'This device has already been registered with us.', 'register'], - ['UnknownError', 'There was a problem communicating with your device.', 'register'], + ['InvalidStateError', 'This device has not been registered with us.', WEBAUTHN_AUTHENTICATE], + ['InvalidStateError', 'This device has already been registered with us.', WEBAUTHN_REGISTER], + ['UnknownError', 'There was a problem communicating with your device.', WEBAUTHN_REGISTER], ])('exception %s will have message %s, flow type: %s', (exception, expectedMessage, flowType) => { expect(new WebAuthnError(new DOMException('', exception), flowType).message()).toEqual( expectedMessage, @@ -24,7 +25,7 @@ describe('WebAuthnError', () => { const expectedMessage = 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.'; expect( - new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(), + new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(), ).toEqual(expectedMessage); }); @@ -33,7 +34,7 @@ describe('WebAuthnError', () => { const expectedMessage = 'There was a problem communicating with your device.'; expect( - new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(), + new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(), ).toEqual(expectedMessage); }); }); diff --git a/spec/frontend/authentication/webauthn/util_spec.js b/spec/frontend/authentication/webauthn/util_spec.js index bc44b47d0ba..831d1636b8c 100644 --- a/spec/frontend/authentication/webauthn/util_spec.js +++ b/spec/frontend/authentication/webauthn/util_spec.js @@ -1,4 +1,9 @@ -import { base64ToBuffer, bufferToBase64, base64ToBase64Url } from '~/authentication/webauthn/util'; +import { + base64ToBuffer, + bufferToBase64, + base64ToBase64Url, + supported, +} from '~/authentication/webauthn/util'; const encodedString = 'SGVsbG8gd29ybGQh'; const stringBytes = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; @@ -31,4 +36,28 @@ describe('Webauthn utils', () => { expect(base64ToBase64Url(argument)).toBe(expectedResult); }); }); + + describe('supported', () => { + afterEach(() => { + global.navigator.credentials = undefined; + window.PublicKeyCredential = undefined; + }); + + it.each` + credentials | PublicKeyCredential | expected + ${undefined} | ${undefined} | ${false} + ${{}} | ${undefined} | ${false} + ${{ create: true }} | ${undefined} | ${false} + ${{ create: true, get: true }} | ${undefined} | ${false} + ${{ create: true, get: true }} | ${true} | ${true} + `( + 'returns $expected when credentials is $credentials and PublicKeyCredential is $PublicKeyCredential', + ({ credentials, PublicKeyCredential, expected }) => { + global.navigator.credentials = credentials; + window.PublicKeyCredential = PublicKeyCredential; + + expect(supported()).toBe(expected); + }, + ); + }); }); diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js index 1a54b9909ba..35a1603d375 100644 --- a/spec/frontend/awards_handler_spec.js +++ b/spec/frontend/awards_handler_spec.js @@ -6,10 +6,8 @@ import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_fra import loadAwardsHandler from '~/awards_handler'; window.gl = window.gl || {}; -window.gon = window.gon || {}; let awardsHandler = null; -const urlRoot = gon.relative_url_root; describe('AwardsHandler', () => { useFakeRequestAnimationFrame(); @@ -95,9 +93,6 @@ describe('AwardsHandler', () => { }); afterEach(() => { - // restore original url root value - gon.relative_url_root = urlRoot; - clearEmojiMock(); // Undo what we did to the shared <body> diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js index 0a736df7075..d7519f1f80d 100644 --- a/spec/frontend/badges/components/badge_form_spec.js +++ b/spec/frontend/badges/components/badge_form_spec.js @@ -43,7 +43,6 @@ describe('BadgeForm component', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js index ee7ccac974a..cbbeb36ff33 100644 --- a/spec/frontend/badges/components/badge_list_row_spec.js +++ b/spec/frontend/badges/components/badge_list_row_spec.js @@ -43,7 +43,6 @@ describe('BadgeListRow component', () => { }; afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js index 606b1bc9cce..374b7b50af4 100644 --- a/spec/frontend/badges/components/badge_list_spec.js +++ b/spec/frontend/badges/components/badge_list_spec.js @@ -38,10 +38,6 @@ describe('BadgeList component', () => { wrapper = mount(BadgeList, { store }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('for project badges', () => { it('renders a header with the badge count', () => { createComponent({ diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js index bddb6d3801c..7ad2c99869c 100644 --- a/spec/frontend/badges/components/badge_settings_spec.js +++ b/spec/frontend/badges/components/badge_settings_spec.js @@ -32,10 +32,6 @@ describe('BadgeSettings component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays modal if button for deleting a badge is clicked', async () => { const button = wrapper.find('[data-testid="delete-badge"]'); diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js index b468e38f19e..c933c1b5434 100644 --- a/spec/frontend/badges/components/badge_spec.js +++ b/spec/frontend/badges/components/badge_spec.js @@ -24,10 +24,6 @@ describe('Badge component', () => { wrapper = mount(Badge, { propsData }); }; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { return createComponent({ ...dummyProps }, '#dummy-element'); }); diff --git a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js index c922d6a9809..f667ebc0fcb 100644 --- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js +++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js @@ -28,10 +28,6 @@ describe('Batch comments diff file drafts component', () => { }); } - afterEach(() => { - vm.destroy(); - }); - it('renders list of draft notes', () => { factory(); diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index 924d88866ee..159e36c1364 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -49,10 +49,6 @@ describe('Batch comments draft note component', () => { draft = createDraft(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders template', () => { createComponent(); expect(wrapper.findComponent(GlBadge).exists()).toBe(true); diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js index c3a7946c85c..850a7efb4ed 100644 --- a/spec/frontend/batch_comments/components/drafts_count_spec.js +++ b/spec/frontend/batch_comments/components/drafts_count_spec.js @@ -15,10 +15,6 @@ describe('Batch comments drafts count component', () => { wrapper = mount(DraftsCount, { store }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders count', () => { expect(wrapper.text()).toContain('1'); }); diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js index f86e003ab5f..3a28bf4ade8 100644 --- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js @@ -1,7 +1,6 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; -import { GlDisclosureDropdown } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; import { visitUrl } from '~/lib/utils/url_utility'; import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue'; @@ -46,9 +45,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = }, }); - wrapper = shallowMount(PreviewDropdown, { + wrapper = mount(PreviewDropdown, { store, - stubs: { GlDisclosureDropdown }, + stubs: { + PreviewItem: true, + }, }); } @@ -59,12 +60,12 @@ describe('Batch comments preview dropdown', () => { viewDiffsFileByFile: true, sortedDrafts: [{ id: 1, file_hash: 'hash' }], }); - - findPreviewItem().vm.$emit('click'); - + findPreviewItem().trigger('click'); await nextTick(); expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash'); + + await nextTick(); expect(scrollToDraft).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ id: 1, file_hash: 'hash' }), @@ -77,7 +78,7 @@ describe('Batch comments preview dropdown', () => { sortedDrafts: [{ id: 1 }], }); - findPreviewItem().vm.$emit('click'); + findPreviewItem().trigger('click'); await nextTick(); @@ -93,7 +94,7 @@ describe('Batch comments preview dropdown', () => { sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }], }); - findPreviewItem().vm.$emit('click'); + findPreviewItem().trigger('click'); await nextTick(); diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js index 6a99294f855..a19a72af813 100644 --- a/spec/frontend/batch_comments/components/preview_item_spec.js +++ b/spec/frontend/batch_comments/components/preview_item_spec.js @@ -26,10 +26,6 @@ describe('Batch comments draft preview item component', () => { wrapper = mount(PreviewItem, { store, propsData: { draft, isLast } }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders text content', () => { createComponent(false, { note_html: '<img src="" /><p>Hello world</p>' }); diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js index 0a4c9ff62e4..923e86a7e64 100644 --- a/spec/frontend/batch_comments/components/review_bar_spec.js +++ b/spec/frontend/batch_comments/components/review_bar_spec.js @@ -20,10 +20,6 @@ describe('Batch comments review bar component', () => { document.body.className = ''; }); - afterEach(() => { - wrapper.destroy(); - }); - 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); diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js index 003a6d86371..ac6198ec8b5 100644 --- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js @@ -47,7 +47,6 @@ const findForm = () => wrapper.findByTestId('submit-gl-form'); describe('Batch comments submit dropdown', () => { afterEach(() => { - wrapper.destroy(); window.mrTabs = null; }); diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js index 20eedcbb25b..57bafb51cd6 100644 --- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js +++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js @@ -317,4 +317,10 @@ describe('Batch comments store actions', () => { expect(window.mrTabs.tabShown).toHaveBeenCalledWith('diffs'); }); }); + + describe('clearDrafts', () => { + it('commits CLEAR_DRAFTS', () => { + return testAction(actions.clearDrafts, null, null, [{ type: 'CLEAR_DRAFTS' }], []); + }); + }); }); diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js index fe01de638c2..ad6a1a38164 100644 --- a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js +++ b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js @@ -104,4 +104,14 @@ describe('Batch comments mutations', () => { ]); }); }); + + describe(types.CLEAR_DRAFTS, () => { + it('clears drafts array', () => { + state.drafts.push({ id: 1 }); + + mutations[types.CLEAR_DRAFTS](state); + + expect(state.drafts).toEqual([]); + }); + }); }); diff --git a/spec/frontend/behaviors/components/diagram_performance_warning_spec.js b/spec/frontend/behaviors/components/diagram_performance_warning_spec.js index c58c2bc55a9..7e6b20da4d4 100644 --- a/spec/frontend/behaviors/components/diagram_performance_warning_spec.js +++ b/spec/frontend/behaviors/components/diagram_performance_warning_spec.js @@ -11,10 +11,6 @@ describe('DiagramPerformanceWarning component', () => { wrapper = shallowMount(DiagramPerformanceWarning); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders warning alert with button', () => { expect(findAlert().props()).toMatchObject({ primaryButtonText: DiagramPerformanceWarning.i18n.buttonText, diff --git a/spec/frontend/behaviors/components/json_table_spec.js b/spec/frontend/behaviors/components/json_table_spec.js index 42b4a051d4d..a82310873ed 100644 --- a/spec/frontend/behaviors/components/json_table_spec.js +++ b/spec/frontend/behaviors/components/json_table_spec.js @@ -59,10 +59,6 @@ describe('behaviors/components/json_table', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findTable = () => wrapper.findComponent(GlTable); const findTableCaption = () => wrapper.findByTestId('slot-table-caption'); const findFilterInput = () => wrapper.findComponent(GlFormInput); diff --git a/spec/frontend/behaviors/copy_to_clipboard_spec.js b/spec/frontend/behaviors/copy_to_clipboard_spec.js index c5beaa0ba5d..74a396eb8cb 100644 --- a/spec/frontend/behaviors/copy_to_clipboard_spec.js +++ b/spec/frontend/behaviors/copy_to_clipboard_spec.js @@ -31,7 +31,7 @@ describe('initCopyToClipboard', () => { const defaultButtonAttributes = { 'data-clipboard-text': 'foo bar', title, - 'data-title': title, + 'data-original-title': title, }; const createButton = (attributes = {}) => { const combinedAttributes = { ...defaultButtonAttributes, ...attributes }; diff --git a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js index 38d19ac3808..ad70efdf7c3 100644 --- a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js +++ b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js @@ -22,14 +22,9 @@ describe('highlightCurrentUser', () => { describe('without current user', () => { beforeEach(() => { - window.gon = window.gon || {}; window.gon.current_user_id = null; }); - afterEach(() => { - delete window.gon.current_user_id; - }); - it('does not highlight the user', () => { const initialHtml = rootElement.outerHTML; @@ -41,14 +36,9 @@ describe('highlightCurrentUser', () => { describe('with current user', () => { beforeEach(() => { - window.gon = window.gon || {}; window.gon.current_user_id = 2; }); - afterEach(() => { - delete window.gon.current_user_id; - }); - it('highlights current user', () => { highlightCurrentUser(elements); diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js index c87d11742dc..f464c01ac15 100644 --- a/spec/frontend/behaviors/markdown/render_observability_spec.js +++ b/spec/frontend/behaviors/markdown/render_observability_spec.js @@ -1,38 +1,43 @@ +import Vue from 'vue'; +import { createWrapper } from '@vue/test-utils'; import renderObservability from '~/behaviors/markdown/render_observability'; -import * as ColorUtils from '~/lib/utils/color_utils'; +import { INLINE_EMBED_DIMENSIONS, SKELETON_VARIANT_EMBED } from '~/observability/constants'; +import ObservabilityApp from '~/observability/components/observability_app.vue'; -describe('Observability iframe renderer', () => { - const findObservabilityIframes = (theme = 'light') => - document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`); - - const renderEmbeddedObservability = () => { - renderObservability([...document.querySelectorAll('.js-render-observability')]); - jest.runAllTimers(); - }; +describe('renderObservability', () => { + let subject; beforeEach(() => { - document.body.dataset.page = ''; - document.body.innerHTML = ''; + subject = document.createElement('div'); + subject.classList.add('js-render-observability'); + subject.dataset.frameUrl = 'https://observe.gitlab.com/'; + document.body.appendChild(subject); }); - it('renders an observability iframe', () => { - document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`; - - expect(findObservabilityIframes()).toHaveLength(0); - - renderEmbeddedObservability(); - - expect(findObservabilityIframes()).toHaveLength(1); + afterEach(() => { + subject.remove(); }); - it('renders iframe with dark param when GL has dark theme', () => { - document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`; - jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true); + it('should return an array of Vue instances', () => { + const vueInstances = renderObservability([ + ...document.querySelectorAll('.js-render-observability'), + ]); + expect(vueInstances).toEqual([expect.any(Vue)]); + }); - expect(findObservabilityIframes('dark')).toHaveLength(0); + it('should correctly pass props to the ObservabilityApp component', () => { + const vueInstances = renderObservability([ + ...document.querySelectorAll('.js-render-observability'), + ]); - renderEmbeddedObservability(); + const wrapper = createWrapper(vueInstances[0]); - expect(findObservabilityIframes('dark')).toHaveLength(1); + expect(wrapper.findComponent(ObservabilityApp).props()).toMatchObject({ + observabilityIframeSrc: 'https://observe.gitlab.com/', + skeletonVariant: SKELETON_VARIANT_EMBED, + inlineEmbed: true, + height: INLINE_EMBED_DIMENSIONS.HEIGHT, + width: INLINE_EMBED_DIMENSIONS.WIDTH, + }); }); }); diff --git a/spec/frontend/blame/blame_redirect_spec.js b/spec/frontend/blame/blame_redirect_spec.js index beb10139b3a..326f60a5b13 100644 --- a/spec/frontend/blame/blame_redirect_spec.js +++ b/spec/frontend/blame/blame_redirect_spec.js @@ -1,8 +1,8 @@ import redirectToCorrectPage from '~/blame/blame_redirect'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Blame page redirect', () => { beforeEach(() => { diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js new file mode 100644 index 00000000000..e048ce3f70e --- /dev/null +++ b/spec/frontend/blame/streaming/index_spec.js @@ -0,0 +1,110 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { renderBlamePageStreams } from '~/blame/streaming'; +import { setHTMLFixture } from 'helpers/fixtures'; +import { renderHtmlStreams } from '~/streaming/render_html_streams'; +import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests'; +import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link'; +import { toPolyfillReadable } from '~/streaming/polyfills'; +import { createAlert } from '~/alert'; + +jest.mock('~/streaming/render_html_streams'); +jest.mock('~/streaming/rate_limit_stream_requests'); +jest.mock('~/streaming/handle_streamed_anchor_link'); +jest.mock('~/streaming/polyfills'); +jest.mock('~/sentry'); +jest.mock('~/alert'); + +global.fetch = jest.fn(); + +describe('renderBlamePageStreams', () => { + let stopAnchor; + const PAGES_URL = 'https://example.com/'; + const findStreamContainer = () => document.querySelector('#blame-stream-container'); + const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading'); + + const setupHtml = (totalExtraPages = 0) => { + setHTMLFixture(` + <div id="blob-content-holder" + data-total-extra-pages="${totalExtraPages}" + data-pages-url="${PAGES_URL}" + ></div> + <div id="blame-stream-container"></div> + <div id="blame-stream-loading"></div> + `); + }; + + handleStreamedAnchorLink.mockImplementation(() => stopAnchor); + rateLimitStreamRequests.mockImplementation(({ factory, total }) => { + return Array.from({ length: total }, (_, i) => { + return Promise.resolve(factory(i)); + }); + }); + toPolyfillReadable.mockImplementation((obj) => obj); + + beforeEach(() => { + stopAnchor = jest.fn(); + fetch.mockClear(); + }); + + it('does nothing for an empty page', async () => { + await renderBlamePageStreams(); + + expect(handleStreamedAnchorLink).not.toHaveBeenCalled(); + expect(renderHtmlStreams).not.toHaveBeenCalled(); + }); + + it('renders a single stream', async () => { + let res; + const stream = new Promise((resolve) => { + res = resolve; + }); + renderHtmlStreams.mockImplementationOnce(() => stream); + setupHtml(); + + renderBlamePageStreams(stream); + + expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1); + expect(stopAnchor).toHaveBeenCalledTimes(0); + expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer()); + expect(findStreamLoadingIndicator()).not.toBe(null); + + res(); + await waitForPromises(); + + expect(stopAnchor).toHaveBeenCalledTimes(1); + expect(findStreamLoadingIndicator()).toBe(null); + }); + + it('renders rest of the streams', async () => { + const stream = Promise.resolve(); + const stream2 = Promise.resolve({ body: null }); + fetch.mockImplementationOnce(() => stream2); + setupHtml(1); + + await renderBlamePageStreams(stream); + + expect(fetch.mock.calls[0][0].toString()).toBe(`${PAGES_URL}?page=3`); + expect(renderHtmlStreams).toHaveBeenCalledWith([stream, stream2], findStreamContainer()); + }); + + it('shows an error message when failed', async () => { + const stream = Promise.resolve(); + const error = new Error(); + renderHtmlStreams.mockImplementationOnce(() => Promise.reject(error)); + setupHtml(); + + try { + await renderBlamePageStreams(stream); + } catch (err) { + expect(err).toBe(error); + } + + expect(createAlert).toHaveBeenCalledWith({ + message: 'Blame could not be loaded as a single page.', + primaryButton: { + text: 'View blame as separate pages', + clickHandler: expect.any(Function), + }, + }); + }); +}); diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js index 0f5885c2acf..203fab94a5c 100644 --- a/spec/frontend/blob/components/blob_content_error_spec.js +++ b/spec/frontend/blob/components/blob_content_error_spec.js @@ -18,10 +18,6 @@ describe('Blob Content Error component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('collapsed and too large blobs', () => { it.each` error | reason | options diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js index f7b819b6e94..91af5f7bfed 100644 --- a/spec/frontend/blob/components/blob_content_spec.js +++ b/spec/frontend/blob/components/blob_content_spec.js @@ -29,10 +29,6 @@ describe('Blob Content component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { it('renders loader if `loading: true`', () => { createComponent({ loading: true }); diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js index c84b5896348..2b1bd1ac4ad 100644 --- a/spec/frontend/blob/components/blob_edit_header_spec.js +++ b/spec/frontend/blob/components/blob_edit_header_spec.js @@ -22,10 +22,6 @@ describe('Blob Header Editing', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { it('matches the snapshot', () => { expect(wrapper.element).toMatchSnapshot(); 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 0f015715dc2..e12021a48d2 100644 --- a/spec/frontend/blob/components/blob_header_default_actions_spec.js +++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js @@ -34,10 +34,6 @@ describe('Blob Header Default Actions', () => { buttons = wrapper.findAllComponents(GlButton); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('renders', () => { const findCopyButton = () => wrapper.findByTestId('copyContentsButton'); const findViewRawButton = () => wrapper.findByTestId('viewRawButton'); diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js index 8c32cba1ba4..be49146ff8a 100644 --- a/spec/frontend/blob/components/blob_header_filepath_spec.js +++ b/spec/frontend/blob/components/blob_header_filepath_spec.js @@ -21,10 +21,6 @@ describe('Blob Header Filepath', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findBadge = () => wrapper.findComponent(GlBadge); describe('rendering', () => { diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js index 46740958090..47e09bb38bc 100644 --- a/spec/frontend/blob/components/blob_header_spec.js +++ b/spec/frontend/blob/components/blob_header_spec.js @@ -1,9 +1,14 @@ import { shallowMount, mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import BlobHeader from '~/blob/components/blob_header.vue'; import DefaultActions from '~/blob/components/blob_header_default_actions.vue'; import BlobFilepath from '~/blob/components/blob_header_filepath.vue'; import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue'; +import { + RICH_BLOB_VIEWER_TITLE, + SIMPLE_BLOB_VIEWER, + SIMPLE_BLOB_VIEWER_TITLE, +} from '~/blob/components/constants'; import TableContents from '~/blob/components/table_contents.vue'; import { Blob } from './mock_data'; @@ -11,12 +16,26 @@ import { Blob } from './mock_data'; describe('Blob Header Default Actions', () => { let wrapper; - function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) { - const method = shouldMount ? mount : shallowMount; - const blobHash = 'foo-bar'; - wrapper = method.call(this, BlobHeader, { + const defaultProvide = { + blobHash: 'foo-bar', + }; + + const findDefaultActions = () => wrapper.findComponent(DefaultActions); + const findTableContents = () => wrapper.findComponent(TableContents); + const findViewSwitcher = () => wrapper.findComponent(ViewerSwitcher); + const findBlobFilePath = () => wrapper.findComponent(BlobFilepath); + const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE); + const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE); + + function createComponent({ + blobProps = {}, + options = {}, + propsData = {}, + mountFn = shallowMount, + } = {}) { + wrapper = mountFn(BlobHeader, { provide: { - blobHash, + ...defaultProvide, }, propsData: { blob: { ...Blob, ...blobProps }, @@ -26,143 +45,123 @@ describe('Blob Header Default Actions', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { - const findDefaultActions = () => wrapper.findComponent(DefaultActions); - - const slots = { - prepend: 'Foo Prepend', - actions: 'Actions Bar', - }; - it('matches the snapshot', () => { createComponent(); expect(wrapper.element).toMatchSnapshot(); }); - it('renders all components', () => { - createComponent(); - expect(wrapper.findComponent(TableContents).exists()).toBe(true); - expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true); - expect(findDefaultActions().exists()).toBe(true); - expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true); + describe('default render', () => { + it.each` + findComponent | componentName + ${findTableContents} | ${'TableContents'} + ${findViewSwitcher} | ${'ViewSwitcher'} + ${findDefaultActions} | ${'DefaultActions'} + ${findBlobFilePath} | ${'BlobFilePath'} + `('renders $componentName component by default', ({ findComponent }) => { + createComponent(); + + expect(findComponent().exists()).toBe(true); + }); }); it('does not render viewer switcher if the blob has only the simple viewer', () => { createComponent({ - richViewer: null, + blobProps: { + richViewer: null, + }, }); - expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false); + expect(findViewSwitcher().exists()).toBe(false); }); it('does not render viewer switcher if a corresponding prop is passed', () => { - createComponent( - {}, - {}, - { + createComponent({ + propsData: { hideViewerSwitcher: true, }, - ); - expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false); + }); + expect(findViewSwitcher().exists()).toBe(false); }); it('does not render default actions is corresponding prop is passed', () => { - createComponent( - {}, - {}, - { + createComponent({ + propsData: { hideDefaultActions: true, }, - ); - expect(wrapper.findComponent(DefaultActions).exists()).toBe(false); + }); + expect(findDefaultActions().exists()).toBe(false); }); - Object.keys(slots).forEach((slot) => { - it('renders the slots', () => { - const slotContent = slots[slot]; - createComponent( - {}, - { - scopedSlots: { - [slot]: `<span>${slotContent}</span>`, - }, + it.each` + slotContent | key + ${'Foo Prepend'} | ${'prepend'} + ${'Actions Bar'} | ${'actions'} + `('renders the slot $key', ({ key, slotContent }) => { + createComponent({ + options: { + scopedSlots: { + [key]: `<span>${slotContent}</span>`, }, - {}, - true, - ); - expect(wrapper.text()).toContain(slotContent); + }, + mountFn: mount, }); + expect(wrapper.text()).toContain(slotContent); }); it('passes information about render error down to default actions', () => { - createComponent( - {}, - {}, - { + createComponent({ + propsData: { hasRenderError: true, }, - ); + }); expect(findDefaultActions().props('hasRenderError')).toBe(true); }); it('passes the correct isBinary value to default actions when viewing a binary file', () => { - createComponent({}, {}, { isBinary: true }); + createComponent({ propsData: { isBinary: true } }); expect(findDefaultActions().props('isBinary')).toBe(true); }); }); describe('functionality', () => { - const newViewer = 'Foo Bar'; - const activeViewerType = 'Alpha Beta'; - const factory = (hideViewerSwitcher = false) => { - createComponent( - {}, - {}, - { - activeViewerType, + createComponent({ + propsData: { + activeViewerType: SIMPLE_BLOB_VIEWER, hideViewerSwitcher, }, - ); + mountFn: mountExtended, + }); }; - it('by default sets viewer data based on activeViewerType', () => { + it('shows the correctly selected view by default', () => { factory(); - expect(wrapper.vm.viewer).toBe(activeViewerType); + + expect(findViewSwitcher().exists()).toBe(true); + expect(findRichTextEditorBtn().props().selected).toBe(false); + expect(findSimpleTextEditorBtn().props().selected).toBe(true); }); - it('sets viewer to null if the viewer switcher should be hidden', () => { + it('Does not show the viewer switcher should be hidden', () => { factory(true); - expect(wrapper.vm.viewer).toBe(null); + + expect(findViewSwitcher().exists()).toBe(false); }); it('watches the changes in viewer data and emits event when the change is registered', async () => { factory(); - jest.spyOn(wrapper.vm, '$emit'); - wrapper.vm.viewer = newViewer; - await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer); - }); - - it('does not emit event if the switcher is not rendered', async () => { - factory(true); - - expect(wrapper.vm.showViewerSwitcher).toBe(false); - jest.spyOn(wrapper.vm, '$emit'); - wrapper.vm.viewer = newViewer; + await findRichTextEditorBtn().trigger('click'); - await nextTick(); - expect(wrapper.vm.$emit).not.toHaveBeenCalled(); + expect(wrapper.emitted('viewer-changed')).toBeDefined(); }); it('sets different icons depending on the blob file type', async () => { factory(); - expect(wrapper.vm.blobSwitcherDocIcon).toBe('document'); + + expect(findViewSwitcher().props('docIcon')).toBe('document'); + await wrapper.setProps({ blob: { ...Blob, @@ -172,7 +171,8 @@ describe('Blob Header Default Actions', () => { }, }, }); - expect(wrapper.vm.blobSwitcherDocIcon).toBe('table'); + + expect(findViewSwitcher().props('docIcon')).toBe('table'); }); }); }); 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 1eac0733646..2ef87f6664b 100644 --- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js +++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js @@ -18,14 +18,14 @@ describe('Blob Header Viewer Switcher', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); + const findSimpleViewerButton = () => wrapper.findComponent('[data-viewer="simple"]'); + const findRichViewerButton = () => wrapper.findComponent('[data-viewer="rich"]'); describe('intiialization', () => { it('is initialized with simple viewer as active', () => { createComponent(); - expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); + expect(findSimpleViewerButton().props('selected')).toBe(true); + expect(findRichViewerButton().props('selected')).toBe(false); }); }); @@ -52,45 +52,34 @@ describe('Blob Header Viewer Switcher', () => { }); describe('viewer changes', () => { - let buttons; - let simpleBtn; - let richBtn; + it('does not switch the viewer if the selected one is already active', async () => { + createComponent(); + expect(findSimpleViewerButton().props('selected')).toBe(true); - function factory(propsData = {}) { - createComponent(propsData); - buttons = wrapper.findAllComponents(GlButton); - simpleBtn = buttons.at(0); - richBtn = buttons.at(1); - - jest.spyOn(wrapper.vm, '$emit'); - } - - it('does not switch the viewer if the selected one is already active', () => { - factory(); - expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); - simpleBtn.vm.$emit('click'); - expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); - expect(wrapper.vm.$emit).not.toHaveBeenCalled(); + findSimpleViewerButton().vm.$emit('click'); + await nextTick(); + + expect(findSimpleViewerButton().props('selected')).toBe(true); + expect(wrapper.emitted('input')).toBe(undefined); }); it('emits an event when a Rich Viewer button is clicked', async () => { - factory(); - expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); - - richBtn.vm.$emit('click'); + createComponent(); + expect(findSimpleViewerButton().props('selected')).toBe(true); + findRichViewerButton().vm.$emit('click'); await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER); + + expect(wrapper.emitted('input')).toEqual([[RICH_BLOB_VIEWER]]); }); it('emits an event when a Simple Viewer button is clicked', async () => { - factory({ - value: RICH_BLOB_VIEWER, - }); - simpleBtn.vm.$emit('click'); + createComponent({ value: RICH_BLOB_VIEWER }); + findSimpleViewerButton().vm.$emit('click'); await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER); + + expect(wrapper.emitted('input')).toEqual([[SIMPLE_BLOB_VIEWER]]); }); }); }); diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js index 6af9cdcae7d..acfcef9704c 100644 --- a/spec/frontend/blob/components/table_contents_spec.js +++ b/spec/frontend/blob/components/table_contents_spec.js @@ -31,7 +31,6 @@ describe('Markdown table of contents component', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js index 9364f76da5e..8f105f04aa7 100644 --- a/spec/frontend/blob/csv/csv_viewer_spec.js +++ b/spec/frontend/blob/csv/csv_viewer_spec.js @@ -29,10 +29,6 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAlert = () => wrapper.findComponent(PapaParseAlert); - afterEach(() => { - wrapper.destroy(); - }); - it('should render loading spinner', () => { createComponent(); diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js index 2e7eadc912d..97b32a42afe 100644 --- a/spec/frontend/blob/notebook/notebook_viever_spec.js +++ b/spec/frontend/blob/notebook/notebook_viever_spec.js @@ -42,8 +42,6 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mock.restore(); }); diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js index 23227df6357..19d404f504b 100644 --- a/spec/frontend/blob/pdf/pdf_viewer_spec.js +++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js @@ -26,11 +26,6 @@ describe('PDF renderer', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('shows loading icon', () => { expect(findLoading().exists()).toBe(true); }); diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js index 81b38cfc278..84efa6041e4 100644 --- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js +++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js @@ -38,7 +38,6 @@ describe('PipelineTourSuccessModal', () => { }); afterEach(() => { - wrapper.destroy(); unmockTracking(); Cookies.remove(modalProps.commitCookie); }); 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 6b329dc078a..b30b0287a34 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 @@ -36,11 +36,6 @@ describe('Suggest gitlab-ci.yml Popover', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when no dismiss cookie is set', () => { beforeEach(() => { createWrapper(defaultTrackLabel); diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js index ed42322b0e6..89d507b4ec5 100644 --- a/spec/frontend/blob_edit/blob_bundle_spec.js +++ b/spec/frontend/blob_edit/blob_bundle_spec.js @@ -5,10 +5,10 @@ import waitForPromises from 'helpers/wait_for_promises'; import blobBundle from '~/blob_edit/blob_bundle'; import SourceEditor from '~/blob_edit/edit_blob'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; jest.mock('~/blob_edit/edit_blob'); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('BlobBundle', () => { it('does not load SourceEditor by default', () => { diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js index dda46e97b85..9ab20fc2cd7 100644 --- a/spec/frontend/blob_edit/edit_blob_spec.js +++ b/spec/frontend/blob_edit/edit_blob_spec.js @@ -20,9 +20,9 @@ jest.mock('~/editor/extensions/source_editor_toolbar_ext'); const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown'; const defaultExtensions = [ + { definition: ToolbarExtension }, { definition: SourceEditorExtension }, { definition: FileTemplateExtension }, - { definition: ToolbarExtension }, ]; const markdownExtensions = [ { definition: EditorMarkdownExtension }, diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 1e823e3321a..a612e863d46 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -84,7 +84,7 @@ describe('Board card component', () => { BoardCardMoveToPosition: true, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, provide: { rootPath: '/', @@ -110,8 +110,6 @@ describe('Board card component', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; store = null; jest.clearAllMocks(); }); @@ -314,10 +312,6 @@ describe('Board card component', () => { }); }); - afterEach(() => { - global.gon.default_avatar_url = null; - }); - it('displays defaults avatar if users avatar is null', () => { expect(wrapper.find('.board-card-assignee img').exists()).toBe(true); expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe( diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index d882ff071b7..43cf6ead1c1 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -92,6 +92,7 @@ export default function createComponent({ boardItems: [issue], canAdminList: true, boardId: 'gid://gitlab/Board/1', + filterParams: {}, ...componentProps, }, provide: { diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index fc8dbf8dc3a..9ec43c6e892 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -36,10 +36,6 @@ describe('Board list component', () => { useFakeRequestAnimationFrame(); - afterEach(() => { - wrapper.destroy(); - }); - describe('When Expanded', () => { beforeEach(() => { wrapper = createComponent({ issuesCount: 1 }); 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 0b3c6cb24c4..4fc9a6859a6 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 @@ -1,15 +1,13 @@ -import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import defaultState from '~/boards/stores/state'; import { mockLabelList } from '../mock_data'; Vue.use(Vuex); -describe('Board card layout', () => { +describe('BoardAddNewColumnForm', () => { let wrapper; const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { @@ -23,56 +21,30 @@ describe('Board card layout', () => { }); }; - const mountComponent = ({ - loading = false, - noneSelected = '', - searchLabel = '', - searchPlaceholder = '', - selectedId, - actions, - slots, - } = {}) => { - wrapper = extendedWrapper( - shallowMount(BoardAddNewColumnForm, { - propsData: { - loading, - noneSelected, - searchLabel, - searchPlaceholder, - selectedId, - }, - slots, - store: createStore({ - actions: { - setAddColumnFormVisibility: jest.fn(), - ...actions, - }, - }), - stubs: { - GlDropdown, + const mountComponent = ({ searchLabel = '', selectedIdValid = true, actions, slots } = {}) => { + wrapper = shallowMountExtended(BoardAddNewColumnForm, { + propsData: { + searchLabel, + selectedIdValid, + }, + slots, + store: createStore({ + actions: { + setAddColumnFormVisibility: jest.fn(), + ...actions, }, }), - ); + }); }; - afterEach(() => { - wrapper.destroy(); - }); - const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); - 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); - it('shows form title & search input', () => { + it('shows form title', () => { mountComponent(); - findDropdown().vm.$emit('show'); - expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); - expect(findSearchInput().exists()).toBe(true); }); it('clicking cancel hides the form', () => { @@ -88,61 +60,6 @@ describe('Board card layout', () => { expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); }); - describe('items', () => { - const mountWithItems = (loading) => - mountComponent({ - loading, - slots: { - items: '<div class="item-slot">Some kind of list</div>', - }, - }); - - it('hides items slot and shows skeleton while loading', () => { - mountWithItems(true); - - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); - expect(wrapper.find('.item-slot').exists()).toBe(false); - }); - - it('shows items slot and hides skeleton while not loading', () => { - mountWithItems(false); - - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); - expect(wrapper.find('.item-slot').exists()).toBe(true); - }); - }); - - describe('search box', () => { - it('sets label and placeholder text from props', () => { - const props = { - searchLabel: 'Some items', - searchPlaceholder: 'Search for an item', - }; - - mountComponent(props); - - 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(); - - const searchText = 'some text'; - - findSearchInput().vm.$emit('input', searchText); - - expect(wrapper.emitted('filter-items')).toEqual([[searchText]]); - }); - }); - describe('Add list button', () => { it('is enabled by default', () => { mountComponent(); @@ -159,16 +76,5 @@ 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_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js index a3b2988ce75..a09c3aaa55e 100644 --- a/spec/frontend/boards/components/board_add_new_column_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -1,8 +1,7 @@ -import { GlFormRadioGroup } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import defaultState from '~/boards/stores/state'; @@ -13,8 +12,9 @@ Vue.use(Vuex); describe('Board card layout', () => { let wrapper; + const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); const selectLabel = (id) => { - wrapper.findComponent(GlFormRadioGroup).vm.$emit('change', id); + findDropdown().vm.$emit('select', id); }; const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { @@ -34,33 +34,34 @@ describe('Board card layout', () => { getListByLabelId = jest.fn(), actions = {}, } = {}) => { - wrapper = extendedWrapper( - shallowMount(BoardAddNewColumn, { - data() { - return { - selectedId, - }; + wrapper = shallowMountExtended(BoardAddNewColumn, { + data() { + return { + selectedId, + }; + }, + store: createStore({ + actions: { + fetchLabels: jest.fn(), + setAddColumnFormVisibility: jest.fn(), + ...actions, }, - store: createStore({ - actions: { - fetchLabels: jest.fn(), - setAddColumnFormVisibility: jest.fn(), - ...actions, - }, - getters: { - getListByLabelId: () => getListByLabelId, - }, - state: { - labels, - labelsLoading: false, - }, - }), - provide: { - scopedLabelsAvailable: true, - isEpicBoard: false, + getters: { + getListByLabelId: () => getListByLabelId, + }, + state: { + labels, + labelsLoading: false, }, }), - ); + provide: { + scopedLabelsAvailable: true, + isEpicBoard: false, + }, + stubs: { + GlCollapsibleListbox, + }, + }); // trigger change event if (selectedId) { @@ -68,10 +69,6 @@ describe('Board card layout', () => { } }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Add list button', () => { it('calls addList', async () => { const getListByLabelId = jest.fn().mockReturnValue(null); 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 354eb7bff16..d8b93e1f3b6 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 @@ -17,7 +17,7 @@ describe('BoardAddNewColumnTrigger', () => { const mountComponent = () => { wrapper = mountExtended(BoardAddNewColumnTrigger, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, store: createStore(), }); @@ -27,10 +27,6 @@ describe('BoardAddNewColumnTrigger', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when button is active', () => { it('does not show the tooltip', () => { const tooltip = findTooltipText(); diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js index 12318fb5d16..148e696b57b 100644 --- a/spec/frontend/boards/components/board_app_spec.js +++ b/spec/frontend/boards/components/board_app_spec.js @@ -28,13 +28,12 @@ describe('BoardApp', () => { store, provide: { initialBoardId: 'gid://gitlab/Board/1', + initialFilterParams: {}, }, }); }; afterEach(() => { - wrapper.destroy(); - wrapper = null; store = null; }); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 84e6318d98e..46116bed4cf 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -82,8 +82,6 @@ describe('Board card', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; store = null; }); diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js index c0bb51620f2..011665eee68 100644 --- a/spec/frontend/boards/components/board_column_spec.js +++ b/spec/frontend/boards/components/board_column_spec.js @@ -10,11 +10,6 @@ describe('Board Column Component', () => { let wrapper; let store; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const initStore = () => { store = createStore(); }; @@ -36,6 +31,7 @@ describe('Board Column Component', () => { propsData: { list: listMock, boardId: 'gid://gitlab/Board/1', + filters: {}, }, provide: { isApolloBoard: false, diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js index 6f0971a9458..d2948daf121 100644 --- a/spec/frontend/boards/components/board_configuration_options_spec.js +++ b/spec/frontend/boards/components/board_configuration_options_spec.js @@ -16,10 +16,6 @@ describe('BoardConfigurationOptions', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const backlogListCheckbox = () => wrapper.find('[data-testid="backlog-list-checkbox"]'); const closedListCheckbox = () => wrapper.find('[data-testid="closed-list-checkbox"]'); diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index 955267a415c..90376a4a553 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -89,10 +89,6 @@ describe('BoardContentSidebar', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('confirms we render GlDrawer', () => { expect(wrapper.findComponent(GlDrawer).exists()).toBe(true); }); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 97596c86198..33351bf8efd 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -4,6 +4,8 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import Draggable from 'vuedraggable'; import Vuex from 'vuex'; + +import eventHub from '~/boards/eventhub'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; @@ -24,7 +26,6 @@ const actions = { describe('BoardContent', () => { let wrapper; let fakeApollo; - window.gon = {}; const defaultState = { isShowingEpicsSwimlanes: false, @@ -61,6 +62,8 @@ describe('BoardContent', () => { apolloProvider: fakeApollo, propsData: { boardId: 'gid://gitlab/Board/1', + filterParams: {}, + isSwimlanesOn: false, ...props, }, provide: { @@ -102,7 +105,6 @@ describe('BoardContent', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -203,5 +205,14 @@ describe('BoardContent', () => { it('renders BoardContentSidebar', () => { expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true); }); + + it('refetches lists when updateBoard event is received', async () => { + jest.spyOn(eventHub, '$on').mockImplementation(() => {}); + + createComponent({ isApolloBoard: true }); + await waitForPromises(); + + expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists); + }); }); }); diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index 4c0cc36889c..d8bc7f95f18 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -55,10 +55,10 @@ describe('BoardFilteredSearch', () => { }, ]; - const createComponent = ({ initialFilterParams = {}, props = {} } = {}) => { + const createComponent = ({ initialFilterParams = {}, props = {}, provide = {} } = {}) => { store = createStore(); wrapper = shallowMount(BoardFilteredSearch, { - provide: { initialFilterParams, fullPath: '' }, + provide: { initialFilterParams, fullPath: '', isApolloBoard: false, ...provide }, store, propsData: { ...props, @@ -69,10 +69,6 @@ describe('BoardFilteredSearch', () => { const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); @@ -191,4 +187,24 @@ describe('BoardFilteredSearch', () => { ]); }); }); + + describe('when Apollo boards FF is on', () => { + beforeEach(() => { + createComponent({ provide: { isApolloBoard: true } }); + }); + + it('emits setFilters and updates URL when onFilter is emitted', () => { + jest.spyOn(urlUtility, 'updateHistory'); + + findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]); + + expect(urlUtility.updateHistory).toHaveBeenCalledWith({ + title: '', + replace: true, + url: 'http://test.host/', + }); + + expect(wrapper.emitted('setFilters')).toHaveLength(1); + }); + }); }); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index f8154145d43..62db59f8f57 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -10,12 +10,14 @@ import { formType } from '~/boards/constants'; import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'; import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql'; import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql'; +import eventHub from '~/boards/eventhub'; import { visitUrl } from '~/lib/utils/url_utility'; jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), visitUrl: jest.fn().mockName('visitUrlMock'), })); +jest.mock('~/boards/eventhub'); Vue.use(Vuex); @@ -59,18 +61,14 @@ describe('BoardForm', () => { }, }); - const createComponent = (props, data) => { + const createComponent = (props, provide) => { wrapper = shallowMountExtended(BoardForm, { propsData: { ...defaultProps, ...props }, - data() { - return { - ...data, - }; - }, provide: { boardBaseUrl: 'root', isGroupBoard: true, isProjectBoard: false, + ...provide, }, mocks: { $apollo: { @@ -83,8 +81,6 @@ describe('BoardForm', () => { }; afterEach(() => { - wrapper.destroy(); - wrapper = null; mutate = null; }); @@ -140,7 +136,7 @@ describe('BoardForm', () => { it('passes correct primary action text and variant', () => { expect(findModalActionPrimary().text).toBe('Create board'); - expect(findModalActionPrimary().attributes[0].variant).toBe('confirm'); + expect(findModalActionPrimary().attributes.variant).toBe('confirm'); }); it('does not render delete confirmation message', () => { @@ -209,6 +205,30 @@ describe('BoardForm', () => { expect(setBoardMock).not.toHaveBeenCalled(); expect(setErrorMock).toHaveBeenCalled(); }); + + describe('when Apollo boards FF is on', () => { + it('calls a correct GraphQL mutation and emits addBoard event when creating a board', async () => { + createComponent( + { canAdminBoard: true, currentPage: formType.new }, + { isApolloBoard: true }, + ); + fillForm(); + + await waitForPromises(); + + expect(mutate).toHaveBeenCalledWith({ + mutation: createBoardMutation, + variables: { + input: expect.objectContaining({ + name: 'test', + }), + }, + }); + + await waitForPromises(); + expect(wrapper.emitted('addBoard')).toHaveLength(1); + }); + }); }); }); @@ -228,7 +248,7 @@ describe('BoardForm', () => { it('passes correct primary action text and variant', () => { expect(findModalActionPrimary().text).toBe('Save changes'); - expect(findModalActionPrimary().attributes[0].variant).toBe('confirm'); + expect(findModalActionPrimary().attributes.variant).toBe('confirm'); }); it('does not render delete confirmation message', () => { @@ -308,13 +328,48 @@ describe('BoardForm', () => { expect(setBoardMock).not.toHaveBeenCalled(); expect(setErrorMock).toHaveBeenCalled(); }); + + describe('when Apollo boards FF is on', () => { + it('calls a correct GraphQL mutation and emits updateBoard event when updating a board', async () => { + mutate = jest.fn().mockResolvedValue({ + data: { + updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } }, + }, + }); + setWindowLocation('https://test/boards/1'); + + createComponent( + { canAdminBoard: true, currentPage: formType.edit }, + { isApolloBoard: true }, + ); + findInput().trigger('keyup.enter', { metaKey: true }); + + await waitForPromises(); + + expect(mutate).toHaveBeenCalledWith({ + mutation: updateBoardMutation, + variables: { + input: expect.objectContaining({ + id: currentBoard.id, + }), + }, + }); + + await waitForPromises(); + expect(eventHub.$emit).toHaveBeenCalledTimes(1); + expect(eventHub.$emit).toHaveBeenCalledWith('updateBoard', { + id: 'gid://gitlab/Board/321', + webPath: 'test-path', + }); + }); + }); }); describe('when deleting a board', () => { it('passes correct primary action text and variant', () => { createComponent({ canAdminBoard: true, currentPage: formType.delete }); expect(findModalActionPrimary().text).toBe('Delete'); - expect(findModalActionPrimary().attributes[0].variant).toBe('danger'); + expect(findModalActionPrimary().attributes.variant).toBe('danger'); }); it('renders delete confirmation message', () => { diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 9e65e900440..466321cf1cc 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -1,10 +1,9 @@ -import { shallowMount } from '@vue/test-utils'; +import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; - +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data'; import BoardListHeader from '~/boards/components/board_list_header.vue'; import { ListType } from '~/boards/constants'; @@ -22,8 +21,6 @@ describe('Board List Header Component', () => { const toggleListCollapsedSpy = jest.fn(); afterEach(() => { - wrapper.destroy(); - wrapper = null; fakeApollo = null; localStorage.clear(); @@ -64,31 +61,34 @@ describe('Board List Header Component', () => { fakeApollo = createMockApollo([[listQuery, listQueryHandler]]); - wrapper = extendedWrapper( - shallowMount(BoardListHeader, { - apolloProvider: fakeApollo, - store, - propsData: { - list: listMock, - }, - provide: { - boardId, - weightFeatureAvailable: false, - currentUserId, - isEpicBoard: false, - disabled: false, - ...injectedProps, - }, - }), - ); + wrapper = shallowMountExtended(BoardListHeader, { + apolloProvider: fakeApollo, + store, + propsData: { + list: listMock, + filterParams: {}, + }, + provide: { + boardId, + weightFeatureAvailable: false, + currentUserId, + isEpicBoard: false, + disabled: false, + ...injectedProps, + }, + stubs: { + GlDisclosureDropdown, + GlDisclosureDropdownItem, + }, + }); }; + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const isCollapsed = () => wrapper.vm.list.collapsed; - - const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' }); const findTitle = () => wrapper.find('.board-title'); const findCaret = () => wrapper.findByTestId('board-title-caret'); - const findSettingsButton = () => wrapper.findComponent({ ref: 'settingsBtn' }); + const findNewIssueButton = () => wrapper.findByTestId('newIssueBtn'); + const findSettingsButton = () => wrapper.findByTestId('settingsBtn'); describe('Add issue button', () => { const hasNoAddButton = [ListType.closed]; @@ -100,59 +100,49 @@ describe('Board List Header Component', () => { ListType.assignee, ]; - it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => { + it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => { createComponent({ listType }); - expect(findAddIssueButton().exists()).toBe(false); + expect(findDropdown().exists()).toBe(false); }); it.each(hasAddButton)('does render when List Type is `%s`', (listType) => { createComponent({ listType }); - expect(findAddIssueButton().exists()).toBe(true); + expect(findDropdown().exists()).toBe(true); + expect(findNewIssueButton().exists()).toBe(true); }); - it('has a test for each list type', () => { - createComponent(); - - Object.values(ListType).forEach((value) => { - expect([...hasAddButton, ...hasNoAddButton]).toContain(value); - }); - }); - - it('does not render when logged out', () => { + it('does not render dropdown when logged out', () => { createComponent({ currentUserId: null, }); - expect(findAddIssueButton().exists()).toBe(false); + expect(findDropdown().exists()).toBe(false); }); }); describe('Settings Button', () => { - describe('with disabled=true', () => { - const hasSettings = [ - ListType.assignee, - ListType.milestone, - ListType.iteration, - ListType.label, - ]; - const hasNoSettings = [ListType.backlog, ListType.closed]; - - it.each(hasSettings)('does render for List Type `%s` when disabled=true', (listType) => { - createComponent({ listType, injectedProps: { disabled: true } }); - - expect(findSettingsButton().exists()).toBe(true); - }); + const hasSettings = [ListType.assignee, ListType.milestone, ListType.iteration, ListType.label]; - it.each(hasNoSettings)( - 'does not render for List Type `%s` when disabled=true', - (listType) => { - createComponent({ listType }); + it.each(hasSettings)('does render for List Type `%s`', (listType) => { + createComponent({ listType }); - expect(findSettingsButton().exists()).toBe(false); - }, - ); + expect(findDropdown().exists()).toBe(true); + expect(findSettingsButton().exists()).toBe(true); + }); + + it('does not render dropdown when ListType `closed`', () => { + createComponent({ listType: ListType.closed }); + + expect(findDropdown().exists()).toBe(false); + }); + + it('renders dropdown but not the Settings button when ListType `backlog`', () => { + createComponent({ listType: ListType.backlog }); + + expect(findDropdown().exists()).toBe(true); + expect(findSettingsButton().exists()).toBe(false); }); }); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index c3e69ba0e40..651d1daee52 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -51,10 +51,6 @@ describe('Issue boards new issue form', () => { await nextTick(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders board-new-item component', () => { const boardNewItem = findBoardNewItem(); expect(boardNewItem.exists()).toBe(true); diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js index f4e9901aad2..f11eb2baca7 100644 --- a/spec/frontend/boards/components/board_new_item_spec.js +++ b/spec/frontend/boards/components/board_new_item_spec.js @@ -35,10 +35,6 @@ describe('BoardNewItem', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { describe('when the user provides a valid input', () => { it('finds an enabled create button', async () => { diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 7d602042685..d0928485caf 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -48,7 +48,7 @@ describe('BoardSettingsSidebar', () => { isIssueBoard: true, }, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, stubs: { GlDrawer: stubComponent(GlDrawer, { @@ -65,8 +65,6 @@ describe('BoardSettingsSidebar', () => { afterEach(() => { jest.restoreAllMocks(); - wrapper.destroy(); - wrapper = null; }); it('finds a MountingPortal component', () => { diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js index 8258d9fe7f4..d97a1dbff47 100644 --- a/spec/frontend/boards/components/board_top_bar_spec.js +++ b/spec/frontend/boards/components/board_top_bar_spec.js @@ -11,7 +11,7 @@ import ConfigToggle from '~/boards/components/config_toggle.vue'; import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue'; import NewBoardButton from '~/boards/components/new_board_button.vue'; import ToggleFocus from '~/boards/components/toggle_focus.vue'; -import { BoardType } from '~/boards/constants'; +import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import groupBoardQuery from '~/boards/graphql/group_board.query.graphql'; import projectBoardQuery from '~/boards/graphql/project_board.query.graphql'; @@ -43,8 +43,9 @@ describe('BoardTopBar', () => { wrapper = shallowMount(BoardTopBar, { store, apolloProvider: mockApollo, - props: { + propsData: { boardId: 'gid://gitlab/Board/1', + isSwimlanesOn: false, }, provide: { swimlanesFeatureAvailable: false, @@ -64,7 +65,6 @@ describe('BoardTopBar', () => { }; afterEach(() => { - wrapper.destroy(); mockApollo = null; }); @@ -96,6 +96,11 @@ describe('BoardTopBar', () => { it('does not render BoardAddNewColumnTrigger component', () => { expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(false); }); + + it('emits setFilters when setFilters is emitted by filtered search', () => { + wrapper.findComponent(IssueBoardFilteredSearch).vm.$emit('setFilters'); + expect(wrapper.emitted('setFilters')).toHaveLength(1); + }); }); describe('when user can admin list', () => { @@ -111,14 +116,14 @@ describe('BoardTopBar', () => { describe('Apollo boards', () => { it.each` boardType | queryHandler | notCalledHandler - ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess} - ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess} + ${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess} + ${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess} `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => { createComponent({ provide: { boardType, - isProjectBoard: boardType === BoardType.project, - isGroupBoard: boardType === BoardType.group, + isProjectBoard: boardType === WORKSPACE_PROJECT, + isGroupBoard: boardType === WORKSPACE_GROUP, isApolloBoard: true, }, }); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 28f51e0ecbf..aa146eb4609 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -5,11 +5,11 @@ import Vuex from 'vuex'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; import BoardsSelector from '~/boards/components/boards_selector.vue'; -import { BoardType } from '~/boards/constants'; import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql'; import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql'; import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql'; import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql'; +import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { @@ -116,7 +116,6 @@ describe('BoardsSelector', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -228,13 +227,13 @@ describe('BoardsSelector', () => { describe('fetching all boards', () => { it.each` boardType | queryHandler | notCalledHandler - ${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess} - ${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess} + ${WORKSPACE_GROUP} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess} + ${WORKSPACE_PROJECT} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess} `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => { createStore(); createComponent({ - isGroupBoard: boardType === BoardType.group, - isProjectBoard: boardType === BoardType.project, + isGroupBoard: boardType === WORKSPACE_GROUP, + isProjectBoard: boardType === WORKSPACE_PROJECT, }); await nextTick(); diff --git a/spec/frontend/boards/components/config_toggle_spec.js b/spec/frontend/boards/components/config_toggle_spec.js index 47d4692453d..5330721451e 100644 --- a/spec/frontend/boards/components/config_toggle_spec.js +++ b/spec/frontend/boards/components/config_toggle_spec.js @@ -23,10 +23,6 @@ describe('ConfigToggle', () => { const findButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a button with label `View scope` when `canAdminList` is `false`', () => { wrapper = createComponent({ canAdminList: false }); expect(findButton().text()).toBe('View scope'); diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js index 57a30ddc512..5b5b68d5dbe 100644 --- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js +++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js @@ -2,10 +2,10 @@ import { orderBy } from 'lodash'; import { shallowMount } from '@vue/test-utils'; import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue'; import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue'; -import issueBoardFilters from '~/boards/issue_board_filters'; +import issueBoardFilters from 'ee_else_ce/boards/issue_board_filters'; import { mockTokens } from '../mock_data'; -jest.mock('~/boards/issue_board_filters'); +jest.mock('ee_else_ce/boards/issue_board_filters'); describe('IssueBoardFilter', () => { let wrapper; @@ -14,6 +14,9 @@ describe('IssueBoardFilter', () => { const createComponent = ({ isSignedIn = false } = {}) => { wrapper = shallowMount(IssueBoardFilteredSpec, { + propsData: { + boardId: 'gid://gitlab/Board/1', + }, provide: { isSignedIn, releasesFetchPath: '/releases', @@ -35,10 +38,6 @@ describe('IssueBoardFilter', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); @@ -48,6 +47,11 @@ describe('IssueBoardFilter', () => { expect(findBoardsFilteredSearch().exists()).toBe(true); }); + it('emits setFilters when setFilters is emitted', () => { + findBoardsFilteredSearch().vm.$emit('setFilters'); + expect(wrapper.emitted('setFilters')).toHaveLength(1); + }); + it.each` isSignedIn ${true} diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js index 45fa10bf03a..dee8febfe4d 100644 --- a/spec/frontend/boards/components/issue_due_date_spec.js +++ b/spec/frontend/boards/components/issue_due_date_spec.js @@ -20,10 +20,6 @@ describe('Issue Due Date component', () => { date = new Date(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render "Today" if the due date is today', () => { wrapper = createComponent(); diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js index 948a7a20f7f..42507ef560b 100644 --- a/spec/frontend/boards/components/issue_time_estimate_spec.js +++ b/spec/frontend/boards/components/issue_time_estimate_spec.js @@ -7,10 +7,6 @@ describe('Issue Time Estimate component', () => { const findIssueTimeEstimate = () => wrapper.find('[data-testid="issue-time-estimate"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('when limitToHours is false', () => { beforeEach(() => { wrapper = shallowMount(IssueTimeEstimate, { diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js index 0c0c7f66933..f2cc8eb1167 100644 --- a/spec/frontend/boards/components/item_count_spec.js +++ b/spec/frontend/boards/components/item_count_spec.js @@ -41,10 +41,6 @@ describe('IssueCount', () => { createComponent({ maxIssueCount, itemsSize }); }); - afterEach(() => { - vm.destroy(); - }); - it('contains issueSize in the template', () => { expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); @@ -66,10 +62,6 @@ describe('IssueCount', () => { createComponent({ maxIssueCount, itemsSize }); }); - afterEach(() => { - vm.destroy(); - }); - it('contains issueSize in the template', () => { expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); 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 5e2222ac3d7..6dbeac3864f 100644 --- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js +++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js @@ -21,11 +21,6 @@ describe('boards sidebar remove issue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('template', () => { it('renders title', () => { const title = 'Sidebar item title'; 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 e2e4baefad0..b01ee01120e 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 @@ -37,11 +37,6 @@ describe('BoardSidebarTimeTracker', () => { store.state.activeId = '1'; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` timeTrackingLimitToHours | canUpdate ${true} | ${false} 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 bc66a0515aa..a20884baf3b 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js @@ -27,9 +27,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { afterEach(() => { localStorage.clear(); - wrapper.destroy(); store = null; - wrapper = null; }); const createWrapper = (item = TEST_ISSUE_A) => { diff --git a/spec/frontend/boards/components/toggle_focus_spec.js b/spec/frontend/boards/components/toggle_focus_spec.js index 3cbaac91f8d..cad287954d7 100644 --- a/spec/frontend/boards/components/toggle_focus_spec.js +++ b/spec/frontend/boards/components/toggle_focus_spec.js @@ -10,7 +10,7 @@ describe('ToggleFocus', () => { const createComponent = () => { wrapper = shallowMountExtended(ToggleFocus, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, attachTo: document.body, }); @@ -18,10 +18,6 @@ describe('ToggleFocus', () => { const findButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a button with `maximize` icon', () => { createComponent(); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 1d011eacf1c..e5167120542 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -477,6 +477,9 @@ export const mockList = { loading: false, issuesCount: 1, maxIssueCount: 0, + metadata: { + epicsCount: 1, + }, __typename: 'BoardList', }; @@ -915,6 +918,7 @@ export const epicBoardListQueryResponse = (totalWeight = 5) => ({ __typename: 'EpicList', id: 'gid://gitlab/Boards::EpicList/3', metadata: { + epicsCount: 1, totalWeight, }, }, diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index 4324e7068e0..74ce4b6b786 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -71,11 +71,6 @@ describe('ProjectSelect component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('displays a header title', () => { createWrapper(); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index ab959abaa99..f430062bb73 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -2,13 +2,7 @@ import * as Sentry from '@sentry/browser'; import { cloneDeep } from 'lodash'; import Vue from 'vue'; import Vuex from 'vuex'; -import { - inactiveId, - ISSUABLE, - ListType, - BoardType, - DraggableItemTypes, -} from 'ee_else_ce/boards/constants'; +import { inactiveId, ISSUABLE, ListType, DraggableItemTypes } from 'ee_else_ce/boards/constants'; import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; import testAction from 'helpers/vuex_action_helper'; import { @@ -26,7 +20,7 @@ import actions from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; import mutations from '~/boards/stores/mutations'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { TYPE_ISSUE } from '~/issues/constants'; +import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql'; import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql'; @@ -49,7 +43,7 @@ import { mockMilestones, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); // We need this helper to make sure projectPath is including // subgroups when the movIssue action is called. @@ -300,8 +294,8 @@ describe('fetchLists', () => { it.each` issuableType | boardType | fullBoardId | isGroup | isProject - ${TYPE_ISSUE} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false} - ${TYPE_ISSUE} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true} + ${TYPE_ISSUE} | ${WORKSPACE_GROUP} | ${'gid://gitlab/Board/1'} | ${true} | ${false} + ${TYPE_ISSUE} | ${WORKSPACE_PROJECT} | ${'gid://gitlab/Board/1'} | ${false} | ${true} `( 'calls $issuableType query with correct variables', async ({ issuableType, boardType, fullBoardId, isGroup, isProject }) => { @@ -336,7 +330,7 @@ describe('fetchLists', () => { describe('fetchMilestones', () => { const queryResponse = { data: { - project: { + workspace: { milestones: { nodes: mockMilestones, }, @@ -346,7 +340,7 @@ describe('fetchMilestones', () => { const queryErrors = { data: { - project: { + workspace: { errors: ['You cannot view these milestones'], milestones: {}, }, diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index c86a256bd96..944a7493504 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -31,10 +31,6 @@ describe('Boards - Getters', () => { }); describe('isSwimlanesOn', () => { - afterEach(() => { - window.gon = { features: {} }; - }); - it('returns false', () => { expect(getters.isSwimlanesOn()).toBe(false); }); @@ -171,10 +167,6 @@ describe('Boards - Getters', () => { }); describe('isEpicBoard', () => { - afterEach(() => { - window.gon = { features: {} }; - }); - it('returns false', () => { expect(getters.isEpicBoard()).toBe(false); }); diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js index b029f34c3d7..5b2ec443c59 100644 --- a/spec/frontend/branches/components/delete_branch_button_spec.js +++ b/spec/frontend/branches/components/delete_branch_button_spec.js @@ -25,10 +25,6 @@ describe('Delete branch button', () => { eventHubSpy = jest.spyOn(eventHub, '$emit'); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the button with default tooltip, style, and icon', () => { createComponent(); diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js index c977868ca93..dd5b7fca564 100644 --- a/spec/frontend/branches/components/delete_branch_modal_spec.js +++ b/spec/frontend/branches/components/delete_branch_modal_spec.js @@ -52,10 +52,6 @@ describe('Delete branch modal', () => { const expectedUnmergedWarning = "This branch hasn't been merged into default. To avoid data loss, consider merging this branch before deleting it."; - afterEach(() => { - wrapper.destroy(); - }); - describe('Deleting a regular branch', () => { const expectedTitle = 'Delete branch. Are you ABSOLUTELY SURE?'; const expectedWarning = "You're about to permanently delete the branch test_modal."; diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js index 4f1e772f4a4..75a669c78f2 100644 --- a/spec/frontend/branches/components/delete_merged_branches_spec.js +++ b/spec/frontend/branches/components/delete_merged_branches_spec.js @@ -27,7 +27,7 @@ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => { ...propsDataMock, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs, }); @@ -78,10 +78,6 @@ describe('Delete merged branches component', () => { createComponent(shallowMountExtended, stubsData); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders correct modal title and text', () => { const modalText = findModal().text(); expect(findModal().props('title')).toBe(i18n.modalTitle); diff --git a/spec/frontend/branches/components/divergence_graph_spec.js b/spec/frontend/branches/components/divergence_graph_spec.js index 9429a6e982c..66193c2ebf0 100644 --- a/spec/frontend/branches/components/divergence_graph_spec.js +++ b/spec/frontend/branches/components/divergence_graph_spec.js @@ -9,10 +9,6 @@ function factory(propsData = {}) { } describe('Branch divergence graph component', () => { - afterEach(() => { - vm.destroy(); - }); - it('renders ahead and behind count', () => { factory({ defaultBranch: 'main', diff --git a/spec/frontend/branches/components/graph_bar_spec.js b/spec/frontend/branches/components/graph_bar_spec.js index 61c051b49c6..585b376081b 100644 --- a/spec/frontend/branches/components/graph_bar_spec.js +++ b/spec/frontend/branches/components/graph_bar_spec.js @@ -8,10 +8,6 @@ function factory(propsData = {}) { } describe('Branch divergence graph bar component', () => { - afterEach(() => { - vm.destroy(); - }); - it.each` position | positionClass ${'left'} | ${'position-right-0'} diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js index 20e69b5a834..6d6d8043797 100644 --- a/spec/frontend/captcha/captcha_modal_spec.js +++ b/spec/frontend/captcha/captcha_modal_spec.js @@ -1,6 +1,5 @@ import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; import CaptchaModal from '~/captcha/captcha_modal.vue'; import { initRecaptchaScript } from '~/captcha/init_recaptcha_script'; @@ -9,10 +8,11 @@ jest.mock('~/captcha/init_recaptcha_script'); describe('Captcha Modal', () => { let wrapper; - let modal; let grecaptcha; const captchaSiteKey = 'abc123'; + const showSpy = jest.fn(); + const hideSpy = jest.fn(); function createComponent({ props = {} } = {}) { wrapper = shallowMount(CaptchaModal, { @@ -21,11 +21,18 @@ describe('Captcha Modal', () => { ...props, }, stubs: { - GlModal: stubComponent(GlModal), + GlModal: stubComponent(GlModal, { + methods: { + show: showSpy, + hide: hideSpy, + }, + }), }, }); } + const findGlModal = () => wrapper.findComponent(GlModal); + beforeEach(() => { grecaptcha = { render: jest.fn(), @@ -34,38 +41,17 @@ describe('Captcha Modal', () => { initRecaptchaScript.mockResolvedValue(grecaptcha); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const findGlModal = () => { - const glModal = wrapper.findComponent(GlModal); - - jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown')); - jest - .spyOn(glModal.vm, 'hide') - .mockImplementation(() => glModal.vm.$emit('hide', { trigger: '' })); - - return glModal; - }; - - const showModal = () => { - wrapper.setProps({ needsCaptchaResponse: true }); - }; - - beforeEach(() => { - createComponent(); - modal = findGlModal(); - }); - describe('rendering', () => { + beforeEach(() => { + createComponent(); + }); + it('renders', () => { - expect(modal.exists()).toBe(true); + expect(findGlModal().exists()).toBe(true); }); it('assigns the modal a unique ID', () => { - const firstInstanceModalId = modal.props('modalId'); + const firstInstanceModalId = findGlModal().props('modalId'); createComponent(); const secondInstanceModalId = findGlModal().props('modalId'); expect(firstInstanceModalId).not.toEqual(secondInstanceModalId); @@ -76,13 +62,12 @@ describe('Captcha Modal', () => { describe('when modal is shown', () => { describe('when initRecaptchaScript promise resolves successfully', () => { beforeEach(async () => { - showModal(); - - await nextTick(); + createComponent({ props: { needsCaptchaResponse: true } }); + findGlModal().vm.$emit('shown'); }); it('shows modal', async () => { - expect(findGlModal().vm.show).toHaveBeenCalled(); + expect(showSpy).toHaveBeenCalled(); }); it('renders window.grecaptcha', () => { @@ -108,7 +93,7 @@ describe('Captcha Modal', () => { it('hides modal with null trigger', async () => { // Assert that hide is called with zero args, so that we don't trigger the logic // for hiding the modal via cancel, esc, headerclose, etc, without a captcha response - expect(modal.vm.hide).toHaveBeenCalledWith(); + expect(hideSpy).toHaveBeenCalledWith(); }); }); @@ -127,7 +112,7 @@ describe('Captcha Modal', () => { const bvModalEvent = { trigger, }; - modal.vm.$emit('hide', bvModalEvent); + findGlModal().vm.$emit('hide', bvModalEvent); }); it(`emits receivedCaptchaResponse with ${JSON.stringify(expected)}`, () => { @@ -141,21 +126,24 @@ describe('Captcha Modal', () => { const fakeError = {}; beforeEach(() => { - initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError)); + createComponent({ + props: { needsCaptchaResponse: true }, + }); + initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError)); jest.spyOn(console, 'error').mockImplementation(); - showModal(); + findGlModal().vm.$emit('shown'); }); it('emits receivedCaptchaResponse exactly once with null', () => { expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[null]]); }); - it('hides modal with null trigger', async () => { + it('hides modal with null trigger', () => { // Assert that hide is called with zero args, so that we don't trigger the logic // for hiding the modal via cancel, esc, headerclose, etc, without a captcha response - expect(modal.vm.hide).toHaveBeenCalledWith(); + expect(hideSpy).toHaveBeenCalledWith(); }); it('calls console.error with a message and the exception', () => { diff --git a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js index d4f588a0e09..4b7ca36f331 100644 --- a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js +++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js @@ -48,7 +48,6 @@ describe('CI Lint', () => { afterEach(() => { mockMutate.mockClear(); - wrapper.destroy(); }); it('displays the editor', () => { diff --git a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js index 5e0c35c9f90..8e012883f09 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js @@ -16,10 +16,6 @@ describe('Ci Project Variable wrapper', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Passes down the correct props to ci_variable_shared', () => { expect(findCiShared().props()).toEqual({ areScopedVariablesAvailable: false, diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js index 2fd395a1230..7181398c2a6 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -27,10 +27,6 @@ describe('Ci environments dropdown', () => { findListbox().vm.$emit('search', searchTerm); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('No environments found', () => { beforeEach(() => { createComponent({ searchTerm: 'stable' }); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js index c0fb133b9b1..77d90a7667d 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js @@ -24,10 +24,6 @@ describe('Ci Group Variable wrapper', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Props', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js index bd1e6b17d6b..ce5237a84f7 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js @@ -25,10 +25,6 @@ describe('Ci Project Variable wrapper', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Passes down the correct props to ci_variable_shared', () => { expect(findCiShared().props()).toEqual({ id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId), diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js index 508af964ca3..8f3fccc2804 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js @@ -85,10 +85,6 @@ describe('Ci variable modal', () => { const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type'); const findEnvironmentScopeText = () => wrapper.findByText('Environment scope'); - afterEach(() => { - wrapper.destroy(); - }); - describe('Adding a variable', () => { describe('when no key/value pair are present', () => { beforeEach(() => { diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js index 32af2ec4de9..0141232a299 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js @@ -22,6 +22,7 @@ describe('Ci variable table', () => { hideEnvironmentScope: false, isLoading: false, maxVariableLimit: 5, + pageInfo: { after: '' }, variables: mockVariablesWithScopes(projectString), }; @@ -37,10 +38,6 @@ describe('Ci variable table', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('props passing', () => { it('passes props down correctly to the ci table', () => { createComponent(); @@ -49,6 +46,7 @@ describe('Ci variable table', () => { entity: 'project', isLoading: defaultProps.isLoading, maxVariableLimit: defaultProps.maxVariableLimit, + pageInfo: defaultProps.pageInfo, variables: defaultProps.variables, }); }); @@ -144,4 +142,22 @@ describe('Ci variable table', () => { expect(wrapper.emitted(eventName)).toEqual([[newVariable]]); }); }); + + describe('pages events', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + eventName | args + ${'handle-prev-page'} | ${undefined} + ${'handle-next-page'} | ${undefined} + ${'sort-changed'} | ${{ sortDesc: true }} + `('bubbles up the $eventName event', async ({ args, eventName }) => { + findCiVariableTable().vm.$emit(eventName, args); + await nextTick(); + + expect(wrapper.emitted(eventName)).toEqual([[args]]); + }); + }); }); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js index c977ae773db..87192006efc 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js @@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { resolvers } from '~/ci/ci_variable_list/graphql/settings'; import { TYPENAME_GROUP } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; @@ -41,7 +41,7 @@ import { mockAdminVariables, } from '../mocks'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -53,6 +53,7 @@ const mockProvide = { const defaultProps = { areScopedVariablesAvailable: true, + pageInfo: {}, hideEnvironmentScope: false, refetchAfterMutation: false, }; @@ -105,345 +106,378 @@ describe('Ci Variable Shared Component', () => { mockVariables = jest.fn(); }); - 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('successfully', () => { - beforeEach(async () => { - mockEnvironments.mockResolvedValue(mockProjectEnvironments); - mockVariables.mockResolvedValue(mockProjectVariables); - - await createComponentWithApollo({ provide: createProjectProvide() }); + describe.each` + isVariablePagesEnabled | text + ${true} | ${'enabled'} + ${false} | ${'disabled'} + `('When Pages FF is $text', ({ isVariablePagesEnabled }) => { + const featureFlagProvide = isVariablePagesEnabled + ? { glFeatures: { ciVariablesPages: true } } + : {}; + + describe('while queries are being fetch', () => { + beforeEach(() => { + createComponentWithApollo({ isLoading: true }); }); - it('passes down the expected max variable limit as props', () => { - expect(findCiSettings().props('maxVariableLimit')).toBe( - mockProjectVariables.data.project.ciVariables.limit, - ); + it('shows a loading icon', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findCiTable().exists()).toBe(false); }); + }); - it('passes down the expected environments as props', () => { - expect(findCiSettings().props('environments')).toEqual([prodName, devName]); - }); + describe('when queries are resolved', () => { + describe('successfully', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockResolvedValue(mockProjectVariables); - it('passes down the expected variables as props', () => { - expect(findCiSettings().props('variables')).toEqual( - mockProjectVariables.data.project.ciVariables.nodes, - ); - }); + await createComponentWithApollo({ + provide: { ...createProjectProvide(), ...featureFlagProvide }, + }); + }); - it('createAlert was not called', () => { - expect(createAlert).not.toHaveBeenCalled(); - }); - }); + it('passes down the expected max variable limit as props', () => { + expect(findCiSettings().props('maxVariableLimit')).toBe( + mockProjectVariables.data.project.ciVariables.limit, + ); + }); - describe('with an error for variables', () => { - beforeEach(async () => { - mockEnvironments.mockResolvedValue(mockProjectEnvironments); - mockVariables.mockRejectedValue(); + it('passes down the expected environments as props', () => { + expect(findCiSettings().props('environments')).toEqual([prodName, devName]); + }); - await createComponentWithApollo(); - }); + it('passes down the expected variables as props', () => { + expect(findCiSettings().props('variables')).toEqual( + mockProjectVariables.data.project.ciVariables.nodes, + ); + }); - it('calls createAlert with the expected error message', () => { - expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText }); + it('createAlert was not called', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); }); - }); - describe('with an error for environments', () => { - beforeEach(async () => { - mockEnvironments.mockRejectedValue(); - mockVariables.mockResolvedValue(mockProjectVariables); + describe('with an error for variables', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockRejectedValue(); - await createComponentWithApollo(); - }); + await createComponentWithApollo({ provide: featureFlagProvide }); + }); - it('calls createAlert with the expected error message', () => { - expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText }); + it('calls createAlert with the expected error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText }); + }); }); - }); - }); - describe('environment query', () => { - describe('when there is an environment key in queryData', () => { - beforeEach(async () => { - mockEnvironments.mockResolvedValue(mockProjectEnvironments); - mockVariables.mockResolvedValue(mockProjectVariables); + describe('with an error for environments', () => { + beforeEach(async () => { + mockEnvironments.mockRejectedValue(); + mockVariables.mockResolvedValue(mockProjectVariables); - await createComponentWithApollo({ props: { ...createProjectProps() } }); - }); + await createComponentWithApollo({ provide: featureFlagProvide }); + }); - it('is executed', () => { - expect(mockVariables).toHaveBeenCalled(); + it('calls createAlert with the expected error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText }); + }); }); }); - describe('when there isnt an environment key in queryData', () => { - beforeEach(async () => { - mockVariables.mockResolvedValue(mockGroupVariables); + describe('environment query', () => { + describe('when there is an environment key in queryData', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockResolvedValue(mockProjectVariables); - await createComponentWithApollo({ props: { ...createGroupProps() } }); - }); + await createComponentWithApollo({ + props: { ...createProjectProps() }, + provide: featureFlagProvide, + }); + }); - it('is skipped', () => { - expect(mockVariables).not.toHaveBeenCalled(); + it('is executed', () => { + expect(mockVariables).toHaveBeenCalled(); + }); }); - }); - }); - describe('mutations', () => { - const groupProps = createGroupProps(); + describe('when there isnt an environment key in queryData', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); - beforeEach(async () => { - mockVariables.mockResolvedValue(mockGroupVariables); + await createComponentWithApollo({ + props: { ...createGroupProps() }, + provide: featureFlagProvide, + }); + }); - await createComponentWithApollo({ - customHandlers: [[getGroupVariables, mockVariables]], - props: groupProps, + it('is skipped', () => { + expect(mockVariables).not.toHaveBeenCalled(); + }); }); }); - it.each` - actionName | mutation | event - ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'} - ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'} - ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'} - `( - 'calls the right mutation from propsData 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: groupProps.fullPath, - id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id), - variable: newVariable, - }, - }); - }, - ); - - it.each` - actionName | event - ${'add'} | ${'add-variable'} - ${'update'} | ${'update-variable'} - ${'delete'} | ${'delete-variable'} - `( - 'throws with the specific graphql error if present when user performs $actionName variable', - async ({ event }) => { - const graphQLErrorMessage = 'There is a problem with this graphQL action'; - jest - .spyOn(wrapper.vm.$apollo, 'mutate') - .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } }); - await findCiSettings().vm.$emit(event, newVariable); - await nextTick(); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); - expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage }); - }, - ); - - it.each` - actionName | event - ${'add'} | ${'add-variable'} - ${'update'} | ${'update-variable'} - ${'delete'} | ${'delete-variable'} - `( - 'throws generic error on failure 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(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText }); - }, - ); + describe('mutations', () => { + const groupProps = createGroupProps(); - describe('without fullpath and ID props', () => { beforeEach(async () => { - mockVariables.mockResolvedValue(mockAdminVariables); + mockVariables.mockResolvedValue(mockGroupVariables); await createComponentWithApollo({ - customHandlers: [[getAdminVariables, mockVariables]], - props: createInstanceProps(), + customHandlers: [[getGroupVariables, mockVariables]], + props: groupProps, + provide: featureFlagProvide, }); }); - - it('does not pass fullPath and ID to the mutation', async () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(); - - await findCiSettings().vm.$emit('add-variable', newVariable); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION], - variables: { - endpoint: mockProvide.endpoint, - variable: newVariable, - }, - }); - }); - }); - }); - - describe('Props', () => { - const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables; - const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables; - - describe('in a specific context as', () => { it.each` - name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit - ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit} - ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit} - ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0} + actionName | mutation | event + ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'} + ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'} + ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'} `( - 'passes down all the required props when its a $name component', - async ({ - mutation, - maxVariableLimit, - mockVariablesValue, - mockEnvironmentsValue, - withEnvironments, - expectedEnvironments, - propsFn, - provideFn, - }) => { - const props = propsFn(); - const provide = provideFn(); - - mockVariables.mockResolvedValue(mockVariablesValue); - - if (withEnvironments) { - mockEnvironments.mockResolvedValue(mockEnvironmentsValue); - } - - let customHandlers = null; - - if (mutation) { - customHandlers = [[mutation, mockVariables]]; - } + 'calls the right mutation from propsData 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: groupProps.fullPath, + id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id), + variable: newVariable, + }, + }); + }, + ); - await createComponentWithApollo({ customHandlers, props, provide }); + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws with the specific graphql error if present when user performs $actionName variable', + async ({ event }) => { + const graphQLErrorMessage = 'There is a problem with this graphQL action'; + jest + .spyOn(wrapper.vm.$apollo, 'mutate') + .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } }); + await findCiSettings().vm.$emit(event, newVariable); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage }); + }, + ); - expect(findCiSettings().props()).toEqual({ - areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable, - hideEnvironmentScope: defaultProps.hideEnvironmentScope, - isLoading: false, - maxVariableLimit, - variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes, - entity: props.entity, - environments: expectedEnvironments, + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws generic error on failure 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(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText }); }, ); - }); - describe('refetchAfterMutation', () => { - it.each` - bool | text - ${true} | ${'refetches the variables'} - ${false} | ${'does not refetch the variables'} - `('when $bool it $text', async ({ bool }) => { - await createComponentWithApollo({ - props: { ...createInstanceProps(), refetchAfterMutation: bool }, - }); + describe('without fullpath and ID props', () => { + beforeEach(async () => { + mockVariables.mockResolvedValue(mockAdminVariables); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} }); - jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn()); + await createComponentWithApollo({ + customHandlers: [[getAdminVariables, mockVariables]], + props: createInstanceProps(), + provide: featureFlagProvide, + }); + }); - await findCiSettings().vm.$emit('add-variable', newVariable); + it('does not pass fullPath and ID to the mutation', async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(); - await nextTick(); + await findCiSettings().vm.$emit('add-variable', newVariable); - if (bool) { - expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled(); - } else { - expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled(); - } + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION], + variables: { + endpoint: mockProvide.endpoint, + variable: newVariable, + }, + }); + }); }); }); - describe('Validators', () => { - describe('queryData', () => { - let error; + describe('Props', () => { + const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables; + const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables; + + describe('in a specific context as', () => { + it.each` + name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit + ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit} + ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit} + ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0} + `( + 'passes down all the required props when its a $name component', + async ({ + mutation, + maxVariableLimit, + mockVariablesValue, + mockEnvironmentsValue, + withEnvironments, + expectedEnvironments, + propsFn, + provideFn, + }) => { + const props = propsFn(); + const provide = provideFn(); - beforeEach(async () => { - mockVariables.mockResolvedValue(mockGroupVariables); - }); + mockVariables.mockResolvedValue(mockVariablesValue); + + if (withEnvironments) { + mockEnvironments.mockResolvedValue(mockEnvironmentsValue); + } + + let customHandlers = null; + + if (mutation) { + customHandlers = [[mutation, mockVariables]]; + } - it('will mount component with right data', async () => { - try { await createComponentWithApollo({ - customHandlers: [[getGroupVariables, mockVariables]], - props: { ...createGroupProps() }, + customHandlers, + props, + provide: { ...provide, ...featureFlagProvide }, }); - } catch (e) { - error = e; - } finally { - expect(wrapper.exists()).toBe(true); - expect(error).toBeUndefined(); - } - }); - it('will not mount component with wrong data', async () => { - try { - await createComponentWithApollo({ - customHandlers: [[getGroupVariables, mockVariables]], - props: { ...createGroupProps(), queryData: { wrongKey: {} } }, + expect(findCiSettings().props()).toEqual({ + areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable, + hideEnvironmentScope: defaultProps.hideEnvironmentScope, + pageInfo: defaultProps.pageInfo, + isLoading: false, + maxVariableLimit, + variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data) + ?.nodes, + entity: props.entity, + environments: expectedEnvironments, }); - } catch (e) { - error = e; - } finally { - expect(wrapper.exists()).toBe(false); - expect(error.toString()).toContain('custom validator check failed for prop'); + }, + ); + }); + + describe('refetchAfterMutation', () => { + it.each` + bool | text + ${true} | ${'refetches the variables'} + ${false} | ${'does not refetch the variables'} + `('when $bool it $text', async ({ bool }) => { + await createComponentWithApollo({ + props: { ...createInstanceProps(), refetchAfterMutation: bool }, + provide: featureFlagProvide, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} }); + jest + .spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch') + .mockImplementation(jest.fn()); + + await findCiSettings().vm.$emit('add-variable', newVariable); + + await nextTick(); + + if (bool) { + expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled(); + } else { + expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled(); } }); }); - describe('mutationData', () => { - let error; + describe('Validators', () => { + describe('queryData', () => { + let error; - beforeEach(async () => { - mockVariables.mockResolvedValue(mockGroupVariables); - }); + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + }); - it('will mount component with right data', async () => { - try { - await createComponentWithApollo({ - props: { ...createGroupProps() }, - }); - } catch (e) { - error = e; - } finally { - expect(wrapper.exists()).toBe(true); - expect(error).toBeUndefined(); - } + it('will mount component with right data', async () => { + try { + await createComponentWithApollo({ + customHandlers: [[getGroupVariables, mockVariables]], + props: { ...createGroupProps() }, + provide: featureFlagProvide, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(true); + expect(error).toBeUndefined(); + } + }); + + it('will not mount component with wrong data', async () => { + try { + await createComponentWithApollo({ + customHandlers: [[getGroupVariables, mockVariables]], + props: { ...createGroupProps(), queryData: { wrongKey: {} } }, + provide: featureFlagProvide, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(false); + expect(error.toString()).toContain('custom validator check failed for prop'); + } + }); }); - it('will not mount component with wrong data', async () => { - try { - await createComponentWithApollo({ - props: { ...createGroupProps(), mutationData: { wrongKey: {} } }, - }); - } catch (e) { - error = e; - } finally { - expect(wrapper.exists()).toBe(false); - expect(error.toString()).toContain('custom validator check failed for prop'); - } + describe('mutationData', () => { + let error; + + beforeEach(async () => { + mockVariables.mockResolvedValue(mockGroupVariables); + }); + + it('will mount component with right data', async () => { + try { + await createComponentWithApollo({ + props: { ...createGroupProps() }, + provide: featureFlagProvide, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(true); + expect(error).toBeUndefined(); + } + }); + + it('will not mount component with wrong data', async () => { + try { + await createComponentWithApollo({ + props: { ...createGroupProps(), mutationData: { wrongKey: {} } }, + provide: featureFlagProvide, + }); + } catch (e) { + error = e; + } finally { + expect(wrapper.exists()).toBe(false); + expect(error.toString()).toContain('custom validator check failed for prop'); + } + }); }); }); }); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js index 9e2508c56ee..2ef789e89c3 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js @@ -12,18 +12,25 @@ describe('Ci variable table', () => { entity: 'project', isLoading: false, maxVariableLimit: mockVariables(projectString).length + 1, + pageInfo: {}, variables: mockVariables(projectString), }; const mockMaxVariableLimit = defaultProps.variables.length; - const createComponent = ({ props = {} } = {}) => { + const createComponent = ({ props = {}, provide = {} } = {}) => { wrapper = mountExtended(CiVariableTable, { attachTo: document.body, propsData: { ...defaultProps, ...props, }, + provide: { + glFeatures: { + ciVariablesPages: false, + }, + ...provide, + }, }); }; @@ -41,132 +48,136 @@ describe('Ci variable table', () => { return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit }); }; - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('When table is empty', () => { - beforeEach(() => { - createComponent({ props: { variables: [] } }); - }); + describe.each` + isVariablePagesEnabled | text + ${true} | ${'enabled'} + ${false} | ${'disabled'} + `('When Pages FF is $text', ({ isVariablePagesEnabled }) => { + const provide = isVariablePagesEnabled ? { glFeatures: { ciVariablesPages: true } } : {}; - it('displays empty message', () => { - expect(findEmptyVariablesPlaceholder().exists()).toBe(true); - }); - - it('hides the reveal button', () => { - expect(findRevealButton().exists()).toBe(false); - }); - }); + describe('When table is empty', () => { + beforeEach(() => { + createComponent({ props: { variables: [] }, provide }); + }); - describe('When table has variables', () => { - beforeEach(() => { - createComponent(); - }); + it('displays empty message', () => { + expect(findEmptyVariablesPlaceholder().exists()).toBe(true); + }); - it('does not display the empty message', () => { - expect(findEmptyVariablesPlaceholder().exists()).toBe(false); + it('hides the reveal button', () => { + expect(findRevealButton().exists()).toBe(false); + }); }); - it('displays the reveal button', () => { - expect(findRevealButton().exists()).toBe(true); - }); + describe('When table has variables', () => { + beforeEach(() => { + createComponent({ provide }); + }); - it('displays the correct amount of variables', async () => { - expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length); - }); + it('does not display the empty message', () => { + expect(findEmptyVariablesPlaceholder().exists()).toBe(false); + }); - it('displays the correct variable options', async () => { - expect(findOptionsValues(0)).toBe('Protected, Expanded'); - expect(findOptionsValues(1)).toBe('Masked'); - }); + it('displays the reveal button', () => { + expect(findRevealButton().exists()).toBe(true); + }); - it('enables the Add Variable button', () => { - expect(findAddButton().props('disabled')).toBe(false); - }); - }); + it('displays the correct amount of variables', async () => { + expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length); + }); - describe('When variables have exceeded the max limit', () => { - beforeEach(() => { - createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } }); - }); + it('displays the correct variable options', async () => { + expect(findOptionsValues(0)).toBe('Protected, Expanded'); + expect(findOptionsValues(1)).toBe('Masked'); + }); - it('disables the Add Variable button', () => { - expect(findAddButton().props('disabled')).toBe(true); + it('enables the Add Variable button', () => { + expect(findAddButton().props('disabled')).toBe(false); + }); }); - }); - describe('max limit reached alert', () => { - describe('when there is no variable limit', () => { + describe('When variables have exceeded the max limit', () => { beforeEach(() => { createComponent({ - props: { maxVariableLimit: 0 }, + props: { maxVariableLimit: mockVariables(projectString).length }, + provide, }); }); - it('hides alert', () => { - expect(findLimitReachedAlerts().length).toBe(0); + it('disables the Add Variable button', () => { + expect(findAddButton().props('disabled')).toBe(true); }); }); - describe('when variable limit exists', () => { - it('hides alert when limit has not been reached', () => { - createComponent(); + describe('max limit reached alert', () => { + describe('when there is no variable limit', () => { + beforeEach(() => { + createComponent({ + props: { maxVariableLimit: 0 }, + provide, + }); + }); - expect(findLimitReachedAlerts().length).toBe(0); + it('hides alert', () => { + expect(findLimitReachedAlerts().length).toBe(0); + }); }); - it('shows alert when limit has been reached', () => { - const exceedsVariableLimitText = generateExceedsVariableLimitText( - defaultProps.entity, - defaultProps.variables.length, - mockMaxVariableLimit, - ); + describe('when variable limit exists', () => { + it('hides alert when limit has not been reached', () => { + createComponent({ provide }); - createComponent({ - props: { maxVariableLimit: mockMaxVariableLimit }, + expect(findLimitReachedAlerts().length).toBe(0); }); - expect(findLimitReachedAlerts().length).toBe(2); + it('shows alert when limit has been reached', () => { + const exceedsVariableLimitText = generateExceedsVariableLimitText( + defaultProps.entity, + defaultProps.variables.length, + mockMaxVariableLimit, + ); + + createComponent({ + props: { maxVariableLimit: mockMaxVariableLimit }, + }); - expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false); - expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText); + expect(findLimitReachedAlerts().length).toBe(2); - expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false); - expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText); + expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false); + expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText); + + expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false); + expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText); + }); }); }); - }); - describe('Table click actions', () => { - beforeEach(() => { - createComponent(); - }); + describe('Table click actions', () => { + beforeEach(() => { + createComponent({ provide }); + }); - it('reveals secret values when button is clicked', async () => { - expect(findHiddenValues()).toHaveLength(defaultProps.variables.length); - expect(findRevealedValues()).toHaveLength(0); + it('reveals secret values when button is clicked', async () => { + expect(findHiddenValues()).toHaveLength(defaultProps.variables.length); + expect(findRevealedValues()).toHaveLength(0); - await findRevealButton().trigger('click'); + await findRevealButton().trigger('click'); - expect(findHiddenValues()).toHaveLength(0); - expect(findRevealedValues()).toHaveLength(defaultProps.variables.length); - }); + expect(findHiddenValues()).toHaveLength(0); + expect(findRevealedValues()).toHaveLength(defaultProps.variables.length); + }); - it('dispatches `setSelectedVariable` with correct variable to edit', async () => { - await findEditButton().trigger('click'); + it('dispatches `setSelectedVariable` with correct variable to edit', async () => { + await findEditButton().trigger('click'); - expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]); - }); + 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'); + it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => { + await findAddButton().trigger('click'); - expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]); + expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]); + }); }); }); }); diff --git a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js index b00e1adab63..48a85eba433 100644 --- a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js @@ -41,10 +41,6 @@ describe('EE - CodeSnippetAlert', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it("provides a link to the feature's documentation", () => { const docsLink = findDocsLink(); diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js index 8e1d8081dd8..b2dfa900b1d 100644 --- a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js @@ -33,10 +33,6 @@ describe('Pipeline Editor | Commit Form', () => { const findSubmitBtn = () => wrapper.find('[type="submit"]'); const findCancelBtn = () => wrapper.find('[type="reset"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('when the form is displayed', () => { beforeEach(async () => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js index f6e93c55bbb..f8be035d33c 100644 --- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js @@ -113,10 +113,6 @@ describe('Pipeline Editor | Commit section', () => { await waitForPromises(); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when the user commits a new file', () => { beforeEach(async () => { mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse); diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js index 137137ec657..0ecb77674d5 100644 --- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js @@ -21,10 +21,6 @@ describe('First pipeline card', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the title', () => { expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); }); diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js index cdce757ce7c..417597eaf1f 100644 --- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js @@ -12,10 +12,6 @@ describe('Getting started card', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the title', () => { expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); }); diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js index 6909916c3e6..5399924b462 100644 --- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js @@ -33,10 +33,6 @@ describe('Pipeline config reference card', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the title', () => { expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); }); diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js index 0c6879020de..547ba3cbd8b 100644 --- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js @@ -12,10 +12,6 @@ describe('Visual and Lint card', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the title', () => { expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); }); diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js index 42e372cc1db..b07d63dd5d9 100644 --- a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js @@ -11,10 +11,6 @@ describe('Pipeline editor drawer', () => { wrapper = shallowMount(PipelineEditorDrawer); }; - afterEach(() => { - wrapper.destroy(); - }); - it('emits close event when closing the drawer', () => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js index f510c61ee74..b0c889cfc9f 100644 --- a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js @@ -17,10 +17,6 @@ describe('Demo job pill', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the jobName', () => { expect(wrapper.text()).toContain(jobName); }); diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js index 2a2bc2547cc..2182b6e9cc6 100644 --- a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js @@ -34,10 +34,6 @@ describe('Text editor component', () => { const findIcon = () => wrapper.findComponent(GlIcon); const findEditor = () => wrapper.findComponent(MockSourceEditor); - afterEach(() => { - wrapper.destroy(); - }); - describe('when status is valid', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js index dc72694d26f..560e8840d57 100644 --- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js @@ -26,7 +26,6 @@ describe('CI Editor Header', () => { const findHelpBtn = () => wrapper.findByTestId('drawer-toggle'); afterEach(() => { - wrapper.destroy(); unmockTracking(); }); diff --git a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js index ec987be8cb8..0be26570fbf 100644 --- a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js @@ -1,7 +1,11 @@ import { shallowMount } from '@vue/test-utils'; +import { editor as monacoEditor } from 'monaco-editor'; +import SourceEditor from '~/vue_shared/components/source_editor.vue'; import { EDITOR_READY_EVENT } from '~/editor/constants'; +import { CiSchemaExtension as MockedCiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext'; import { SOURCE_EDITOR_DEBOUNCE } from '~/ci/pipeline_editor/constants'; +import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub'; import TextEditor from '~/ci/pipeline_editor/components/editor/text_editor.vue'; import { mockCiConfigPath, @@ -12,19 +16,26 @@ import { mockDefaultBranch, } from '../../mock_data'; +jest.mock('monaco-editor'); +jest.mock('~/editor/extensions/source_editor_ci_schema_ext', () => { + const { createMockSourceEditorExtension } = jest.requireActual( + 'helpers/create_mock_source_editor_extension', + ); + const { CiSchemaExtension } = jest.requireActual( + '~/editor/extensions/source_editor_ci_schema_ext', + ); + + return { + CiSchemaExtension: createMockSourceEditorExtension(CiSchemaExtension), + }; +}); + describe('Pipeline Editor | Text editor component', () => { let wrapper; let editorReadyListener; - let mockUse; - let mockRegisterCiSchema; - let mockEditorInstance; - let editorInstanceDetail; - - const MockSourceEditor = { - template: '<div/>', - props: ['value', 'fileName', 'editorOptions', 'debounceValue'], - }; + + const getMonacoEditor = () => monacoEditor.create.mock.results[0].value; const createComponent = (mountFn = shallowMount) => { wrapper = mountFn(TextEditor, { @@ -44,33 +55,17 @@ describe('Pipeline Editor | Text editor component', () => { [EDITOR_READY_EVENT]: editorReadyListener, }, stubs: { - SourceEditor: MockSourceEditor, + SourceEditor, }, }); }; - const findEditor = () => wrapper.findComponent(MockSourceEditor); + const findEditor = () => wrapper.findComponent(SourceEditor); beforeEach(() => { - editorReadyListener = jest.fn(); - mockUse = jest.fn(); - mockRegisterCiSchema = jest.fn(); - mockEditorInstance = { - use: mockUse, - registerCiSchema: mockRegisterCiSchema, - }; - editorInstanceDetail = { - detail: { - instance: mockEditorInstance, - }, - }; - }); + jest.spyOn(monacoEditor, 'create'); - afterEach(() => { - wrapper.destroy(); - - mockUse.mockClear(); - mockRegisterCiSchema.mockClear(); + editorReadyListener = jest.fn(); }); describe('template', () => { @@ -99,21 +94,34 @@ describe('Pipeline Editor | Text editor component', () => { }); it('bubbles up events', () => { - findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail); - expect(editorReadyListener).toHaveBeenCalled(); }); + + it('scrolls editor to bottom on scroll editor to bottom event', () => { + const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop'); + + eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM); + + expect(setScrollTop).toHaveBeenCalledWith(getMonacoEditor().getScrollHeight()); + }); + + it('when destroyed, destroys scroll listener', () => { + const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop'); + + wrapper.destroy(); + eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM); + + expect(setScrollTop).not.toHaveBeenCalled(); + }); }); describe('CI schema', () => { beforeEach(() => { createComponent(); - findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail); }); it('configures editor with syntax highlight', () => { - expect(mockUse).toHaveBeenCalledTimes(1); - expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1); + expect(MockedCiSchemaExtension.mockedMethods.registerCiSchema).toHaveBeenCalledTimes(1); }); }); }); diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js index a26232df58f..bf14f4c4cd6 100644 --- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -133,10 +133,6 @@ describe('Pipeline editor branch switcher', () => { mockAvailableBranchQuery = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - const testErrorHandling = () => { expect(wrapper.emitted('showError')).toBeDefined(); expect(wrapper.emitted('showError')[0]).toEqual([ diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js index 907db16913c..19c113689c2 100644 --- a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js @@ -48,10 +48,6 @@ describe('Pipeline editor file nav', () => { const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle'); const findPopoverContainer = () => wrapper.findComponent(FileTreePopover); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js index 11ba517e0eb..306dd78d395 100644 --- a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js @@ -22,7 +22,7 @@ describe('Pipeline editor file nav', () => { includes, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs, }), @@ -35,7 +35,6 @@ describe('Pipeline editor file nav', () => { afterEach(() => { localStorage.clear(); - wrapper.destroy(); }); describe('template', () => { diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js index bceb741f91c..80737e9a8ab 100644 --- a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js @@ -18,10 +18,6 @@ describe('Pipeline editor file nav', () => { const fileIcon = () => wrapper.findComponent(FileIcon); const link = () => wrapper.findComponent(GlLink); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js index 555b9f29fbf..a651664851e 100644 --- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js @@ -26,11 +26,6 @@ describe('Pipeline editor header', () => { const findPipelineStatus = () => wrapper.findComponent(PipelineStatus); const findValidationSegment = () => wrapper.findComponent(ValidationSegment); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('template', () => { it('hides the pipeline status for new projects without a CI file', () => { createComponent({ props: { isNewCiConfigFile: true } }); diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js index a62c51ffb59..3faa2890254 100644 --- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js @@ -48,7 +48,6 @@ describe('Pipeline Status', () => { afterEach(() => { mockPipelineQuery.mockReset(); - wrapper.destroy(); }); describe('loading icon', () => { diff --git a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js index 0853a6f4ca4..a107a626c6d 100644 --- a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js @@ -1,11 +1,10 @@ import VueApollo from 'vue-apollo'; -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import Vue from 'vue'; import { escape } from 'lodash'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; import { sprintf } from '~/locale'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; import ValidationSegment, { i18n, } from '~/ci/pipeline_editor/components/header/validation_segment.vue'; @@ -20,8 +19,8 @@ import { } from '~/ci/pipeline_editor/constants'; import { mergeUnwrappedCiConfig, + mockCiTroubleshootingPath, mockCiYml, - mockLintUnavailableHelpPagePath, mockYmlHelpPagePath, } from '../../mock_data'; @@ -43,29 +42,27 @@ describe('Validation segment component', () => { }, }); - wrapper = extendedWrapper( - shallowMount(ValidationSegment, { - apolloProvider: mockApollo, - provide: { - ymlHelpPagePath: mockYmlHelpPagePath, - lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath, - }, - propsData: { - ciConfig: mergeUnwrappedCiConfig(), - ciFileContent: mockCiYml, - ...props, - }, - }), - ); + wrapper = shallowMountExtended(ValidationSegment, { + apolloProvider: mockApollo, + provide: { + ymlHelpPagePath: mockYmlHelpPagePath, + ciTroubleshootingPath: mockCiTroubleshootingPath, + }, + propsData: { + ciConfig: mergeUnwrappedCiConfig(), + ciFileContent: mockCiYml, + ...props, + }, + stubs: { + GlSprintf, + }, + }); }; const findIcon = () => wrapper.findComponent(GlIcon); - const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink'); - const findValidationMsg = () => wrapper.findByTestId('validationMsg'); - - afterEach(() => { - wrapper.destroy(); - }); + const findHelpLink = () => wrapper.findComponent(GlLink); + const findValidationMsg = () => wrapper.findComponent(GlSprintf); + const findValidationSegment = () => wrapper.findByTestId('validation-segment'); it('shows the loading state', () => { createComponent({ appStatus: EDITOR_APP_STATUS_LOADING }); @@ -82,8 +79,12 @@ describe('Validation segment component', () => { expect(findIcon().props('name')).toBe('check'); }); + it('does not render a link', () => { + expect(findHelpLink().exists()).toBe(false); + }); + it('shows a message for empty state', () => { - expect(findValidationMsg().text()).toBe(i18n.empty); + expect(findValidationSegment().text()).toBe(i18n.empty); }); }); @@ -97,12 +98,15 @@ describe('Validation segment component', () => { }); it('shows a message for valid state', () => { - expect(findValidationMsg().text()).toContain(i18n.valid); + expect(findValidationSegment().text()).toBe( + sprintf(i18n.valid, { linkStart: '', linkEnd: '' }), + ); }); it('shows the learn more link', () => { - expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath); - expect(findLearnMoreLink().text()).toBe(i18n.learnMore); + expect(findValidationMsg().exists()).toBe(true); + expect(findValidationMsg().text()).toBe('Learn more'); + expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath); }); }); @@ -117,13 +121,16 @@ describe('Validation segment component', () => { expect(findIcon().props('name')).toBe('warning-solid'); }); - it('has message for invalid state', () => { - expect(findValidationMsg().text()).toBe(i18n.invalid); + it('shows a message for invalid state', () => { + expect(findValidationSegment().text()).toBe( + sprintf(i18n.invalid, { linkStart: '', linkEnd: '' }), + ); }); it('shows the learn more link', () => { - expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath); - expect(findLearnMoreLink().text()).toBe('Learn more'); + expect(findValidationMsg().exists()).toBe(true); + expect(findValidationMsg().text()).toBe('Learn more'); + expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath); }); describe('with multiple errors', () => { @@ -140,11 +147,16 @@ describe('Validation segment component', () => { }, }); }); + + it('shows the learn more link', () => { + expect(findValidationMsg().exists()).toBe(true); + expect(findValidationMsg().text()).toBe('Learn more'); + expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath); + }); + it('shows an invalid state with an error', () => { - // Test the error is shown _and_ the string matches - expect(findValidationMsg().text()).toContain(firstError); - expect(findValidationMsg().text()).toBe( - sprintf(i18n.invalidWithReason, { reason: firstError }), + expect(findValidationSegment().text()).toBe( + sprintf(i18n.invalidWithReason, { reason: firstError, linkStart: '', linkEnd: '' }), ); }); }); @@ -163,10 +175,8 @@ describe('Validation segment component', () => { }); }); it('shows an invalid state with an error while preventing XSS', () => { - const { innerHTML } = findValidationMsg().element; - - expect(innerHTML).not.toContain(evilError); - expect(innerHTML).toContain(escape(evilError)); + expect(findValidationSegment().html()).not.toContain(evilError); + expect(findValidationSegment().html()).toContain(escape(evilError)); }); }); }); @@ -182,16 +192,18 @@ describe('Validation segment component', () => { }); it('show a message that the service is unavailable', () => { - expect(findValidationMsg().text()).toBe(i18n.unavailableValidation); + expect(findValidationSegment().text()).toBe( + sprintf(i18n.unavailableValidation, { linkStart: '', linkEnd: '' }), + ); }); it('shows the time-out icon', () => { expect(findIcon().props('name')).toBe('time-out'); }); - it('shows the learn more link', () => { - expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath); - expect(findLearnMoreLink().text()).toBe(i18n.learnMore); + it('shows the link to ci troubleshooting', () => { + expect(findValidationMsg().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe(mockCiTroubleshootingPath); }); }); }); diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js new file mode 100644 index 00000000000..c7c40c3a4b9 --- /dev/null +++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js @@ -0,0 +1,39 @@ +import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants'; + +describe('Image item', () => { + let wrapper; + + const findImageNameInput = () => wrapper.findByTestId('image-name-input'); + const findImageEntrypointInput = () => wrapper.findByTestId('image-entrypoint-input'); + + const dummyImageName = 'dummyImageName'; + const dummyImageEntrypoint = 'dummyImageEntrypoint'; + + const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => { + wrapper = shallowMountExtended(ImageItem, { + propsData: { + job, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('should emit update job event when filling inputs', () => { + expect(wrapper.emitted('update-job')).toBeUndefined(); + + findImageNameInput().vm.$emit('input', dummyImageName); + + expect(wrapper.emitted('update-job')).toHaveLength(1); + expect(wrapper.emitted('update-job')[0]).toEqual(['image.name', dummyImageName]); + + findImageEntrypointInput().vm.$emit('input', dummyImageEntrypoint); + + expect(wrapper.emitted('update-job')).toHaveLength(2); + expect(wrapper.emitted('update-job')[1]).toEqual(['image.entrypoint', [dummyImageEntrypoint]]); + }); +}); diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js new file mode 100644 index 00000000000..eaad0dae90d --- /dev/null +++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js @@ -0,0 +1,61 @@ +import createStore from '~/ci/pipeline_editor/store'; +import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants'; + +describe('Job setup item', () => { + let wrapper; + + const findJobNameInput = () => wrapper.findByTestId('job-name-input'); + const findJobScriptInput = () => wrapper.findByTestId('job-script-input'); + const findJobTagsInput = () => wrapper.findByTestId('job-tags-input'); + const findJobStageInput = () => wrapper.findByTestId('job-stage-input'); + + const dummyJobName = 'dummyJobName'; + const dummyJobScript = 'dummyJobScript'; + const dummyJobStage = 'dummyJobStage'; + const dummyJobTags = ['tag1']; + + const createComponent = () => { + wrapper = shallowMountExtended(JobSetupItem, { + store: createStore(), + propsData: { + tagOptions: [ + { id: 'tag1', name: 'tag1' }, + { id: 'tag2', name: 'tag2' }, + ], + isNameValid: true, + isScriptValid: true, + job: JSON.parse(JSON.stringify(JOB_TEMPLATE)), + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('should emit update job event when filling inputs', () => { + expect(wrapper.emitted('update-job')).toBeUndefined(); + + findJobNameInput().vm.$emit('input', dummyJobName); + + expect(wrapper.emitted('update-job')).toHaveLength(1); + expect(wrapper.emitted('update-job')[0]).toEqual(['name', dummyJobName]); + + findJobScriptInput().vm.$emit('input', dummyJobScript); + + expect(wrapper.emitted('update-job')).toHaveLength(2); + expect(wrapper.emitted('update-job')[1]).toEqual(['script', dummyJobScript]); + + findJobStageInput().vm.$emit('input', dummyJobStage); + + expect(wrapper.emitted('update-job')).toHaveLength(3); + expect(wrapper.emitted('update-job')[2]).toEqual(['stage', dummyJobStage]); + + findJobTagsInput().vm.$emit('input', dummyJobTags); + + expect(wrapper.emitted('update-job')).toHaveLength(4); + expect(wrapper.emitted('update-job')[3]).toEqual(['tags', dummyJobTags]); + }); +}); diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js index 79200d92598..b293805d653 100644 --- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js @@ -1,24 +1,47 @@ import { GlDrawer } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; +import { stringify } from 'yaml'; import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue'; +import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue'; +import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue'; +import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import createStore from '~/ci/pipeline_editor/store'; +import { mockAllRunnersQueryResponse } from 'jest/ci/pipeline_editor/mock_data'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub'; Vue.use(VueApollo); describe('Job assistant drawer', () => { let wrapper; + let mockApollo; + + const dummyJobName = 'a'; + const dummyJobScript = 'b'; + const dummyImageName = 'c'; + const dummyImageEntrypoint = 'd'; const findDrawer = () => wrapper.findComponent(GlDrawer); + const findJobSetupItem = () => wrapper.findComponent(JobSetupItem); + const findImageItem = () => wrapper.findComponent(ImageItem); + const findConfirmButton = () => wrapper.findByTestId('confirm-button'); const findCancelButton = () => wrapper.findByTestId('cancel-button'); const createComponent = () => { + mockApollo = createMockApollo([ + [getAllRunners, jest.fn().mockResolvedValue(mockAllRunnersQueryResponse)], + ]); + wrapper = mountExtended(JobAssistantDrawer, { + store: createStore(), propsData: { isVisible: true, }, + apolloProvider: mockApollo, }); }; @@ -27,6 +50,14 @@ describe('Job assistant drawer', () => { await waitForPromises(); }); + it('should contain job setup accordion', () => { + expect(findJobSetupItem().exists()).toBe(true); + }); + + it('should contain image accordion', () => { + expect(findImageItem().exists()).toBe(true); + }); + it('should emit close job assistant drawer event when closing the drawer', () => { expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined(); @@ -42,4 +73,83 @@ describe('Job assistant drawer', () => { expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1); }); + + it('trigger validate if job name is empty', async () => { + const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig'); + findJobSetupItem().vm.$emit('update-job', 'script', 'b'); + findConfirmButton().trigger('click'); + + await nextTick(); + + expect(findJobSetupItem().props('isNameValid')).toBe(false); + expect(findJobSetupItem().props('isScriptValid')).toBe(true); + expect(updateCiConfigSpy).toHaveBeenCalledTimes(0); + }); + + describe('when enter valid input', () => { + beforeEach(() => { + findJobSetupItem().vm.$emit('update-job', 'name', dummyJobName); + findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript); + findImageItem().vm.$emit('update-job', 'image.name', dummyImageName); + findImageItem().vm.$emit('update-job', 'image.entrypoint', [dummyImageEntrypoint]); + }); + + it('passes correct prop to accordions', () => { + const accordions = [findJobSetupItem(), findImageItem()]; + accordions.forEach((accordion) => { + expect(accordion.props('job')).toMatchObject({ + name: dummyJobName, + script: dummyJobScript, + image: { + name: dummyImageName, + entrypoint: [dummyImageEntrypoint], + }, + }); + }); + }); + + it('job name and script state should be valid', () => { + expect(findJobSetupItem().props('isNameValid')).toBe(true); + expect(findJobSetupItem().props('isScriptValid')).toBe(true); + }); + + it('should clear job data when click confirm button', async () => { + findConfirmButton().trigger('click'); + + await nextTick(); + + expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' }); + }); + + it('should clear job data when click cancel button', async () => { + findCancelButton().trigger('click'); + + await nextTick(); + + expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' }); + }); + + it('should update correct ci content when click add button', () => { + const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig'); + + findConfirmButton().trigger('click'); + + expect(updateCiConfigSpy).toHaveBeenCalledWith( + `\n${stringify({ + [dummyJobName]: { + script: dummyJobScript, + image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] }, + }, + })}`, + ); + }); + + it('should emit scroll editor to button event when click add button', () => { + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + + findConfirmButton().trigger('click'); + + expect(eventHubSpy).toHaveBeenCalledWith(SCROLL_EDITOR_TO_BOTTOM); + }); + }); }); diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js index d43bdec3a33..cc9a77ae525 100644 --- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js @@ -40,10 +40,6 @@ describe('CI Lint Results', () => { const findAfterScripts = findAllByTestId('after-script'); const filterEmptyScripts = (property) => mockJobs.filter((job) => job[property].length !== 0); - afterEach(() => { - wrapper.destroy(); - }); - describe('Empty results', () => { it('renders with no jobs, errors or warnings defined', () => { createComponent({ jobs: undefined, errors: undefined, warnings: undefined }, shallowMount); diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js index b5e3ea06c2c..d09e22898cd 100644 --- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js @@ -21,11 +21,6 @@ describe('CI lint warnings', () => { const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]'); const findWarningMessage = () => trimText(wrapper.findComponent(GlSprintf).text()); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('displays the warning alert', () => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js index f40db50aab7..52a543c7686 100644 --- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -119,6 +119,7 @@ describe('Pipeline editor tabs component', () => { }); afterEach(() => { + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy wrapper.destroy(); }); diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js index 63ebfc0559d..a9aabb103f2 100644 --- a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js @@ -22,7 +22,6 @@ describe('FileTreePopover component', () => { afterEach(() => { localStorage.clear(); - wrapper.destroy(); }); describe('default', () => { diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js index cf0b974081e..23f9c7a87ee 100644 --- a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js @@ -19,10 +19,6 @@ describe('ValidatePopover component', () => { const findHelpLink = () => wrapper.findByTestId('help-link'); const findFeedbackLink = () => wrapper.findByTestId('feedback-link'); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { beforeEach(async () => { createComponent({ diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js index ca6033f2ff5..186fd803d47 100644 --- a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js @@ -12,10 +12,6 @@ describe('WalkthroughPopover component', () => { return extendedWrapper(mountFn(WalkthroughPopover)); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('CTA button clicked', () => { beforeEach(async () => { wrapper = createComponent(mount); diff --git a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js index b22c98e5544..8b8dd4d22c2 100644 --- a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js @@ -4,10 +4,9 @@ import ConfirmDialog from '~/ci/pipeline_editor/components/ui/confirm_unsaved_ch describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => { let beforeUnloadEvent; let setDialogContent; - let wrapper; const createComponent = (propsData = {}) => { - wrapper = shallowMount(ConfirmDialog, { + shallowMount(ConfirmDialog, { propsData, }); }; @@ -21,7 +20,6 @@ describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => { afterEach(() => { beforeUnloadEvent.preventDefault.mockRestore(); setDialogContent.mockRestore(); - wrapper.destroy(); }); it('shows confirmation dialog when there are unsaved changes', () => { diff --git a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js index 3c68f74af43..e636a89c6d9 100644 --- a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js @@ -23,10 +23,6 @@ describe('Pipeline editor empty state', () => { const findConfirmButton = () => wrapper.findComponent(GlButton); const findDescription = () => wrapper.findComponent(GlSprintf); - afterEach(() => { - wrapper.destroy(); - }); - describe('when project uses an external CI config', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js index ae25142b455..8874add6bb2 100644 --- a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js @@ -99,10 +99,6 @@ describe('Pipeline Editor Validate Tab', () => { mockBlobContentData = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('while initial CI content is loading', () => { beforeEach(() => { createComponent({ isBlobLoading: true }); diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js index 541123d7efc..ecfc477184b 100644 --- a/spec/frontend/ci/pipeline_editor/mock_data.js +++ b/spec/frontend/ci/pipeline_editor/mock_data.js @@ -12,7 +12,7 @@ export const mockCommitSha = 'aabbccdd'; export const mockCommitNextSha = 'eeffgghh'; export const mockIncludesHelpPagePath = '/-/includes/help'; export const mockLintHelpPagePath = '/-/lint-help'; -export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot'; +export const mockCiTroubleshootingPath = '/-/pipeline-editor/troubleshoot'; export const mockSimulatePipelineHelpPagePath = '/-/simulate-pipeline-help'; export const mockYmlHelpPagePath = '/-/yml-help'; export const mockCommitMessage = 'My commit message'; @@ -583,6 +583,91 @@ export const mockCommitCreateResponse = { }, }; +export const mockAllRunnersQueryResponse = { + data: { + runners: { + nodes: [ + { + id: 'gid://gitlab/Ci::Runner/1', + description: 'test', + runnerType: 'PROJECT_TYPE', + shortSha: 'DdTYMQGS', + version: '15.6.1', + ipAddress: '127.0.0.1', + active: true, + locked: true, + jobCount: 0, + jobExecutionStatus: 'IDLE', + tagList: ['tag1', 'tag2', 'tag3'], + createdAt: '2022-11-29T09:37:43Z', + contactedAt: null, + status: 'NEVER_CONTACTED', + userPermissions: { + updateRunner: true, + deleteRunner: true, + __typename: 'RunnerPermissions', + }, + groups: null, + ownerProject: { + id: 'gid://gitlab/Project/1', + name: '123', + nameWithNamespace: 'Administrator / 123', + webUrl: 'http://127.0.0.1:3000/root/test', + __typename: 'Project', + }, + __typename: 'CiRunner', + upgradeStatus: 'NOT_AVAILABLE', + adminUrl: 'http://127.0.0.1:3000/admin/runners/1', + editAdminUrl: 'http://127.0.0.1:3000/admin/runners/1/edit', + }, + { + id: 'gid://gitlab/Ci::Runner/2', + description: 'test', + runnerType: 'PROJECT_TYPE', + shortSha: 'DdTYMQGA', + version: '15.6.1', + ipAddress: '127.0.0.1', + active: true, + locked: true, + jobCount: 0, + jobExecutionStatus: 'IDLE', + tagList: ['tag3', 'tag4'], + createdAt: '2022-11-29T09:37:43Z', + contactedAt: null, + status: 'NEVER_CONTACTED', + userPermissions: { + updateRunner: true, + deleteRunner: true, + __typename: 'RunnerPermissions', + }, + groups: null, + ownerProject: { + id: 'gid://gitlab/Project/1', + name: '123', + nameWithNamespace: 'Administrator / 123', + webUrl: 'http://127.0.0.1:3000/root/test', + __typename: 'Project', + }, + __typename: 'CiRunner', + upgradeStatus: 'NOT_AVAILABLE', + adminUrl: 'http://127.0.0.1:3000/admin/runners/2', + editAdminUrl: 'http://127.0.0.1:3000/admin/runners/2/edit', + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: + 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ', + endCursor: + 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ', + __typename: 'PageInfo', + }, + __typename: 'CiRunnerConnection', + }, + }, +}; + export const mockCommitCreateResponseNewEtag = { data: { commitCreate: { diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js index a103acb33bc..7a13bfbd1ab 100644 --- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; import { objectToQuery, redirectTo } from '~/lib/utils/url_utility'; import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers'; +import createStore from '~/ci/pipeline_editor/store'; import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue'; import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue'; import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue'; @@ -80,7 +81,9 @@ describe('Pipeline editor app component', () => { provide = {}, stubs = {}, } = {}) => { + const store = createStore(); wrapper = shallowMount(PipelineEditorApp, { + store, provide: { ...defaultProvide, ...provide }, stubs, mocks: { @@ -162,10 +165,6 @@ describe('Pipeline editor app component', () => { mockPipelineQuery = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('loading state', () => { it('displays a loading icon if the blob query is loading', () => { createComponent({ blobLoading: true }); @@ -256,6 +255,10 @@ describe('Pipeline editor app component', () => { .mockImplementation(jest.fn()); }); + it('available stages is updated', () => { + expect(wrapper.vm.$store.state.availableStages).toStrictEqual(['test', 'build']); + }); + it('shows pipeline editor home component', () => { expect(findEditorHome().exists()).toBe(true); }); @@ -351,7 +354,9 @@ describe('Pipeline editor app component', () => { }); it('shows that the lint service is down', () => { - expect(findValidationSegment().text()).toContain( + const validationMessage = findValidationSegment().findComponent(GlSprintf); + + expect(validationMessage.attributes('message')).toContain( validationSegmenti18n.unavailableValidation, ); }); diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js index 4f8f2112abe..7ec6d4c6a01 100644 --- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js @@ -67,7 +67,6 @@ describe('Pipeline editor home wrapper', () => { afterEach(() => { localStorage.clear(); - wrapper.destroy(); }); describe('renders', () => { diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js index 6f18899ebac..1349461d8bc 100644 --- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js @@ -32,6 +32,7 @@ import { mockProjectId, mockRefs, mockYamlVariables, + mockPipelineConfigButtonText, } from '../mock_data'; Vue.use(VueApollo); @@ -42,6 +43,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ const projectRefsEndpoint = '/root/project/refs'; const pipelinesPath = '/root/project/-/pipelines'; +const pipelinesEditorPath = '/root/project/-/ci/editor'; const projectPath = '/root/project/-/pipelines/config_variables'; const newPipelinePostResponse = { id: 1 }; const defaultBranch = 'main'; @@ -65,6 +67,7 @@ describe('Pipeline New Form', () => { wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown'); const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem); const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert'); + const findPipelineConfigButton = () => wrapper.findByTestId('ci-cd-pipeline-configuration'); const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert'); const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf); const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning'); @@ -106,6 +109,8 @@ describe('Pipeline New Form', () => { propsData: { projectId: mockProjectId, pipelinesPath, + pipelinesEditorPath, + canViewPipelineEditor: true, projectPath, defaultBranch, refParam: defaultBranch, @@ -128,7 +133,6 @@ describe('Pipeline New Form', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('Form', () => { @@ -500,6 +504,17 @@ describe('Pipeline New Form', () => { expect(findSubmitButton().props('disabled')).toBe(false); }); + it('shows pipeline configuration button for user who can view', () => { + expect(findPipelineConfigButton().exists()).toBe(true); + expect(findPipelineConfigButton().text()).toBe(mockPipelineConfigButtonText); + }); + + it('does not show pipeline configuration button for user who can not view', () => { + createComponentWithApollo({ props: { canViewPipelineEditor: false } }); + + expect(findPipelineConfigButton().exists()).toBe(false); + }); + it('does not show the credit card validation required alert', () => { expect(findCCAlert().exists()).toBe(false); }); diff --git a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js index cf8009e388f..60ace483712 100644 --- a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js +++ b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js @@ -1,4 +1,4 @@ -import { GlListbox, GlListboxItem } from '@gitlab/ui'; +import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -13,13 +13,13 @@ const projectRefsEndpoint = '/root/project/refs'; const refShortName = 'main'; const refFullName = 'refs/heads/main'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Pipeline New Form', () => { let wrapper; let mock; - const findDropdown = () => wrapper.findComponent(GlListbox); + const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); const findRefsDropdownItems = () => wrapper.findAllComponents(GlListboxItem); const findSearchBox = () => wrapper.findByTestId('listbox-search-input'); const findListboxGroups = () => wrapper.findAll('ul[role="group"]'); diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js index 5b935c0c819..175f513217b 100644 --- a/spec/frontend/ci/pipeline_new/mock_data.js +++ b/spec/frontend/ci/pipeline_new/mock_data.js @@ -133,3 +133,5 @@ export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQue mockYamlVariablesWithoutDesc, ); export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null); + +export const mockPipelineConfigButtonText = 'Go to the pipeline editor'; diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js index ba948f12b33..c45267e5a47 100644 --- a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js @@ -20,10 +20,6 @@ describe('Delete pipeline schedule modal', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('emits the deleteSchedule event', async () => { findModal().vm.$emit('primary'); diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js index 611993556e3..50008cedd9c 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js @@ -16,6 +16,7 @@ import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/g import { mockGetPipelineSchedulesGraphQLResponse, mockPipelineScheduleNodes, + mockPipelineScheduleCurrentUser, deleteMutationResponse, playMutationResponse, takeOwnershipMutationResponse, @@ -79,10 +80,6 @@ describe('Pipeline schedules app', () => { const findSchedulesCharacteristics = () => wrapper.findByTestId('pipeline-schedules-characteristics'); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); @@ -115,6 +112,7 @@ describe('Pipeline schedules app', () => { await waitForPromises(); expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes); + expect(findTable().props('currentUser')).toEqual(mockPipelineScheduleCurrentUser); }); it('shows query error alert', async () => { diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js index 6fb6a8bc33b..be0052fc7cf 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js @@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue'; import { mockPipelineScheduleNodes, + mockPipelineScheduleCurrentUser, mockPipelineScheduleAsGuestNodes, mockTakeOwnershipNodes, } from '../../../mock_data'; @@ -12,6 +13,7 @@ describe('Pipeline schedule actions', () => { const defaultProps = { schedule: mockPipelineScheduleNodes[0], + currentUser: mockPipelineScheduleCurrentUser, }; const createComponent = (props = defaultProps) => { @@ -27,18 +29,17 @@ describe('Pipeline schedule actions', () => { const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn'); const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn'); - afterEach(() => { - wrapper.destroy(); - }); - - it('displays action buttons', () => { + it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => { createComponent(); expect(findAllButtons()).toHaveLength(3); }); - it('does not display action buttons', () => { - createComponent({ schedule: mockPipelineScheduleAsGuestNodes[0] }); + it('does not display action buttons when user is not owner and does not have adminPipelineSchedule permission', () => { + createComponent({ + schedule: mockPipelineScheduleAsGuestNodes[0], + currentUser: mockPipelineScheduleCurrentUser, + }); expect(findAllButtons()).toHaveLength(0); }); @@ -54,7 +55,10 @@ describe('Pipeline schedule actions', () => { }); it('take ownership button emits showTakeOwnershipModal event and schedule id', () => { - createComponent({ schedule: mockTakeOwnershipNodes[0] }); + createComponent({ + schedule: mockTakeOwnershipNodes[0], + currentUser: mockPipelineScheduleCurrentUser, + }); findTakeOwnershipBtn().vm.$emit('click'); diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js index 0821c59c8a0..ae069145292 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js @@ -21,10 +21,6 @@ describe('Pipeline schedule last pipeline', () => { const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink); const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text'); - afterEach(() => { - wrapper.destroy(); - }); - it('displays pipeline status', () => { createComponent(); diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js index 1c06c411097..3bdbb371ddc 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js @@ -21,10 +21,6 @@ describe('Pipeline schedule next run', () => { const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip); const findInactive = () => wrapper.findByTestId('pipeline-schedule-inactive'); - afterEach(() => { - wrapper.destroy(); - }); - it('displays time ago', () => { createComponent(); diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js index 6c1991cb4ac..849bef80f42 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js @@ -25,10 +25,6 @@ describe('Pipeline schedule owner', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays avatar', () => { expect(findAvatar().exists()).toBe(true); expect(findAvatar().props('src')).toBe(defaultProps.schedule.owner.avatarUrl); diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js index f531f04a736..5cc3829efbd 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js @@ -25,10 +25,6 @@ describe('Pipeline schedule target', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays icon', () => { expect(findIcon().exists()).toBe(true); expect(findIcon().props('name')).toBe('fork'); diff --git a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js index 316b3bcf926..e488a36f3dc 100644 --- a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js @@ -1,13 +1,14 @@ import { GlTableLite } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue'; -import { mockPipelineScheduleNodes } from '../../mock_data'; +import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../../mock_data'; describe('Pipeline schedules table', () => { let wrapper; const defaultProps = { schedules: mockPipelineScheduleNodes, + currentUser: mockPipelineScheduleCurrentUser, }; const createComponent = (props = defaultProps) => { @@ -25,10 +26,6 @@ describe('Pipeline schedules table', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays table', () => { expect(findTable().exists()).toBe(true); }); diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js index 7e6d4ec4bf8..e4ff9a0545b 100644 --- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js @@ -25,14 +25,12 @@ describe('Take ownership modal', () => { const actionPrimary = findModal().props('actionPrimary'); expect(actionPrimary.attributes).toEqual( - expect.objectContaining([ - { - category: 'primary', - variant: 'confirm', - href: url, - 'data-method': 'post', - }, - ]), + expect.objectContaining({ + category: 'primary', + variant: 'confirm', + href: url, + 'data-method': 'post', + }), ); }); diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js index 2826c054249..1485f6beea4 100644 --- a/spec/frontend/ci/pipeline_schedules/mock_data.js +++ b/spec/frontend/ci/pipeline_schedules/mock_data.js @@ -5,6 +5,7 @@ import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/ const { data: { + currentUser, project: { pipelineSchedules: { nodes }, }, @@ -28,6 +29,7 @@ const { } = mockGetPipelineSchedulesTakeOwnershipGraphQLResponse; export const mockPipelineScheduleNodes = nodes; +export const mockPipelineScheduleCurrentUser = currentUser; export const mockPipelineScheduleAsGuestNodes = guestNodes; diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js index 90ca2a07266..f7386cfec74 100644 --- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js +++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js @@ -30,11 +30,6 @@ describe('code quality issue body issue body', () => { ); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('severity rating', () => { it.each` severity | iconClass | iconName diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js index 3e4adfc7794..8beec220802 100644 --- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js +++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js @@ -15,10 +15,6 @@ describe('Grouped Issues List', () => { const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a smart virtual list with the correct props', () => { createComponent({ propsData: { diff --git a/spec/frontend/ci/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js index fb13d4407e2..82b655dd598 100644 --- a/spec/frontend/ci/reports/components/issue_status_icon_spec.js +++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js @@ -13,11 +13,6 @@ describe('IssueStatusIcon', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])( 'renders "%s" state correctly', (status) => { diff --git a/spec/frontend/ci/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js index ba541ba0303..4a97afd77df 100644 --- a/spec/frontend/ci/reports/components/report_link_spec.js +++ b/spec/frontend/ci/reports/components/report_link_spec.js @@ -4,10 +4,6 @@ import ReportLink from '~/ci/reports/components/report_link.vue'; describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - const defaultProps = { issue: {}, }; diff --git a/spec/frontend/ci/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js index f032b210184..f4012fe0215 100644 --- a/spec/frontend/ci/reports/components/report_section_spec.js +++ b/spec/frontend/ci/reports/components/report_section_spec.js @@ -49,10 +49,6 @@ describe('ReportSection component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('isCollapsible', () => { const testMatrix = [ diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js index fb2ae5371d5..b1ae9e26b5b 100644 --- a/spec/frontend/ci/reports/components/summary_row_spec.js +++ b/spec/frontend/ci/reports/components/summary_row_spec.js @@ -31,11 +31,6 @@ describe('Summary row', () => { const findStatusIcon = () => wrapper.findByTestId('summary-row-icon'); const findHelpPopover = () => wrapper.findComponent(HelpPopover); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders provided summary', () => { createComponent(); expect(findSummary().text()).toContain(summary); diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js index edf3d1706cc..85b1d3b1b2f 100644 --- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js +++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js @@ -1,40 +1,53 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; + import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue'; +import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; -import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; -import { DEFAULT_PLATFORM } from '~/ci/runner/constants'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { runnerCreateResult } from '../mock_data'; const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN'; Vue.use(VueApollo); +jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage'); +jest.mock('~/alert'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + redirectTo: jest.fn(), +})); + +const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner; + describe('AdminNewRunnerApp', () => { let wrapper; const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link'); const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal); const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup); - const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields); + const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm); - const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { - wrapper = mountFn(AdminNewRunnerApp, { + const createComponent = () => { + wrapper = shallowMountExtended(AdminNewRunnerApp, { propsData: { legacyRegistrationToken: mockLegacyRegistrationToken, - ...props, }, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, stubs: { GlSprintf, }, - ...options, }); }; @@ -56,25 +69,59 @@ describe('AdminNewRunnerApp', () => { }); }); - describe('New runner form fields', () => { - describe('Platform', () => { - it('shows the platforms radio group', () => { - expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM); - }); + describe('Platform', () => { + it('shows the platforms radio group', () => { + expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM); + }); + }); + + describe('Runner form', () => { + it('shows the runner create form', () => { + expect(findRunnerCreateForm().exists()).toBe(true); }); - describe('Runner', () => { - it('shows the runners fields', () => { - expect(findRunnerFormFields().props('value')).toEqual({ - accessLevel: 'NOT_PROTECTED', - paused: false, - description: '', - maintenanceNote: '', - maximumTimeout: ' ', - runUntagged: false, - tagList: '', + describe('When a runner is saved', () => { + beforeEach(() => { + findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner); + }); + + it('pushes an alert to be shown after redirection', () => { + expect(saveAlertToLocalStorage).toHaveBeenCalledWith({ + message: s__('Runners|Runner created.'), + variant: VARIANT_SUCCESS, }); }); + + it('redirects to the registration page', () => { + const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`; + + expect(redirectTo).toHaveBeenCalledWith(url); + }); + }); + + describe('When another platform is selected and a runner is saved', () => { + beforeEach(() => { + findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM); + findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner); + }); + + it('redirects to the registration page with the platform', () => { + const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`; + + expect(redirectTo).toHaveBeenCalledWith(url); + }); + }); + + describe('When runner fails to save', () => { + const ERROR_MSG = 'Cannot save!'; + + beforeEach(() => { + findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG)); + }); + + it('shows an error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG }); + }); }); }); }); diff --git a/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js new file mode 100644 index 00000000000..d04df85d58f --- /dev/null +++ b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js @@ -0,0 +1,122 @@ +import { nextTick } from 'vue'; +import { GlButton } from '@gitlab/ui'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { TEST_HOST } from 'helpers/test_constants'; + +import { updateHistory } from '~/lib/utils/url_utility'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants'; +import AdminRegisterRunnerApp from '~/ci/runner/admin_register_runner/admin_register_runner_app.vue'; +import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue'; +import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue'; +import { runnerForRegistration } from '../mock_data'; + +const mockRunnerId = runnerForRegistration.data.runner.id; +const mockRunnersPath = '/admin/runners'; + +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + updateHistory: jest.fn(), +})); + +describe('AdminRegisterRunnerApp', () => { + let wrapper; + + const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions); + const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer); + const findBtn = () => wrapper.findComponent(GlButton); + + const createComponent = () => { + wrapper = shallowMountExtended(AdminRegisterRunnerApp, { + propsData: { + runnerId: mockRunnerId, + runnersPath: mockRunnersPath, + }, + }); + }; + + describe('When showing runner details', () => { + beforeEach(async () => { + createComponent(); + }); + + describe('when runner token is available', () => { + it('shows registration instructions', () => { + expect(findRegistrationInstructions().props()).toEqual({ + platform: DEFAULT_PLATFORM, + runnerId: mockRunnerId, + }); + }); + + it('configures platform drawer', () => { + expect(findPlatformsDrawer().props()).toEqual({ + open: false, + platform: DEFAULT_PLATFORM, + }); + }); + + it('shows runner list button', () => { + expect(findBtn().attributes('href')).toBe(mockRunnersPath); + expect(findBtn().props('variant')).toBe('confirm'); + }); + }); + }); + + describe('When another platform has been selected', () => { + beforeEach(async () => { + setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`); + + createComponent(); + }); + + it('shows registration instructions for the platform', () => { + expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM); + }); + }); + + describe('When opening install instructions', () => { + beforeEach(async () => { + createComponent(); + + findRegistrationInstructions().vm.$emit('toggleDrawer'); + await nextTick(); + }); + + it('opens platform drawer', () => { + expect(findPlatformsDrawer().props('open')).toBe(true); + }); + + it('closes platform drawer', async () => { + findRegistrationInstructions().vm.$emit('toggleDrawer'); + await nextTick(); + + expect(findPlatformsDrawer().props('open')).toBe(false); + }); + + it('closes platform drawer from drawer', async () => { + findPlatformsDrawer().vm.$emit('close'); + await nextTick(); + + expect(findPlatformsDrawer().props('open')).toBe(false); + }); + + describe('when selecting a platform', () => { + beforeEach(async () => { + findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM); + await nextTick(); + }); + + it('updates the url', () => { + expect(updateHistory).toHaveBeenCalledTimes(1); + expect(updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`, + }); + }); + + it('updates the registration instructions', () => { + expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM); + }); + }); + }); +}); diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js index ed4f43c12d8..9d9142f2c68 100644 --- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al import { runnerData } from '../mock_data'; jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility'); @@ -72,7 +72,6 @@ describe('AdminRunnerShowApp', () => { afterEach(() => { mockRunnerQuery.mockReset(); - wrapper.destroy(); }); describe('When showing runner details', () => { diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js index 7fc240e520b..0cf6241c24f 100644 --- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js @@ -9,7 +9,7 @@ import { mountExtended, } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; @@ -70,7 +70,7 @@ const mockRunnersCount = runnersCountData.data.runners.count; const mockRunnersHandler = jest.fn(); const mockRunnersCountHandler = jest.fn(); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), @@ -143,7 +143,6 @@ describe('AdminRunnersApp', () => { mockRunnersHandler.mockReset(); mockRunnersCountHandler.mockReset(); showToast.mockReset(); - wrapper.destroy(); }); it('shows the runner setup instructions', () => { diff --git a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js index 82e262d1b73..8ac0c5a61f8 100644 --- a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js @@ -31,10 +31,6 @@ describe('RunnerActionsCell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Edit Action', () => { it('Displays the runner edit link with the correct href', () => { createComponent(); diff --git a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js index 3097e43e583..03f1ace3897 100644 --- a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js @@ -16,7 +16,7 @@ describe('RunnerOwnerCell', () => { const createComponent = ({ runner } = {}) => { wrapper = shallowMount(RunnerOwnerCell, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { runner, @@ -24,10 +24,6 @@ describe('RunnerOwnerCell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('When its an instance runner', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js index 1ff60ff1a9d..ec23d8415e8 100644 --- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js @@ -34,10 +34,6 @@ describe('RunnerStatusCell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Displays online status', () => { createComponent(); diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js index 1711df42491..585a03c0811 100644 --- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js @@ -45,10 +45,6 @@ describe('RunnerTypeCell', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Displays the runner name as id and short token', () => { expect(wrapper.text()).toContain( `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`, diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js index f536e0dcbcf..7748890cf77 100644 --- a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js @@ -17,16 +17,12 @@ describe('RunnerSummaryField', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, ...options, }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('shows content in slot', () => { createComponent({ slots: { default: 'content' }, diff --git a/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap new file mode 100644 index 00000000000..09d032fd32d --- /dev/null +++ b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`registration utils for "linux" platform commandPrompt is correct 1`] = `"$"`; + +exports[`registration utils for "linux" platform installScript is correct for "386" architecture 1`] = ` +"# Download the binary for your system +sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386 + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# Create a GitLab Runner user +sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash + +# Install and run as a service +sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner +sudo gitlab-runner start" +`; + +exports[`registration utils for "linux" platform installScript is correct for "amd64" architecture 1`] = ` +"# Download the binary for your system +sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# Create a GitLab Runner user +sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash + +# Install and run as a service +sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner +sudo gitlab-runner start" +`; + +exports[`registration utils for "linux" platform installScript is correct for "arm" architecture 1`] = ` +"# Download the binary for your system +sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# Create a GitLab Runner user +sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash + +# Install and run as a service +sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner +sudo gitlab-runner start" +`; + +exports[`registration utils for "linux" platform installScript is correct for "arm64" architecture 1`] = ` +"# Download the binary for your system +sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64 + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# Create a GitLab Runner user +sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash + +# Install and run as a service +sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner +sudo gitlab-runner start" +`; + +exports[`registration utils for "linux" platform platformArchitectures returns correct list of architectures 1`] = ` +Array [ + "amd64", + "386", + "arm", + "arm64", +] +`; + +exports[`registration utils for "linux" platform registerCommand is correct 1`] = ` +Array [ + "gitlab-runner register", + " --url http://test.host", + " --registration-token REGISTRATION_TOKEN", + " --description 'RUNNER'", +] +`; + +exports[`registration utils for "linux" platform registerCommand is correct 2`] = ` +Array [ + "gitlab-runner register", + " --url http://test.host", +] +`; + +exports[`registration utils for "linux" platform runCommand is correct 1`] = `"gitlab-runner run"`; + +exports[`registration utils for "osx" platform commandPrompt is correct 1`] = `"$"`; + +exports[`registration utils for "osx" platform installScript is correct for "amd64" architecture 1`] = ` +"# Download the binary for your system +sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64 + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# The rest of the commands execute as the user who will run the runner +# Register the runner (steps below), then run +cd ~ +gitlab-runner install +gitlab-runner start" +`; + +exports[`registration utils for "osx" platform installScript is correct for "arm64" architecture 1`] = ` +"# Download the binary for your system +sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64 + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# The rest of the commands execute as the user who will run the runner +# Register the runner (steps below), then run +cd ~ +gitlab-runner install +gitlab-runner start" +`; + +exports[`registration utils for "osx" platform platformArchitectures returns correct list of architectures 1`] = ` +Array [ + "amd64", + "arm64", +] +`; + +exports[`registration utils for "osx" platform registerCommand is correct 1`] = ` +Array [ + "gitlab-runner register", + " --url http://test.host", + " --registration-token REGISTRATION_TOKEN", + " --description 'RUNNER'", +] +`; + +exports[`registration utils for "osx" platform registerCommand is correct 2`] = ` +Array [ + "gitlab-runner register", + " --url http://test.host", +] +`; + +exports[`registration utils for "osx" platform runCommand is correct 1`] = `"gitlab-runner run"`; + +exports[`registration utils for "windows" platform commandPrompt is correct 1`] = `">"`; + +exports[`registration utils for "windows" platform installScript is correct for "386" architecture 1`] = ` +"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator +# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner +New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory + +# Change to the folder +cd 'C:\\\\GitLab-Runner' + +# Download binary +Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe\\" -OutFile \\"gitlab-runner.exe\\" + +# Register the runner (steps below), then run +.\\\\gitlab-runner.exe install +.\\\\gitlab-runner.exe start" +`; + +exports[`registration utils for "windows" platform installScript is correct for "amd64" architecture 1`] = ` +"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator +# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner +New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory + +# Change to the folder +cd 'C:\\\\GitLab-Runner' + +# Download binary +Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe\\" -OutFile \\"gitlab-runner.exe\\" + +# Register the runner (steps below), then run +.\\\\gitlab-runner.exe install +.\\\\gitlab-runner.exe start" +`; + +exports[`registration utils for "windows" platform platformArchitectures returns correct list of architectures 1`] = ` +Array [ + "amd64", + "386", +] +`; + +exports[`registration utils for "windows" platform registerCommand is correct 1`] = ` +Array [ + ".\\\\gitlab-runner.exe register", + " --url http://test.host", + " --registration-token REGISTRATION_TOKEN", + " --description 'RUNNER'", +] +`; + +exports[`registration utils for "windows" platform registerCommand is correct 2`] = ` +Array [ + ".\\\\gitlab-runner.exe register", + " --url http://test.host", +] +`; + +exports[`registration utils for "windows" platform runCommand is correct 1`] = `".\\\\gitlab-runner.exe run"`; diff --git a/spec/frontend/ci/runner/components/registration/cli_command_spec.js b/spec/frontend/ci/runner/components/registration/cli_command_spec.js new file mode 100644 index 00000000000..78c2b94c3ea --- /dev/null +++ b/spec/frontend/ci/runner/components/registration/cli_command_spec.js @@ -0,0 +1,39 @@ +import CliCommand from '~/ci/runner/components/registration/cli_command.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('CliCommand', () => { + let wrapper; + + // use .textContent instead of .text() to capture whitespace that's visible in <pre> + const getPreTextContent = () => wrapper.find('pre').element.textContent; + const getClipboardText = () => wrapper.findComponent(ClipboardButton).props('text'); + + const createComponent = (props) => { + wrapper = shallowMountExtended(CliCommand, { + propsData: { + ...props, + }, + }); + }; + + it('when rendering a command', () => { + createComponent({ + prompt: '#', + command: 'echo hi', + }); + + expect(getPreTextContent()).toBe('# echo hi'); + expect(getClipboardText()).toBe('echo hi'); + }); + + it('when rendering a multi-line command', () => { + createComponent({ + prompt: '#', + command: ['git', ' --version'], + }); + + expect(getPreTextContent()).toBe('# git --version'); + expect(getClipboardText()).toBe('git --version'); + }); +}); diff --git a/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js new file mode 100644 index 00000000000..0b438455b5b --- /dev/null +++ b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js @@ -0,0 +1,108 @@ +import { nextTick } from 'vue'; +import { GlDrawer, GlLink, GlIcon, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; + +import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue'; +import CliCommand from '~/ci/runner/components/registration/cli_command.vue'; +import { + LINUX_PLATFORM, + MACOS_PLATFORM, + WINDOWS_PLATFORM, + INSTALL_HELP_URL, +} from '~/ci/runner/constants'; +import { installScript, platformArchitectures } from '~/ci/runner/components/registration/utils'; + +const MOCK_WRAPPER_HEIGHT = '99px'; +const LINUX_ARCHS = platformArchitectures({ platform: LINUX_PLATFORM }); +const MACOS_ARCHS = platformArchitectures({ platform: MACOS_PLATFORM }); + +jest.mock('~/lib/utils/dom_utils', () => ({ + getContentWrapperHeight: () => MOCK_WRAPPER_HEIGHT, +})); + +describe('RegistrationInstructions', () => { + let wrapper; + + const findDrawer = () => wrapper.findComponent(GlDrawer); + const findEnvironmentOptions = () => + wrapper.findByLabelText(s__('Runners|Environment')).findAll('option'); + const findArchitectureOptions = () => + wrapper.findByLabelText(s__('Runners|Architecture')).findAll('option'); + const findCliCommand = () => wrapper.findComponent(CliCommand); + const findLink = () => wrapper.findComponent(GlLink); + + const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { + wrapper = mountFn(PlatformsDrawer, { + propsData: { + open: true, + ...props, + }, + stubs: { + GlSprintf, + }, + }); + }; + + it('shows drawer', () => { + createComponent(); + + expect(findDrawer().props()).toMatchObject({ + open: true, + headerHeight: MOCK_WRAPPER_HEIGHT, + }); + }); + + it('closes drawer', () => { + createComponent(); + findDrawer().vm.$emit('close'); + + expect(wrapper.emitted('close')).toHaveLength(1); + }); + + it('shows selection options', () => { + createComponent({ mountFn: mountExtended }); + + expect(findEnvironmentOptions().wrappers.map((w) => w.attributes('value'))).toEqual([ + LINUX_PLATFORM, + MACOS_PLATFORM, + WINDOWS_PLATFORM, + ]); + + expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual( + LINUX_ARCHS, + ); + }); + + it('shows script', () => { + createComponent(); + + expect(findCliCommand().props('command')).toBe( + installScript({ platform: LINUX_PLATFORM, architecture: LINUX_ARCHS[0] }), + ); + }); + + it('shows selection options for another platform', async () => { + createComponent({ mountFn: mountExtended }); + + findEnvironmentOptions().at(1).setSelected(); // macos + await nextTick(); + + expect(wrapper.emitted('selectPlatform')).toEqual([[MACOS_PLATFORM]]); + + expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual( + MACOS_ARCHS, + ); + + expect(findCliCommand().props('command')).toBe( + installScript({ platform: MACOS_PLATFORM, architecture: MACOS_ARCHS[0] }), + ); + }); + + it('shows external link for more information', () => { + createComponent(); + + expect(findLink().attributes('href')).toBe(INSTALL_HELP_URL); + expect(findLink().findComponent(GlIcon).props('name')).toBe('external-link'); + }); +}); diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js index 0daaca9c4ff..9ed59b0a57d 100644 --- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js @@ -116,10 +116,6 @@ describe('RegistrationDropdown', () => { await openModal(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('opens the modal with contents', () => { const modalText = findModalContent(); diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js new file mode 100644 index 00000000000..eb4b659091d --- /dev/null +++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js @@ -0,0 +1,293 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { TEST_HOST } from 'helpers/test_constants'; + +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue'; +import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql'; +import CliCommand from '~/ci/runner/components/registration/cli_command.vue'; +import { + DEFAULT_PLATFORM, + EXECUTORS_HELP_URL, + SERVICE_COMMANDS_HELP_URL, + STATUS_NEVER_CONTACTED, + STATUS_ONLINE, + RUNNER_REGISTRATION_POLLING_INTERVAL_MS, + I18N_REGISTRATION_SUCCESS, +} from '~/ci/runner/constants'; +import { runnerForRegistration } from '../../mock_data'; + +Vue.use(VueApollo); + +const MOCK_TOKEN = 'MOCK_TOKEN'; +const mockDescription = runnerForRegistration.data.runner.description; + +const mockRunner = { + ...runnerForRegistration.data.runner, + ephemeralAuthenticationToken: MOCK_TOKEN, +}; +const mockRunnerWithoutToken = { + ...runnerForRegistration.data.runner, + ephemeralAuthenticationToken: null, +}; + +const mockRunnerId = `${getIdFromGraphQLId(mockRunner.id)}`; + +describe('RegistrationInstructions', () => { + let wrapper; + let mockRunnerQuery; + + const findHeading = () => wrapper.find('h1'); + const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i)); + const findByText = (text, container = wrapper) => container.findByText(text); + + const waitForPolling = async () => { + jest.advanceTimersByTime(RUNNER_REGISTRATION_POLLING_INTERVAL_MS); + await waitForPromises(); + }; + + const mockResolvedRunner = (runner = mockRunner) => { + mockRunnerQuery.mockResolvedValue({ + data: { + runner, + }, + }); + }; + + const createComponent = (props) => { + wrapper = shallowMountExtended(RegistrationInstructions, { + apolloProvider: createMockApollo([[runnerForRegistrationQuery, mockRunnerQuery]]), + propsData: { + runnerId: mockRunnerId, + platform: DEFAULT_PLATFORM, + ...props, + }, + stubs: { + GlSprintf, + }, + }); + }; + + beforeEach(() => { + mockRunnerQuery = jest.fn(); + mockResolvedRunner(); + }); + + beforeEach(() => { + window.gon.gitlab_url = TEST_HOST; + }); + + it('loads runner with id', async () => { + createComponent(); + + expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id }); + }); + + describe('heading', () => { + it('when runner is loaded, shows heading', async () => { + createComponent(); + await waitForPromises(); + + expect(findHeading().text()).toContain(mockRunner.description); + }); + + it('when runner is loaded, shows heading safely', async () => { + mockResolvedRunner({ + ...mockRunner, + description: '<script>hacked();</script>', + }); + + createComponent(); + await waitForPromises(); + + expect(findHeading().text()).toBe('Register "<script>hacked();</script>" runner'); + expect(findHeading().element.innerHTML).toBe( + 'Register "<script>hacked();</script>" runner', + ); + }); + + it('when runner is loading, shows default heading', () => { + createComponent(); + + expect(findHeading().text()).toBe(s__('Runners|Register runner')); + }); + }); + + it('renders legacy instructions', () => { + createComponent(); + + findByText('How do I install GitLab Runner?').vm.$emit('click'); + + expect(wrapper.emitted('toggleDrawer')).toHaveLength(1); + }); + + describe('step 1', () => { + it('renders step 1', async () => { + createComponent(); + await waitForPromises(); + + const step1 = findStepAt(0); + + expect(step1.findComponent(CliCommand).props()).toEqual({ + command: [ + 'gitlab-runner register', + ` --url ${TEST_HOST}`, + ` --registration-token ${MOCK_TOKEN}`, + ` --description '${mockDescription}'`, + ], + prompt: '$', + }); + expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN); + expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN); + }); + + it('renders step 1 in loading state', () => { + createComponent(); + + const step1 = findStepAt(0); + + expect(step1.findComponent(GlSkeletonLoader).exists()).toBe(true); + expect(step1.find('code').exists()).toBe(false); + expect(step1.findComponent(ClipboardButton).exists()).toBe(false); + }); + + it('render step 1 after token is not visible', async () => { + mockResolvedRunner(mockRunnerWithoutToken); + + createComponent(); + await waitForPromises(); + + const step1 = findStepAt(0); + + expect(step1.findComponent(CliCommand).props('command')).toEqual([ + 'gitlab-runner register', + ` --url ${TEST_HOST}`, + ` --description '${mockDescription}'`, + ]); + expect(step1.find('[data-testid="runner-token"]').exists()).toBe(false); + expect(step1.findComponent(ClipboardButton).exists()).toBe(false); + }); + + describe('polling for changes', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('fetches data', () => { + expect(mockRunnerQuery).toHaveBeenCalledTimes(1); + }); + + it('polls', async () => { + await waitForPolling(); + expect(mockRunnerQuery).toHaveBeenCalledTimes(2); + + await waitForPolling(); + expect(mockRunnerQuery).toHaveBeenCalledTimes(3); + }); + + it('when runner is online, stops polling', async () => { + mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE }); + await waitForPolling(); + + expect(mockRunnerQuery).toHaveBeenCalledTimes(2); + await waitForPolling(); + + expect(mockRunnerQuery).toHaveBeenCalledTimes(2); + }); + + it('when token is no longer visible in the API, it is still visible in the UI', async () => { + mockResolvedRunner(mockRunnerWithoutToken); + await waitForPolling(); + + const step1 = findStepAt(0); + expect(step1.findComponent(CliCommand).props('command')).toEqual([ + 'gitlab-runner register', + ` --url ${TEST_HOST}`, + ` --registration-token ${MOCK_TOKEN}`, + ` --description '${mockDescription}'`, + ]); + expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN); + expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN); + }); + + it('when runner is not available (e.g. deleted), the UI does not update', async () => { + mockResolvedRunner(null); + await waitForPolling(); + + const step1 = findStepAt(0); + expect(step1.findComponent(CliCommand).props('command')).toEqual([ + 'gitlab-runner register', + ` --url ${TEST_HOST}`, + ` --registration-token ${MOCK_TOKEN}`, + ` --description '${mockDescription}'`, + ]); + expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN); + expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN); + }); + }); + }); + + it('renders step 2', () => { + createComponent(); + const step2 = findStepAt(1); + + expect(findByText('Not sure which one to select?', step2).attributes('href')).toBe( + EXECUTORS_HELP_URL, + ); + }); + + it('renders step 3', () => { + createComponent(); + const step3 = findStepAt(2); + + expect(step3.findComponent(CliCommand).props()).toEqual({ + command: 'gitlab-runner run', + prompt: '$', + }); + + expect(findByText('system or user service', step3).attributes('href')).toBe( + SERVICE_COMMANDS_HELP_URL, + ); + }); + + describe('success state', () => { + describe('when the runner has not been registered', () => { + beforeEach(async () => { + createComponent(); + + await waitForPolling(); + + mockResolvedRunner({ ...mockRunner, status: STATUS_NEVER_CONTACTED }); + + await waitForPolling(); + }); + + it('does not show success message', () => { + expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS); + }); + }); + + describe('when the runner has been registered', () => { + beforeEach(async () => { + createComponent(); + await waitForPolling(); + + mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE }); + await waitForPolling(); + }); + + it('shows success message', () => { + expect(wrapper.text()).toContain('🎉'); + expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS); + }); + }); + }); +}); diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js index 783a4d9252a..ff69fd6d3d6 100644 --- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js @@ -5,14 +5,14 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants'; import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql'; import { captureException } from '~/ci/runner/sentry_utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); Vue.use(VueApollo); @@ -43,7 +43,7 @@ describe('RegistrationTokenResetDropdownItem', () => { [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler], ]), directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, }); @@ -63,10 +63,6 @@ describe('RegistrationTokenResetDropdownItem', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Displays reset button', () => { expect(findDropdownItem().exists()).toBe(true); }); diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js index d2a51c0d910..4f44e6e10b2 100644 --- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js @@ -27,10 +27,6 @@ describe('RegistrationToken', () => { showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; }; - afterEach(() => { - wrapper.destroy(); - }); - it('Displays value and copy button', () => { createComponent(); diff --git a/spec/frontend/ci/runner/components/registration/utils_spec.js b/spec/frontend/ci/runner/components/registration/utils_spec.js new file mode 100644 index 00000000000..acf5993b15b --- /dev/null +++ b/spec/frontend/ci/runner/components/registration/utils_spec.js @@ -0,0 +1,118 @@ +import { TEST_HOST } from 'helpers/test_constants'; +import { + DEFAULT_PLATFORM, + LINUX_PLATFORM, + MACOS_PLATFORM, + WINDOWS_PLATFORM, +} from '~/ci/runner/constants'; + +import { + commandPrompt, + registerCommand, + runCommand, + installScript, + platformArchitectures, +} from '~/ci/runner/components/registration/utils'; + +const REGISTRATION_TOKEN = 'REGISTRATION_TOKEN'; +const DESCRIPTION = 'RUNNER'; + +describe('registration utils', () => { + beforeEach(() => { + window.gon.gitlab_url = TEST_HOST; + }); + + describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])( + 'for "%s" platform', + (platform) => { + it('commandPrompt is correct', () => { + expect(commandPrompt({ platform })).toMatchSnapshot(); + }); + + it('registerCommand is correct', () => { + expect( + registerCommand({ + platform, + registrationToken: REGISTRATION_TOKEN, + description: DESCRIPTION, + }), + ).toMatchSnapshot(); + + expect(registerCommand({ platform })).toMatchSnapshot(); + }); + + it('runCommand is correct', () => { + expect(runCommand({ platform })).toMatchSnapshot(); + }); + }, + ); + + describe.each([LINUX_PLATFORM, MACOS_PLATFORM])('for "%s" platform', (platform) => { + it.each` + description | parameter + ${'my runner'} | ${"'my runner'"} + ${"bob's runner"} | ${"'bob'\\''s runner'"} + `('registerCommand escapes description `$description`', ({ description, parameter }) => { + expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`); + }); + }); + + describe.each([WINDOWS_PLATFORM])('for "%s" platform', (platform) => { + it.each` + description | parameter + ${'my runner'} | ${"'my runner'"} + ${"bob's runner"} | ${"'bob''s runner'"} + `('registerCommand escapes description `$description`', ({ description, parameter }) => { + expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`); + }); + }); + + describe('for missing platform', () => { + it('commandPrompt uses the default', () => { + const expected = commandPrompt({ platform: DEFAULT_PLATFORM }); + + expect(commandPrompt({ platform: null })).toEqual(expected); + expect(commandPrompt({ platform: undefined })).toEqual(expected); + }); + + it('registerCommand uses the default', () => { + const expected = registerCommand({ + platform: DEFAULT_PLATFORM, + registrationToken: REGISTRATION_TOKEN, + }); + + expect(registerCommand({ platform: null, registrationToken: REGISTRATION_TOKEN })).toEqual( + expected, + ); + expect( + registerCommand({ platform: undefined, registrationToken: REGISTRATION_TOKEN }), + ).toEqual(expected); + }); + + it('runCommand uses the default', () => { + const expected = runCommand({ platform: DEFAULT_PLATFORM }); + + expect(runCommand({ platform: null })).toEqual(expected); + expect(runCommand({ platform: undefined })).toEqual(expected); + }); + }); + + describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])( + 'for "%s" platform', + (platform) => { + describe('platformArchitectures', () => { + it('returns correct list of architectures', () => { + expect(platformArchitectures({ platform })).toMatchSnapshot(); + }); + }); + + describe('installScript', () => { + const architectures = platformArchitectures({ platform }); + + it.each(architectures)('is correct for "%s" architecture', (architecture) => { + expect(installScript({ platform, architecture })).toMatchSnapshot(); + }); + }); + }, + ); +}); diff --git a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js index 5df2e04c340..a1fd9e4c1aa 100644 --- a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js +++ b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js @@ -33,10 +33,6 @@ describe('RunnerAssignedItem', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Shows an avatar', () => { const avatar = findAvatar(); diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js index 0dc5a90fb83..f609c6be41a 100644 --- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js +++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { makeVar } from '@apollo/client/core'; import { GlModal, GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { s__ } from '~/locale'; @@ -15,7 +15,7 @@ import { allRunnersData } from '../mock_data'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('RunnerBulkDelete', () => { let wrapper; @@ -51,7 +51,7 @@ describe('RunnerBulkDelete', () => { runners: mockRunners, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { GlSprintf, diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js new file mode 100644 index 00000000000..1123a026a4d --- /dev/null +++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js @@ -0,0 +1,170 @@ +import Vue from 'vue'; +import { GlForm } from '@gitlab/ui'; +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 RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; +import { DEFAULT_ACCESS_LEVEL } from '~/ci/runner/constants'; +import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql'; +import { captureException } from '~/ci/runner/sentry_utils'; +import { runnerCreateResult } from '../mock_data'; + +jest.mock('~/ci/runner/sentry_utils'); + +const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner; + +const defaultRunnerModel = { + description: '', + accessLevel: DEFAULT_ACCESS_LEVEL, + paused: false, + maintenanceNote: '', + maximumTimeout: '', + runUntagged: false, + tagList: '', +}; + +Vue.use(VueApollo); + +describe('RunnerCreateForm', () => { + let wrapper; + let runnerCreateHandler; + + const findForm = () => wrapper.findComponent(GlForm); + const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields); + const findSubmitBtn = () => wrapper.find('[type="submit"]'); + + const createComponent = () => { + wrapper = shallowMountExtended(RunnerCreateForm, { + apolloProvider: createMockApollo([[runnerCreateMutation, runnerCreateHandler]]), + }); + }; + + beforeEach(() => { + runnerCreateHandler = jest.fn().mockResolvedValue(runnerCreateResult); + + createComponent(); + }); + + it('shows default runner values', () => { + expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel); + }); + + it('shows a submit button', () => { + expect(findSubmitBtn().exists()).toBe(true); + }); + + describe('when user submits', () => { + let preventDefault; + + beforeEach(() => { + preventDefault = jest.fn(); + + findRunnerFormFields().vm.$emit('input', { + ...defaultRunnerModel, + description: 'My runner', + maximumTimeout: 0, + tagList: 'tag1, tag2', + }); + }); + + describe('immediately after submit', () => { + beforeEach(() => { + findForm().vm.$emit('submit', { preventDefault }); + }); + + it('prevents default form submission', () => { + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + it('shows a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(true); + }); + + it('saves runner', async () => { + expect(runnerCreateHandler).toHaveBeenCalledWith({ + input: { + ...defaultRunnerModel, + description: 'My runner', + maximumTimeout: 0, + tagList: ['tag1', 'tag2'], + }, + }); + }); + }); + + describe('when saved successfully', () => { + beforeEach(async () => { + findForm().vm.$emit('submit', { preventDefault }); + await waitForPromises(); + }); + + it('emits "saved" result', async () => { + expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]); + }); + + it('does not show a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(false); + }); + }); + + describe('when a server error occurs', () => { + const error = new Error('Error!'); + + beforeEach(async () => { + runnerCreateHandler.mockRejectedValue(error); + + findForm().vm.$emit('submit', { preventDefault }); + await waitForPromises(); + }); + + it('emits "error" result', async () => { + expect(wrapper.emitted('error')[0]).toEqual([error]); + }); + + it('does not show a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(false); + }); + + it('reports error', () => { + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith({ + component: 'RunnerCreateForm', + error, + }); + }); + }); + + describe('when a validation error occurs', () => { + const errorMsg1 = 'Issue1!'; + const errorMsg2 = 'Issue2!'; + + beforeEach(async () => { + runnerCreateHandler.mockResolvedValue({ + data: { + runnerCreate: { + errors: [errorMsg1, errorMsg2], + runner: null, + }, + }, + }); + + findForm().vm.$emit('submit', { preventDefault }); + await waitForPromises(); + }); + + it('emits "error" results', async () => { + expect(wrapper.emitted('error')[0]).toEqual([new Error(`${errorMsg1} ${errorMsg2}`)]); + }); + + it('does not show a saving state', () => { + expect(findSubmitBtn().props('loading')).toBe(false); + }); + + it('does not report error', () => { + expect(captureException).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js index 02960ad427e..f9bea318d84 100644 --- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js @@ -8,7 +8,7 @@ import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutat import waitForPromises from 'helpers/wait_for_promises'; import { captureException } from '~/ci/runner/sentry_utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { I18N_DELETE_RUNNER } from '~/ci/runner/constants'; import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue'; @@ -21,7 +21,7 @@ const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); describe('RunnerDeleteButton', () => { @@ -53,8 +53,8 @@ describe('RunnerDeleteButton', () => { }, apolloProvider, directives: { - GlTooltip: createMockDirective(), - GlModal: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), + GlModal: createMockDirective('gl-modal'), }, }); }; @@ -83,10 +83,6 @@ describe('RunnerDeleteButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Displays a delete button without an icon', () => { expect(findBtn().props()).toMatchObject({ loading: false, diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js index 65a81973869..c2d9e86aa91 100644 --- a/spec/frontend/ci/runner/components/runner_details_spec.js +++ b/spec/frontend/ci/runner/components/runner_details_spec.js @@ -37,10 +37,6 @@ describe('RunnerDetails', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Details tab', () => { describe.each` field | runner | expectedValue diff --git a/spec/frontend/ci/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js index 907cdc90100..5cc1ee049f4 100644 --- a/spec/frontend/ci/runner/components/runner_edit_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js @@ -11,7 +11,7 @@ describe('RunnerEditButton', () => { wrapper = mountFn(RunnerEditButton, { attrs, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -20,10 +20,6 @@ describe('RunnerEditButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Displays Edit text', () => { expect(wrapper.attributes('aria-label')).toBe('Edit'); }); diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js index 408750e646f..ac84c7898bf 100644 --- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js @@ -65,10 +65,6 @@ describe('RunnerList', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('binds a namespace to the filtered search', () => { expect(findFilteredSearch().props('namespace')).toBe('runners'); }); diff --git a/spec/frontend/ci/runner/components/runner_groups_spec.js b/spec/frontend/ci/runner/components/runner_groups_spec.js index 0991feb2e55..e4f5f55ab4b 100644 --- a/spec/frontend/ci/runner/components/runner_groups_spec.js +++ b/spec/frontend/ci/runner/components/runner_groups_spec.js @@ -23,10 +23,6 @@ describe('RunnerGroups', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Shows a heading', () => { createComponent(); diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js index abe3b47767e..c851966431d 100644 --- a/spec/frontend/ci/runner/components/runner_header_spec.js +++ b/spec/frontend/ci/runner/components/runner_header_spec.js @@ -42,10 +42,6 @@ describe('RunnerHeader', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('displays the runner status', () => { createComponent({ mountFn: mountExtended, diff --git a/spec/frontend/ci/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js index bdb8a4a31a3..365b0f1f5ba 100644 --- a/spec/frontend/ci/runner/components/runner_jobs_spec.js +++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js @@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import RunnerJobs from '~/ci/runner/components/runner_jobs.vue'; import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue'; import RunnerPagination from '~/ci/runner/components/runner_pagination.vue'; @@ -15,7 +15,7 @@ import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql' import { runnerData, runnerJobsData } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); const mockRunner = runnerData.data.runner; @@ -47,7 +47,6 @@ describe('RunnerJobs', () => { afterEach(() => { mockRunnerJobsQuery.mockReset(); - wrapper.destroy(); }); it('Requests runner jobs', async () => { diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js index 281aa1aeb77..694c5a6ed17 100644 --- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js +++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js @@ -37,10 +37,6 @@ describe('RunnerJobsTable', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Sets job id as a row key', () => { createComponent(); diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js index 6aea3ddf58c..3e813723b5b 100644 --- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js +++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js @@ -31,7 +31,7 @@ describe('RunnerListEmptyState', () => { ...props, }, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, stubs: { GlEmptyState, @@ -62,11 +62,11 @@ describe('RunnerListEmptyState', () => { expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); }); - describe('when create_runner_workflow is enabled', () => { + describe('when create_runner_workflow_for_admin is enabled', () => { beforeEach(() => { createComponent({ provide: { - glFeatures: { createRunnerWorkflow: true }, + glFeatures: { createRunnerWorkflowForAdmin: true }, }, }); }); @@ -76,14 +76,14 @@ describe('RunnerListEmptyState', () => { }); }); - describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => { + describe('when create_runner_workflow_for_admin is enabled and newRunnerPath not defined', () => { beforeEach(() => { createComponent({ props: { newRunnerPath: null, }, provide: { - glFeatures: { createRunnerWorkflow: true }, + glFeatures: { createRunnerWorkflowForAdmin: true }, }, }); }); @@ -95,11 +95,11 @@ describe('RunnerListEmptyState', () => { }); }); - describe('when create_runner_workflow is disabled', () => { + describe('when create_runner_workflow_for_admin is disabled', () => { beforeEach(() => { createComponent({ provide: { - glFeatures: { createRunnerWorkflow: false }, + glFeatures: { createRunnerWorkflowForAdmin: false }, }, }); }); diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js index 2e5d1dbd063..6f4913dca3e 100644 --- a/spec/frontend/ci/runner/components/runner_list_spec.js +++ b/spec/frontend/ci/runner/components/runner_list_spec.js @@ -57,10 +57,6 @@ describe('RunnerList', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Displays headers', () => { createComponent( { diff --git a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js index f089becd400..7ff3ec92042 100644 --- a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js +++ b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js @@ -18,10 +18,6 @@ describe('RunnerMembershipToggle', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Displays text', () => { createComponent({ mountFn: mount }); diff --git a/spec/frontend/ci/runner/components/runner_pagination_spec.js b/spec/frontend/ci/runner/components/runner_pagination_spec.js index f835ee4514d..6d84eb810f8 100644 --- a/spec/frontend/ci/runner/components/runner_pagination_spec.js +++ b/spec/frontend/ci/runner/components/runner_pagination_spec.js @@ -16,10 +16,6 @@ describe('RunnerPagination', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('When in between pages', () => { const mockPageInfo = { startCursor: mockStartCursor, diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js index 12680e01b98..62e6cc902b7 100644 --- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js @@ -7,7 +7,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql'; import waitForPromises from 'helpers/wait_for_promises'; import { captureException } from '~/ci/runner/sentry_utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, @@ -22,7 +22,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0]; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); describe('RunnerPauseButton', () => { @@ -46,7 +46,7 @@ describe('RunnerPauseButton', () => { }, apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]), directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -74,10 +74,6 @@ describe('RunnerPauseButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('Pause/Resume action', () => { describe.each` runnerState | icon | content | tooltip | isActive | newActiveValue diff --git a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js index b051ebe99a7..54768ea50da 100644 --- a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js +++ b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js @@ -16,7 +16,7 @@ describe('RunnerTypeBadge', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -25,10 +25,6 @@ describe('RunnerTypeBadge', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders paused state', () => { expect(wrapper.text()).toBe(I18N_PAUSED); expect(findBadge().props('variant')).toBe('warning'); diff --git a/spec/frontend/ci/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js index 17517c4db66..ccc1bc18675 100644 --- a/spec/frontend/ci/runner/components/runner_projects_spec.js +++ b/spec/frontend/ci/runner/components/runner_projects_spec.js @@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { sprintf } from '~/locale'; import { I18N_ASSIGNED_PROJECTS, @@ -22,7 +22,7 @@ import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query. import { runnerData, runnerProjectsData } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); const mockRunner = runnerData.data.runner; @@ -56,7 +56,6 @@ describe('RunnerProjects', () => { afterEach(() => { mockRunnerProjectsQuery.mockReset(); - wrapper.destroy(); }); it('Requests runner projects', async () => { diff --git a/spec/frontend/ci/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js index 45b410df2d4..e1eb81f2d23 100644 --- a/spec/frontend/ci/runner/components/runner_status_badge_spec.js +++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js @@ -31,7 +31,7 @@ describe('RunnerTypeBadge', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -43,8 +43,6 @@ describe('RunnerTypeBadge', () => { afterEach(() => { jest.useFakeTimers({ legacyFakeTimers: true }); - - wrapper.destroy(); }); it('renders online state', () => { diff --git a/spec/frontend/ci/runner/components/runner_tag_spec.js b/spec/frontend/ci/runner/components/runner_tag_spec.js index 7bcb046ae43..e3d46e5d6df 100644 --- a/spec/frontend/ci/runner/components/runner_tag_spec.js +++ b/spec/frontend/ci/runner/components/runner_tag_spec.js @@ -29,8 +29,8 @@ describe('RunnerTag', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), - GlResizeObserver: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), + GlResizeObserver: createMockDirective('gl-resize-observer'), }, }); }; @@ -39,10 +39,6 @@ describe('RunnerTag', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Displays tag text', () => { expect(wrapper.text()).toBe(mockTag); }); diff --git a/spec/frontend/ci/runner/components/runner_tags_spec.js b/spec/frontend/ci/runner/components/runner_tags_spec.js index 96bec00302b..bcb1d1f9e13 100644 --- a/spec/frontend/ci/runner/components/runner_tags_spec.js +++ b/spec/frontend/ci/runner/components/runner_tags_spec.js @@ -21,10 +21,6 @@ describe('RunnerTags', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Displays tags text', () => { expect(wrapper.text()).toMatchInterpolatedText('tag1 tag2'); diff --git a/spec/frontend/ci/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js index 58f09362759..7a0fb6f69ea 100644 --- a/spec/frontend/ci/runner/components/runner_type_badge_spec.js +++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js @@ -23,15 +23,11 @@ describe('RunnerTypeBadge', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe.each` type | text ${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE} diff --git a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js index 3347c190083..6e15c84ad7e 100644 --- a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js +++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js @@ -63,10 +63,6 @@ describe('RunnerTypeTabs', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Renders all options to filter runners by default', () => { createComponent(); diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js index a0e51ebf958..620e2f85890 100644 --- a/spec/frontend/ci/runner/components/runner_update_form_spec.js +++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js @@ -5,7 +5,7 @@ import { __ } from '~/locale'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue'; import { @@ -21,7 +21,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al import { runnerFormData } from '../mock_data'; jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility'); @@ -107,10 +107,6 @@ describe('RunnerUpdateForm', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Form has a submit button', () => { expect(findSubmit().exists()).toBe(true); }); diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js index b7d9d3ad23e..e9f2e888b9a 100644 --- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js +++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js @@ -3,14 +3,14 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue'; import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants'; import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ ...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'), @@ -90,7 +90,6 @@ describe('TagToken', () => { afterEach(() => { getRecentlyUsedSuggestions.mockReset(); - wrapper.destroy(); }); describe('when the tags token is displayed', () => { diff --git a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js index cad61f26012..f30b75ee614 100644 --- a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js +++ b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js @@ -32,10 +32,6 @@ describe('RunnerStats', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` case | count | value ${'number'} | ${99} | ${'99'} diff --git a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js index 3d45674d106..13366a788d5 100644 --- a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js +++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js @@ -47,10 +47,6 @@ describe('RunnerStats', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('Displays all the stats', () => { createComponent({ mountFn: mount, diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js index 2ad31dea774..fadc6e5ebc5 100644 --- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js +++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al import { runnerData } from '../mock_data'; jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility'); @@ -74,7 +74,6 @@ describe('GroupRunnerShowApp', () => { afterEach(() => { mockRunnerQuery.mockReset(); - wrapper.destroy(); }); describe('When showing runner details', () => { diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js index 39ea5cade28..00c7262e38b 100644 --- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js @@ -9,7 +9,7 @@ import { mountExtended, } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; @@ -74,7 +74,7 @@ const mockGroupRunnersCount = mockGroupRunnersEdges.length; const mockGroupRunnersHandler = jest.fn(); const mockGroupRunnersCountHandler = jest.fn(); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), @@ -138,7 +138,6 @@ describe('GroupRunnersApp', () => { afterEach(() => { mockGroupRunnersHandler.mockReset(); mockGroupRunnersCountHandler.mockReset(); - wrapper.destroy(); }); it('shows the runner tabs with a runner count for each type', async () => { diff --git a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js index 03908891cfd..30e49fc7644 100644 --- a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js +++ b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js @@ -2,9 +2,9 @@ import AccessorUtilities from '~/lib/utils/accessor'; import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage'; import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('showAlertFromLocalStorage', () => { useLocalStorageSpy(); diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js index 5cdf0ea4e3b..092a419c1fe 100644 --- a/spec/frontend/ci/runner/mock_data.js +++ b/spec/frontend/ci/runner/mock_data.js @@ -1,6 +1,10 @@ // Fixtures generated by: spec/frontend/fixtures/runner.rb +// Register runner queries +import runnerForRegistration from 'test_fixtures/graphql/ci/runner/register/runner_for_registration.query.graphql.json'; + // Show runner queries +import runnerCreateResult from 'test_fixtures/graphql/ci/runner/new/runner_create.mutation.graphql.json'; import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.json'; import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json'; import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json'; @@ -9,6 +13,8 @@ import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.que // Edit runner queries import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json'; +// New runner queries + // List queries import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json'; import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json'; @@ -321,4 +327,6 @@ export { runnerProjectsData, runnerJobsData, runnerFormData, + runnerCreateResult, + runnerForRegistration, }; diff --git a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js index a9369a5e626..79bbf95f8f0 100644 --- a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js +++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/ci/runner/components/runner_header.vue'; @@ -15,7 +15,7 @@ import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/ci/runner/con import { runnerFormData } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/ci/runner/sentry_utils'); const mockRunner = runnerFormData.data.runner; @@ -51,7 +51,6 @@ describe('RunnerEditApp', () => { afterEach(() => { mockRunnerQuery.mockReset(); - wrapper.destroy(); }); it('expect GraphQL ID to be requested', async () => { diff --git a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap index b2084e3a7de..1be89ae832d 100644 --- a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap +++ b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap @@ -15,7 +15,7 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the > <table - aria-busy="false" + aria-busy="" aria-colcount="2" class="table b-table gl-table" role="table" @@ -196,7 +196,7 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat > <table - aria-busy="false" + aria-busy="" aria-colcount="2" class="table b-table gl-table" role="table" diff --git a/spec/frontend/ci_secure_files/components/metadata/button_spec.js b/spec/frontend/ci_secure_files/components/metadata/button_spec.js index 4ac5b3325d4..5bd4bab25af 100644 --- a/spec/frontend/ci_secure_files/components/metadata/button_spec.js +++ b/spec/frontend/ci_secure_files/components/metadata/button_spec.js @@ -12,10 +12,6 @@ describe('Secure File Metadata Button', () => { const findButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - wrapper.destroy(); - }); - const createWrapper = (secureFile = {}, admin = false) => { wrapper = mount(Button, { propsData: { diff --git a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js index 230507d32d7..e181d15f2f9 100644 --- a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js +++ b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js @@ -37,7 +37,6 @@ describe('Secure File Metadata Modal', () => { afterEach(() => { unmockTracking(); - wrapper.destroy(); }); describe('when a .cer file is supplied', () => { diff --git a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js index ab6200ca6f4..17b5fdc4dde 100644 --- a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js +++ b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js @@ -15,11 +15,6 @@ const dummyApiVersion = 'v3000'; const dummyProjectId = 1; const fileSizeLimit = 5; const dummyUrlRoot = '/gitlab'; -const dummyGon = { - api_version: dummyApiVersion, - relative_url_root: dummyUrlRoot, -}; -let originalGon; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${dummyProjectId}/secure_files`; describe('SecureFilesList', () => { @@ -28,16 +23,16 @@ describe('SecureFilesList', () => { let trackingSpy; beforeEach(() => { - originalGon = window.gon; trackingSpy = mockTracking(undefined, undefined, jest.spyOn); - window.gon = { ...dummyGon }; + window.gon = { + api_version: dummyApiVersion, + relative_url_root: dummyUrlRoot, + }; }); afterEach(() => { - wrapper.destroy(); mock.restore(); unmockTracking(); - window.gon = originalGon; }); const createWrapper = (admin = true, props = {}) => { diff --git a/spec/frontend/clusters/agents/components/activity_events_list_spec.js b/spec/frontend/clusters/agents/components/activity_events_list_spec.js index 6b374b6620d..770815a9403 100644 --- a/spec/frontend/clusters/agents/components/activity_events_list_spec.js +++ b/spec/frontend/clusters/agents/components/activity_events_list_spec.js @@ -44,10 +44,6 @@ describe('ActivityEvents', () => { const findAllActivityHistoryItems = () => wrapper.findAllComponents(ActivityHistoryItem); const findSectionTitle = (at) => wrapper.findAllByTestId('activity-section-title').at(at); - afterEach(() => { - wrapper.destroy(); - }); - describe('while the agentEvents query is loading', () => { it('displays a loading icon', async () => { createWrapper(); 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 68f6f11aa8f..48460519c6c 100644 --- a/spec/frontend/clusters/agents/components/activity_history_item_spec.js +++ b/spec/frontend/clusters/agents/components/activity_history_item_spec.js @@ -25,10 +25,6 @@ describe('ActivityHistoryItem', () => { const findHistoryItem = () => wrapper.findComponent(HistoryItem); const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` kind | icon | title | lineNumber ${'token_created'} | ${EVENT_DETAILS.token_created.eventTypeIcon} | ${sprintf(EVENT_DETAILS.token_created.title, { tokenName: agentName })} | ${0} diff --git a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js index db1219ccb41..ac0ce89f334 100644 --- a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js +++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js @@ -25,10 +25,6 @@ describe('IntegrationStatus', () => { const findIcon = () => wrapper.findComponent(GlIcon); const findBadge = () => wrapper.findComponent(GlBadge); - afterEach(() => { - wrapper.destroy(); - }); - describe('icon', () => { const icon = 'status-success'; const iconClass = 'gl-text-green-500'; diff --git a/spec/frontend/clusters/agents/components/create_token_button_spec.js b/spec/frontend/clusters/agents/components/create_token_button_spec.js index 73856b74a8d..5a8906813cf 100644 --- a/spec/frontend/clusters/agents/components/create_token_button_spec.js +++ b/spec/frontend/clusters/agents/components/create_token_button_spec.js @@ -21,7 +21,7 @@ describe('CreateTokenButton', () => { ...provideData, }, directives: { - GlModalDirective: createMockDirective(), + GlModalDirective: createMockDirective('gl-modal-directive'), }, stubs: { GlTooltip, @@ -29,10 +29,6 @@ describe('CreateTokenButton', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when user can create token', () => { beforeEach(() => { createWrapper(); 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 0d10801e80e..ff698952c6b 100644 --- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js +++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js @@ -119,7 +119,6 @@ describe('CreateTokenModal', () => { }); afterEach(() => { - wrapper.destroy(); apolloProvider = null; createResponse = null; }); diff --git a/spec/frontend/clusters/agents/components/integration_status_spec.js b/spec/frontend/clusters/agents/components/integration_status_spec.js index 36f0e622452..28a59391578 100644 --- a/spec/frontend/clusters/agents/components/integration_status_spec.js +++ b/spec/frontend/clusters/agents/components/integration_status_spec.js @@ -27,10 +27,6 @@ describe('IntegrationStatus', () => { const findAgentStatus = () => wrapper.findByTestId('agent-status'); const findAgentIntegrationStatusRows = () => wrapper.findAllComponents(AgentIntegrationStatusRow); - afterEach(() => { - wrapper.destroy(); - }); - it.each` lastUsedAt | status | iconName ${null} | ${'Never connected'} | ${'status-neutral'} diff --git a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js index 6521221cbd7..ed7c940bb04 100644 --- a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js +++ b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js @@ -45,7 +45,7 @@ describe('RevokeTokenButton', () => { const findInput = () => wrapper.findComponent(GlFormInput); const findTooltip = () => wrapper.findComponent(GlTooltip); const findPrimaryAction = () => findModal().props('actionPrimary'); - const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr]; + const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr]; const createMockApolloProvider = ({ mutationResponse }) => { revokeSpy = jest.fn().mockResolvedValue(mutationResponse); @@ -105,7 +105,6 @@ describe('RevokeTokenButton', () => { }); afterEach(() => { - wrapper.destroy(); apolloProvider = null; revokeSpy = null; }); diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js index efa85136b17..118a3af48e0 100644 --- a/spec/frontend/clusters/agents/components/show_spec.js +++ b/spec/frontend/clusters/agents/components/show_spec.js @@ -79,10 +79,6 @@ describe('ClusterAgentShow', () => { const findActivity = () => wrapper.findComponent(ActivityEvents); const findIntegrationStatus = () => wrapper.findComponent(IntegrationStatus); - afterEach(() => { - wrapper.destroy(); - }); - describe('default behaviour', () => { beforeEach(async () => { createWrapper({ clusterAgent: defaultClusterAgent }); diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js index 334615f1818..1a6aeedb694 100644 --- a/spec/frontend/clusters/agents/components/token_table_spec.js +++ b/spec/frontend/clusters/agents/components/token_table_spec.js @@ -57,10 +57,6 @@ describe('ClusterAgentTokenTable', () => { return createComponent(defaultTokens); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays the create token button', () => { expect(findCreateTokenBtn().exists()).toBe(true); }); diff --git a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap index 656e72baf77..21ffda8578a 100644 --- a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap +++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap @@ -3,7 +3,7 @@ exports[`NewCluster renders the cluster component correctly 1`] = ` "<div class=\\"gl-pt-4\\"> <h4>Enter your Kubernetes cluster certificate details</h4> - <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub> + <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub> </p> </div>" `; diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap index 46ee123a12d..67b0ecdf7eb 100644 --- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap +++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap @@ -43,3 +43,212 @@ exports[`Remove cluster confirmation modal renders buttons with modal included 1 <!----> </div> `; + +exports[`Remove cluster confirmation modal two buttons open modal with "cleanup" option 1`] = ` +<div + class="gl-display-flex" +> + <button + class="btn gl-mr-3 btn-danger btn-md gl-button" + data-testid="remove-integration-and-resources-button" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Remove integration and resources + + </span> + </button> + + <button + class="btn btn-danger btn-md gl-button btn-danger-secondary" + data-testid="remove-integration-button" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Remove integration + + </span> + </button> + + <div + kind="danger" + > + <p> + You are about to remove your cluster integration and all GitLab-created resources associated with this cluster. + </p> + + <div> + + This will permanently delete the following resources: + + <ul> + <li> + Any project namespaces + </li> + + <li> + <code> + clusterroles + </code> + </li> + + <li> + <code> + clusterrolebindings + </code> + </li> + </ul> + </div> + + <strong> + To remove your integration and resources, type + <code> + my-test-cluster + </code> + to confirm: + </strong> + + <form + action="clusterPath" + class="gl-mb-5" + method="post" + > + <input + name="_method" + type="hidden" + value="delete" + /> + + <input + name="authenticity_token" + type="hidden" + /> + + <input + name="cleanup" + type="hidden" + value="true" + /> + + <input + autocomplete="off" + class="gl-form-input form-control" + id="__BVID__14" + name="confirm_cluster_name_input" + type="text" + /> + </form> + + <span> + If you do not wish to delete all associated GitLab resources, you can simply remove the integration. + </span> + </div> +</div> +`; + +exports[`Remove cluster confirmation modal two buttons open modal without "cleanup" option 1`] = ` +<div + class="gl-display-flex" +> + <button + class="btn gl-mr-3 btn-danger btn-md gl-button" + data-testid="remove-integration-and-resources-button" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Remove integration and resources + + </span> + </button> + + <button + class="btn btn-danger btn-md gl-button btn-danger-secondary" + data-testid="remove-integration-button" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Remove integration + + </span> + </button> + + <div + kind="danger" + > + <p> + You are about to remove your cluster integration. + </p> + + <!----> + + <strong> + To remove your integration, type + <code> + my-test-cluster + </code> + to confirm: + </strong> + + <form + action="clusterPath" + class="gl-mb-5" + method="post" + > + <input + name="_method" + type="hidden" + value="delete" + /> + + <input + name="authenticity_token" + type="hidden" + /> + + <input + name="cleanup" + type="hidden" + value="true" + /> + + <input + autocomplete="off" + class="gl-form-input form-control" + id="__BVID__21" + name="confirm_cluster_name_input" + type="text" + /> + </form> + + <!----> + </div> +</div> +`; diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js index ef39c90aaef..398b472a3a7 100644 --- a/spec/frontend/clusters/components/new_cluster_spec.js +++ b/spec/frontend/clusters/components/new_cluster_spec.js @@ -20,10 +20,6 @@ describe('NewCluster', () => { return createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the cluster component correctly', () => { expect(wrapper.html()).toMatchSnapshot(); }); diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js index 53683af893a..04b7909b534 100644 --- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js +++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js @@ -6,6 +6,7 @@ import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_conf describe('Remove cluster confirmation modal', () => { let wrapper; + const showMock = jest.fn(); const createComponent = ({ props = {}, stubs = {} } = {}) => { wrapper = mount(RemoveClusterConfirmation, { @@ -18,11 +19,6 @@ describe('Remove cluster confirmation modal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders buttons with modal included', () => { createComponent(); expect(wrapper.element).toMatchSnapshot(); @@ -38,9 +34,13 @@ describe('Remove cluster confirmation modal', () => { beforeEach(() => { createComponent({ props: { clusterName: 'my-test-cluster' }, - stubs: { GlSprintf, GlModal: stubComponent(GlModal) }, + stubs: { + GlSprintf, + GlModal: stubComponent(GlModal, { + methods: { show: showMock }, + }), + }, }); - jest.spyOn(findModal().vm, 'show').mockReturnValue(); }); it('open modal with "cleanup" option', async () => { @@ -48,8 +48,8 @@ describe('Remove cluster confirmation modal', () => { await nextTick(); - expect(findModal().vm.show).toHaveBeenCalled(); - expect(wrapper.vm.confirmCleanup).toEqual(true); + expect(showMock).toHaveBeenCalled(); + expect(wrapper.element).toMatchSnapshot(); expect(findModal().html()).toContain( '<strong>To remove your integration and resources, type <code>my-test-cluster</code> to confirm:</strong>', ); @@ -60,8 +60,8 @@ describe('Remove cluster confirmation modal', () => { await nextTick(); - expect(findModal().vm.show).toHaveBeenCalled(); - expect(wrapper.vm.confirmCleanup).toEqual(false); + expect(showMock).toHaveBeenCalled(); + expect(wrapper.element).toMatchSnapshot(); expect(findModal().html()).toContain( '<strong>To remove your integration, type <code>my-test-cluster</code> to confirm:</strong>', ); diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js index b17886a5826..396f8215b9f 100644 --- a/spec/frontend/clusters/forms/components/integration_form_spec.js +++ b/spec/frontend/clusters/forms/components/integration_form_spec.js @@ -1,6 +1,6 @@ import { GlToggle, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import Vuex from 'vuex'; import IntegrationForm from '~/clusters/forms/components/integration_form.vue'; import { createStore } from '~/clusters/forms/stores/index'; @@ -27,17 +27,9 @@ describe('ClusterIntegrationForm', () => { }); }; - const destroyWrapper = () => { - wrapper.destroy(); - wrapper = null; - }; - const findSubmitButton = () => wrapper.findComponent(GlButton); const findGlToggle = () => wrapper.findComponent(GlToggle); - - afterEach(() => { - destroyWrapper(); - }); + const findClusterEnvironmentScopeInput = () => wrapper.find('[id="cluster_environment_scope"]'); describe('rendering', () => { beforeEach(() => createWrapper()); @@ -50,7 +42,9 @@ describe('ClusterIntegrationForm', () => { }); it('sets the envScope to default', () => { - expect(wrapper.find('[id="cluster_environment_scope"]').attributes('value')).toBe('*'); + expect(findClusterEnvironmentScopeInput().attributes('value')).toBe( + defaultStoreValues.environmentScope, + ); }); it('sets the baseDomain to default', () => { @@ -76,20 +70,15 @@ describe('ClusterIntegrationForm', () => { beforeEach(() => createWrapper()); it('enables the submit button on changing toggle to different value', async () => { - await nextTick(); - // setData is a bad approach because it changes the internal implementation which we should not touch - // but our GlFormInput lacks the ability to set a new value. - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - await wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled }); + await findGlToggle().vm.$emit('change', false); expect(findSubmitButton().props('disabled')).toBe(false); }); it('enables the submit button on changing input values', async () => { - await nextTick(); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - await wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` }); + await findClusterEnvironmentScopeInput().vm.$emit( + 'input', + `${defaultStoreValues.environmentScope}1`, + ); expect(findSubmitButton().props('disabled')).toBe(false); }); }); diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js index a92a03fedb6..edb8b22d79e 100644 --- a/spec/frontend/clusters_list/components/agent_token_spec.js +++ b/spec/frontend/clusters_list/components/agent_token_spec.js @@ -53,10 +53,6 @@ describe('InstallAgentModal', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('initial state', () => { it('shows basic agent installation instructions', () => { expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallTitle); diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js index 2372ab30300..d91245ba9b4 100644 --- a/spec/frontend/clusters_list/components/agents_spec.js +++ b/spec/frontend/clusters_list/components/agents_spec.js @@ -83,8 +83,6 @@ describe('Agents', () => { const findBanner = () => wrapper.findComponent(GlBanner); afterEach(() => { - wrapper.destroy(); - localStorage.removeItem(AGENT_FEEDBACK_KEY); }); diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js index 758f6586e1a..4a2effa3463 100644 --- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js +++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js @@ -18,10 +18,6 @@ describe('ClustersAncestorNotice', () => { return createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when cluster does not have ancestors', () => { beforeEach(async () => { store.state.hasAncestorClusters = false; diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js index f4ee3f93cb5..e4e1986f705 100644 --- a/spec/frontend/clusters_list/components/clusters_actions_spec.js +++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js @@ -35,7 +35,7 @@ describe('ClustersActionsComponent', () => { ...provideData, }, directives: { - GlModalDirective: createMockDirective(), + GlModalDirective: createMockDirective('gl-modal-directive'), }, }); }; @@ -44,9 +44,6 @@ describe('ClustersActionsComponent', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); describe('when the certificate based clusters are enabled', () => { it('renders actions menu', () => { expect(findDropdown().exists()).toBe(true); diff --git a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js index 2c3a224f3c8..5a5006d24c4 100644 --- a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js +++ b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js @@ -21,10 +21,6 @@ describe('ClustersEmptyStateComponent', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when the help text is not provided', () => { beforeEach(() => { createWrapper(); diff --git a/spec/frontend/clusters_list/components/clusters_main_view_spec.js b/spec/frontend/clusters_list/components/clusters_main_view_spec.js index 6f23ed47d2a..af8d3b59869 100644 --- a/spec/frontend/clusters_list/components/clusters_main_view_spec.js +++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js @@ -40,10 +40,6 @@ describe('ClustersMainViewComponent', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findTabs = () => wrapper.findComponent(GlTabs); const findAllTabs = () => wrapper.findAllComponents(GlTab); const findGlTabAtIndex = (index) => findAllTabs().at(index); diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js index 20dbff9df15..207bfddcb4f 100644 --- a/spec/frontend/clusters_list/components/clusters_spec.js +++ b/spec/frontend/clusters_list/components/clusters_spec.js @@ -75,7 +75,6 @@ describe('Clusters', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); captureException.mockRestore(); }); @@ -271,9 +270,7 @@ describe('Clusters', () => { describe('when updating currentPage', () => { beforeEach(() => { mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalSecondPage, perPage, 2)); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ currentPage: 2 }); + findPaginatedButtons().vm.$emit('input', 2); return axios.waitForAll(); }); diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js index b4eb9242003..e81b242dd90 100644 --- a/spec/frontend/clusters_list/components/clusters_view_all_spec.js +++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js @@ -60,10 +60,6 @@ describe('ClustersViewAllComponent', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when agents and clusters are not loaded', () => { const initialState = { loadingClusters: true, diff --git a/spec/frontend/clusters_list/components/delete_agent_button_spec.js b/spec/frontend/clusters_list/components/delete_agent_button_spec.js index 82850b9dea4..53cf67bca0f 100644 --- a/spec/frontend/clusters_list/components/delete_agent_button_spec.js +++ b/spec/frontend/clusters_list/components/delete_agent_button_spec.js @@ -33,7 +33,7 @@ describe('DeleteAgentButton', () => { const findDeleteBtn = () => wrapper.findComponent(GlButton); const findInput = () => wrapper.findComponent(GlFormInput); const findPrimaryAction = () => findModal().props('actionPrimary'); - const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr]; + const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr]; const findDeleteAgentButtonTooltip = () => wrapper.findByTestId('delete-agent-button-tooltip'); const getTooltipText = (el) => { const binding = getBinding(el, 'gl-tooltip'); @@ -84,7 +84,7 @@ describe('DeleteAgentButton', () => { ...provideData, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData, mocks: { $toast: { show: toast } }, @@ -108,7 +108,6 @@ describe('DeleteAgentButton', () => { }); afterEach(() => { - wrapper.destroy(); apolloProvider = null; deleteResponse = null; toast = null; 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 10264d6a011..3156eaaecfc 100644 --- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js +++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js @@ -139,7 +139,6 @@ describe('InstallAgentModal', () => { }); afterEach(() => { - wrapper.destroy(); apolloProvider = null; }); diff --git a/spec/frontend/clusters_list/components/node_error_help_text_spec.js b/spec/frontend/clusters_list/components/node_error_help_text_spec.js index 3211ba44eff..a3dfc848fc8 100644 --- a/spec/frontend/clusters_list/components/node_error_help_text_spec.js +++ b/spec/frontend/clusters_list/components/node_error_help_text_spec.js @@ -13,10 +13,6 @@ describe('NodeErrorHelpText', () => { const findPopover = () => wrapper.findComponent(GlPopover); - afterEach(() => { - wrapper.destroy(); - }); - it.each` errorType | wrapperText | popoverText ${'authentication_error'} | ${'Unable to Authenticate'} | ${'GitLab failed to authenticate'} diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js index 360fd3b2842..6d23db0517d 100644 --- a/spec/frontend/clusters_list/store/actions_spec.js +++ b/spec/frontend/clusters_list/store/actions_spec.js @@ -5,13 +5,13 @@ import waitForPromises from 'helpers/wait_for_promises'; import { MAX_REQUESTS } from '~/clusters_list/constants'; import * as actions from '~/clusters_list/store/actions'; import * as types from '~/clusters_list/store/mutation_types'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import Poll from '~/lib/utils/poll'; import { apiData } from '../mock_data'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('Clusters store actions', () => { let captureException; @@ -81,7 +81,7 @@ describe('Clusters store actions', () => { ); }); - it('should show flash on API error', async () => { + it('should show alert on API error', async () => { mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found'); await testAction( diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js index b9be262efd0..88861b0d08a 100644 --- a/spec/frontend/code_navigation/components/app_spec.js +++ b/spec/frontend/code_navigation/components/app_spec.js @@ -32,10 +32,6 @@ function factory(initialState = {}, props = {}) { } describe('Code navigation app component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('sets initial data on mount if the correct props are passed', () => { const codeNavigationPath = 'code/nav/path.js'; const path = 'blob/path.js'; diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js index 874263e046a..1bfaf7e959e 100644 --- a/spec/frontend/code_navigation/components/popover_spec.js +++ b/spec/frontend/code_navigation/components/popover_spec.js @@ -61,10 +61,6 @@ function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' } } describe('Code navigation popover component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('renders popover', () => { factory({ position: { x: 0, y: 0, height: 0 }, diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js index debd10de118..64623968aa0 100644 --- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js +++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js @@ -5,7 +5,7 @@ import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue'; import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants'; @@ -20,7 +20,7 @@ import { mockUpstreamQueryResponse, } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -69,10 +69,6 @@ describe('Commit box pipeline mini graph', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('loading state', () => { it('should display loading state when loading', () => { createComponent(); diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js index e75fb697a7b..e474ef9c635 100644 --- a/spec/frontend/commit/commit_pipeline_status_component_spec.js +++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js @@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils'; import Visibility from 'visibilityjs'; import { nextTick } from 'vue'; import fixture from 'test_fixtures/pipelines/pipelines.json'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import Poll from '~/lib/utils/poll'; import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; jest.mock('~/lib/utils/poll'); jest.mock('visibilityjs'); -jest.mock('~/flash'); +jest.mock('~/alert'); const mockFetchData = jest.fn(); jest.mock('~/projects/tree/services/commit_pipeline_service', () => @@ -41,11 +41,6 @@ describe('Commit pipeline status component', () => { const findLink = () => wrapper.find('a'); const findCiIcon = () => findLink().findComponent(CiIcon); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('Visibility management', () => { describe('when component is hidden', () => { beforeEach(() => { @@ -169,7 +164,7 @@ describe('Commit pipeline status component', () => { }); }); - it('displays flash error message', () => { + it('displays alert error message', () => { expect(createAlert).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js index 8d455f8a3d7..9c7a41b3506 100644 --- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js +++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue'; import { @@ -23,7 +23,7 @@ const mockProvide = { Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Commit box pipeline status', () => { let wrapper; @@ -54,10 +54,6 @@ describe('Commit box pipeline status', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('loading state', () => { it('should display loading state when loading', () => { createComponent(); diff --git a/spec/frontend/commit/components/signature_badge_spec.js b/spec/frontend/commit/components/signature_badge_spec.js new file mode 100644 index 00000000000..d52ad2b43e2 --- /dev/null +++ b/spec/frontend/commit/components/signature_badge_spec.js @@ -0,0 +1,134 @@ +import { GlBadge, GlLink, GlPopover } from '@gitlab/ui'; +import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; +import SignatureBadge from '~/commit/components/signature_badge.vue'; +import X509CertificateDetails from '~/commit/components/x509_certificate_details.vue'; +import { typeConfig, statusConfig, verificationStatuses, signatureTypes } from '~/commit/constants'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { sshSignatureProp, gpgSignatureProp, x509SignatureProp } from '../mock_data'; + +describe('Commit signature', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mountExtended(SignatureBadge, { + propsData: { + signature: { + ...props, + }, + stubs: { + GlBadge, + GlLink, + X509CertificateDetails, + GlPopover: stubComponent(GlPopover, { template: RENDER_ALL_SLOTS_TEMPLATE }), + }, + }, + }); + }; + + const signatureBadge = () => wrapper.findComponent(GlBadge); + const signaturePopover = () => wrapper.findComponent(GlPopover); + const signatureDescription = () => wrapper.findByTestId('signature-description'); + const signatureKeyLabel = () => wrapper.findByTestId('signature-key-label'); + const signatureKey = () => wrapper.findByTestId('signature-key'); + const helpLink = () => wrapper.findComponent(GlLink); + const X509CertificateDetailsComponents = () => wrapper.findAllComponents(X509CertificateDetails); + + describe.each` + signatureType | verificationStatus + ${signatureTypes.GPG} | ${verificationStatuses.VERIFIED} + ${signatureTypes.GPG} | ${verificationStatuses.UNVERIFIED} + ${signatureTypes.GPG} | ${verificationStatuses.UNVERIFIED_KEY} + ${signatureTypes.GPG} | ${verificationStatuses.UNKNOWN_KEY} + ${signatureTypes.GPG} | ${verificationStatuses.OTHER_USER} + ${signatureTypes.GPG} | ${verificationStatuses.SAME_USER_DIFFERENT_EMAIL} + ${signatureTypes.GPG} | ${verificationStatuses.MULTIPLE_SIGNATURES} + ${signatureTypes.X509} | ${verificationStatuses.VERIFIED} + ${signatureTypes.SSH} | ${verificationStatuses.VERIFIED} + ${signatureTypes.SSH} | ${verificationStatuses.REVOKED_KEY} + `( + 'For a specified `$signatureType` and `$verificationStatus` it renders component correctly', + ({ signatureType, verificationStatus }) => { + beforeEach(() => { + createComponent({ __typename: signatureType, verificationStatus }); + }); + it('renders correct badge class', () => { + expect(signatureBadge().props('variant')).toBe(statusConfig[verificationStatus].variant); + }); + it('renders badge text', () => { + expect(signatureBadge().text()).toBe(statusConfig[verificationStatus].label); + }); + it('renders popover header text', () => { + expect(signaturePopover().text()).toMatch(statusConfig[verificationStatus].title); + }); + it('renders signature description', () => { + expect(signatureDescription().text()).toBe(statusConfig[verificationStatus].description); + }); + it('renders help link with correct path', () => { + expect(helpLink().text()).toBe(typeConfig[signatureType].helpLink.label); + expect(helpLink().attributes('href')).toBe( + helpPagePath(typeConfig[signatureType].helpLink.path), + ); + }); + }, + ); + + describe('SSH signature', () => { + beforeEach(() => { + createComponent(sshSignatureProp); + }); + + it('renders key label', () => { + expect(signatureKeyLabel().text()).toMatch(typeConfig[signatureTypes.SSH].keyLabel); + }); + + it('renders key signature', () => { + expect(signatureKey().text()).toBe(sshSignatureProp.keyFingerprintSha256); + }); + }); + + describe('GPG signature', () => { + beforeEach(() => { + createComponent(gpgSignatureProp); + }); + + it('renders key label', () => { + expect(signatureKeyLabel().text()).toMatch(typeConfig[signatureTypes.GPG].keyLabel); + }); + + it('renders key signature for GGP signature', () => { + expect(signatureKey().text()).toBe(gpgSignatureProp.gpgKeyPrimaryKeyid); + }); + }); + + describe('X509 signature', () => { + beforeEach(() => { + createComponent(x509SignatureProp); + }); + + it('does not render key label', () => { + expect(signatureKeyLabel().exists()).toBe(false); + }); + + it('renders X509 certificate details components', () => { + expect(X509CertificateDetailsComponents()).toHaveLength(2); + }); + + it('passes correct props', () => { + expect(X509CertificateDetailsComponents().at(0).props()).toStrictEqual({ + subject: x509SignatureProp.x509Certificate.subject, + title: typeConfig[signatureTypes.X509].subjectTitle, + subjectKeyIdentifier: wrapper.vm.getSubjectKeyIdentifierToDisplay( + x509SignatureProp.x509Certificate.subjectKeyIdentifier, + ), + }); + expect(X509CertificateDetailsComponents().at(1).props()).toStrictEqual({ + subject: x509SignatureProp.x509Certificate.x509Issuer.subject, + title: typeConfig[signatureTypes.X509].issuerTitle, + subjectKeyIdentifier: wrapper.vm.getSubjectKeyIdentifierToDisplay( + x509SignatureProp.x509Certificate.x509Issuer.subjectKeyIdentifier, + ), + }); + }); + }); +}); diff --git a/spec/frontend/commit/components/x509_certificate_details_spec.js b/spec/frontend/commit/components/x509_certificate_details_spec.js new file mode 100644 index 00000000000..5d9398b572b --- /dev/null +++ b/spec/frontend/commit/components/x509_certificate_details_spec.js @@ -0,0 +1,36 @@ +import { shallowMount } from '@vue/test-utils'; +import X509CertificateDetails from '~/commit/components/x509_certificate_details.vue'; +import { X509_CERTIFICATE_KEY_IDENTIFIER_TITLE } from '~/commit/constants'; +import { x509CertificateDetailsProp } from '../mock_data'; + +describe('X509 certificate details', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(X509CertificateDetails, { + propsData: x509CertificateDetailsProp, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + const findTitle = () => wrapper.find('strong'); + const findSubjectValues = () => wrapper.findAll("[data-testid='subject-value']"); + const findKeyIdentifier = () => wrapper.find("[data-testid='key-identifier']"); + + it('renders a title', () => { + expect(findTitle().text()).toBe(x509CertificateDetailsProp.title); + }); + + it('renders subject values', () => { + expect(findSubjectValues()).toHaveLength(3); + }); + + it('renders key identifier', () => { + expect(findKeyIdentifier().text()).toBe( + `${X509_CERTIFICATE_KEY_IDENTIFIER_TITLE} ${x509CertificateDetailsProp.subjectKeyIdentifier}`, + ); + }); +}); diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js index a13ef9c563e..3b6971d9607 100644 --- a/spec/frontend/commit/mock_data.js +++ b/spec/frontend/commit/mock_data.js @@ -201,3 +201,34 @@ export const mockUpstreamQueryResponse = { }, }, }; + +export const sshSignatureProp = { + __typename: 'SshSignature', + verificationStatus: 'VERIFIED', + keyFingerprintSha256: 'xxx', +}; + +export const gpgSignatureProp = { + __typename: 'GpgSignature', + verificationStatus: 'VERIFIED', + gpgKeyPrimaryKeyid: 'yyy', +}; + +export const x509SignatureProp = { + __typename: 'X509Signature', + verificationStatus: 'VERIFIED', + x509Certificate: { + subject: 'CN=gitlab@example.org,OU=Example,O=World', + subjectKeyIdentifier: 'BC:BC:BC:BC:BC:BC:BC:BC', + x509Issuer: { + subject: 'CN=PKI,OU=Example,O=World', + subjectKeyIdentifier: 'AB:AB:AB:AB:AB:AB:AB:AB:', + }, + }, +}; + +export const x509CertificateDetailsProp = { + title: 'Title', + subject: 'CN=gitlab@example.org,OU=Example,O=World', + subjectKeyIdentifier: 'BC BC BC BC BC BC BC BC', +}; diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js index 4bffb6a0fd3..009ec68ddcf 100644 --- a/spec/frontend/commit/pipelines/pipelines_table_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js @@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import fixture from 'test_fixtures/pipelines/pipelines.json'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import Api from '~/api'; import PipelinesTable from '~/commit/pipelines/pipelines_table.vue'; @@ -13,7 +14,7 @@ import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED, } from '~/lib/utils/http_status'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { TOAST_MESSAGE } from '~/pipelines/constants'; import axios from '~/lib/utils/axios_utils'; @@ -21,12 +22,13 @@ const $toast = { show: jest.fn(), }; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Pipelines table in Commits and Merge requests', () => { let wrapper; let pipeline; let mock; + const showMock = jest.fn(); const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button'); const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile'); @@ -38,7 +40,7 @@ describe('Pipelines table in Commits and Merge requests', () => { const findModal = () => wrapper.findComponent(GlModal); const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link'); - const createComponent = (props = {}) => { + const createComponent = ({ props = {} } = {}) => { wrapper = extendedWrapper( mount(PipelinesTable, { propsData: { @@ -50,6 +52,12 @@ describe('Pipelines table in Commits and Merge requests', () => { mocks: { $toast, }, + stubs: { + GlModal: stubComponent(GlModal, { + template: '<div />', + methods: { show: showMock }, + }), + }, }), ); }; @@ -62,11 +70,6 @@ describe('Pipelines table in Commits and Merge requests', () => { pipeline = pipelines.find((p) => p.user !== null && p.commit !== null); }); - afterEach(() => { - wrapper.destroy(); - mock.restore(); - }); - describe('successful request', () => { describe('without pipelines', () => { beforeEach(async () => { @@ -95,6 +98,35 @@ describe('Pipelines table in Commits and Merge requests', () => { }); }); + describe('with pagination', () => { + beforeEach(async () => { + mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { + 'X-TOTAL': 10, + 'X-PER-PAGE': 2, + 'X-PAGE': 1, + 'X-TOTAL-PAGES': 5, + 'X-NEXT-PAGE': 2, + 'X-PREV-PAGE': 2, + }); + + createComponent(); + + await waitForPromises(); + }); + + it('should make an API request when using pagination', async () => { + expect(mock.history.get).toHaveLength(1); + expect(mock.history.get[0].params.page).toBe('1'); + + wrapper.find('.next-page-item').trigger('click'); + + await waitForPromises(); + + expect(mock.history.get).toHaveLength(2); + expect(mock.history.get[1].params.page).toBe('2'); + }); + }); + describe('with pipelines', () => { beforeEach(async () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'x-total': 10 }); @@ -111,32 +143,6 @@ describe('Pipelines table in Commits and Merge requests', () => { expect(findErrorEmptyState().exists()).toBe(false); }); - describe('with pagination', () => { - it('should make an API request when using pagination', async () => { - jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {}); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - await wrapper.setData({ - store: { - state: { - pageInfo: { - page: 1, - total: 10, - perPage: 2, - nextPage: 2, - totalPages: 5, - }, - }, - }, - }); - - wrapper.find('.next-page-item').trigger('click'); - - expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ page: '2' }); - }); - }); - describe('pipeline badge counts', () => { it('should receive update-pipelines-count event', () => { const element = document.createElement('div'); @@ -203,16 +209,18 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]); createComponent({ - canRunPipeline: true, - projectId: '5', - mergeRequestId: 3, + props: { + canRunPipeline: true, + projectId: '5', + mergeRequestId: 3, + }, }); await waitForPromises(); }); describe('success', () => { beforeEach(() => { - jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve()); + jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue(); }); it('displays a toast message during pipeline creation', async () => { await findRunPipelineBtn().trigger('click'); @@ -255,9 +263,7 @@ describe('Pipelines table in Commits and Merge requests', () => { `('displays permissions error message', async ({ status, message }) => { const response = { response: { status } }; - jest - .spyOn(Api, 'postMergeRequestPipeline') - .mockImplementation(() => Promise.reject(response)); + jest.spyOn(Api, 'postMergeRequestPipeline').mockRejectedValue(response); await findRunPipelineBtn().trigger('click'); @@ -281,14 +287,16 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]); createComponent({ - projectId: '5', - mergeRequestId: 3, - canCreatePipelineInTargetProject: true, - sourceProjectFullPath: 'test/parent-project', - targetProjectFullPath: 'test/fork-project', + props: { + projectId: '5', + mergeRequestId: 3, + canCreatePipelineInTargetProject: true, + sourceProjectFullPath: 'test/parent-project', + targetProjectFullPath: 'test/fork-project', + }, }); - jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve()); + jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue(); await waitForPromises(); }); @@ -313,15 +321,15 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []); createComponent({ - projectId: '5', - mergeRequestId: 3, - canCreatePipelineInTargetProject: true, - sourceProjectFullPath: 'test/parent-project', - targetProjectFullPath: 'test/fork-project', + props: { + projectId: '5', + mergeRequestId: 3, + canCreatePipelineInTargetProject: true, + sourceProjectFullPath: 'test/parent-project', + targetProjectFullPath: 'test/fork-project', + }, }); - jest.spyOn(findModal().vm, 'show').mockReturnValue(); - await waitForPromises(); }); @@ -331,7 +339,7 @@ describe('Pipelines table in Commits and Merge requests', () => { findRunPipelineBtn().trigger('click'); - expect(findModal().vm.show).toHaveBeenCalled(); + expect(showMock).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js index d6f16f1a644..a7ae07a36d9 100644 --- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js +++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js @@ -46,7 +46,6 @@ function factory(projects = mockData) { describe('Confidential merge request project form group component', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); it('renders fork dropdown', async () => { diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap index a63cca006da..b8e6bcbc3c4 100644 --- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap +++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = ` -"<b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\"> +"<b-button-stub size=\\"md\\" tag=\\"button\\" type=\\"button\\" variant=\\"default\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\"> <!----> <gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub> <!----> diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js index 0700cf5d529..271e63abf21 100644 --- a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js @@ -51,10 +51,6 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => { setupMocks(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('initializes BubbleMenuPlugin', async () => { createWrapper({}); diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js index 378b11f4ae9..085a6d3a28d 100644 --- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js @@ -64,10 +64,6 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () => buildWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders bubble menu component', async () => { tiptapEditor.commands.insertContent(preTag()); bubbleMenu = wrapper.findComponent(BubbleMenu); diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js index 98001858851..7bab473529f 100644 --- a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js @@ -37,10 +37,6 @@ describe('content_editor/components/bubble_menus/formatting_bubble_menu', () => buildEditor(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders bubble menu component', () => { buildWrapper(); const bubbleMenu = wrapper.findComponent(BubbleMenu); diff --git a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js index 9aa9c6483f4..eb5a3b61591 100644 --- a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js @@ -71,10 +71,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => { .run(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders bubble menu component', async () => { await buildWrapperAndDisplayMenu(); diff --git a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js index 13c6495ac41..c918f068c07 100644 --- a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js @@ -4,22 +4,28 @@ import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue'; import { stubComponent } from 'helpers/stub_component'; import eventHubFactory from '~/helpers/event_hub_factory'; -import Image from '~/content_editor/extensions/image'; import Audio from '~/content_editor/extensions/audio'; +import DrawioDiagram from '~/content_editor/extensions/drawio_diagram'; +import Image from '~/content_editor/extensions/image'; import Video from '~/content_editor/extensions/video'; import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils'; import { PROJECT_WIKI_ATTACHMENT_IMAGE_HTML, PROJECT_WIKI_ATTACHMENT_AUDIO_HTML, PROJECT_WIKI_ATTACHMENT_VIDEO_HTML, + PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML, } from '../../test_constants'; -const TIPTAP_IMAGE_HTML = `<p> +const TIPTAP_AUDIO_HTML = `<p> + <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span> +</p>`; + +const TIPTAP_DIAGRAM_HTML = `<p> <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon"> </p>`; -const TIPTAP_AUDIO_HTML = `<p> - <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span> +const TIPTAP_IMAGE_HTML = `<p> + <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon"> </p>`; const TIPTAP_VIDEO_HTML = `<p> @@ -29,10 +35,11 @@ const TIPTAP_VIDEO_HTML = `<p> const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); describe.each` - mediaType | mediaHTML | filePath | mediaOutputHTML - ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML} - ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML} - ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML} + mediaType | mediaHTML | filePath | mediaOutputHTML + ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML} + ${'drawio_diagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${'test-file.drawio.svg'} | ${TIPTAP_DIAGRAM_HTML} + ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML} + ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML} `( 'content_editor/components/bubble_menus/media_bubble_menu ($mediaType)', ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => { @@ -43,7 +50,7 @@ describe.each` let eventHub; const buildEditor = () => { - tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] }); + tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video, DrawioDiagram] }); contentEditor = { resolveUrl: jest.fn() }; eventHub = eventHubFactory(); }; @@ -93,10 +100,6 @@ describe.each` bubbleMenu = wrapper.findComponent(BubbleMenu); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders bubble menu component', async () => { expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); }); @@ -114,6 +117,24 @@ describe.each` expect(link.text()).toBe(filePath); }); + describe('when BubbleMenu emits hidden event', () => { + it('resets media bubble menu state', async () => { + // Switch to edit mode to access component state in form fields + await wrapper.findByTestId('edit-media').vm.$emit('click'); + + const mediaSrcInput = wrapper.findByTestId('media-src').vm.$el; + const mediaAltInput = wrapper.findByTestId('media-alt').vm.$el; + + expect(mediaSrcInput.value).not.toBe(''); + expect(mediaAltInput.value).not.toBe(''); + + await wrapper.findComponent(BubbleMenu).vm.$emit('hidden'); + + expect(mediaSrcInput.value).toBe(''); + expect(mediaAltInput.value).toBe(''); + }); + }); + describe('copy button', () => { it(`copies the canonical link to the ${mediaType} to clipboard`, async () => { jest.spyOn(navigator.clipboard, 'writeText'); @@ -133,23 +154,39 @@ describe.each` }); describe(`replace ${mediaType} button`, () => { - it('uploads and replaces the selected image when file input changes', async () => { - const commands = mockChainedCommands(tiptapEditor, [ - 'focus', - 'deleteSelection', - 'uploadAttachment', - 'run', - ]); - const file = new File(['foo'], 'foo.png', { type: 'image/png' }); - - await wrapper.findByTestId('replace-media').vm.$emit('click'); - await selectFile(file); - - expect(commands.focus).toHaveBeenCalled(); - expect(commands.deleteSelection).toHaveBeenCalled(); - expect(commands.uploadAttachment).toHaveBeenCalledWith({ file }); - expect(commands.run).toHaveBeenCalled(); - }); + if (mediaType !== 'drawio_diagram') { + it('uploads and replaces the selected image when file input changes', async () => { + const commands = mockChainedCommands(tiptapEditor, [ + 'focus', + 'deleteSelection', + 'uploadAttachment', + 'run', + ]); + const file = new File(['foo'], 'foo.png', { type: 'image/png' }); + + await wrapper.findByTestId('replace-media').vm.$emit('click'); + await selectFile(file); + + expect(commands.focus).toHaveBeenCalled(); + expect(commands.deleteSelection).toHaveBeenCalled(); + expect(commands.uploadAttachment).toHaveBeenCalledWith({ file }); + expect(commands.run).toHaveBeenCalled(); + }); + } else { + // draw.io diagrams are replaced using the edit diagram button + it('invokes editDiagram command', async () => { + const commands = mockChainedCommands(tiptapEditor, [ + 'focus', + 'createOrEditDiagram', + 'run', + ]); + await wrapper.findByTestId('edit-diagram').vm.$emit('click'); + + expect(commands.focus).toHaveBeenCalled(); + expect(commands.createOrEditDiagram).toHaveBeenCalled(); + expect(commands.run).toHaveBeenCalled(); + }); + } }); describe('edit button', () => { diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js index ee9ead8f8a7..e62e2331d25 100644 --- a/spec/frontend/content_editor/components/content_editor_alert_spec.js +++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js @@ -29,10 +29,6 @@ describe('content_editor/components/content_editor_alert', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` variant | message ${'danger'} | ${'An error occurred'} diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 1a3cd36a8bb..b642ac9c46b 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -1,7 +1,8 @@ -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { EditorContent, Editor } from '@tiptap/vue-2'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue'; import ContentEditor from '~/content_editor/components/content_editor.vue'; import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue'; @@ -27,19 +28,23 @@ describe('ContentEditor', () => { const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator); const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert); - const createWrapper = ({ markdown, autofocus, useBottomToolbar } = {}) => { + const createWrapper = ({ markdown, autofocus, ...props } = {}) => { wrapper = shallowMountExtended(ContentEditor, { propsData: { renderMarkdown, uploadsPath, markdown, autofocus, - useBottomToolbar, + placeholder: 'Enter some text here...', + ...props, }, stubs: { EditorStateObserver, ContentEditorProvider, ContentEditorAlert, + GlLink, + GlSprintf, + EditorModeDropdown, }, }); }; @@ -48,10 +53,6 @@ describe('ContentEditor', () => { renderMarkdown = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('triggers initialized event', () => { createWrapper(); @@ -87,22 +88,29 @@ describe('ContentEditor', () => { expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true); }); - it('renders top toolbar component', () => { + it('renders toolbar component', () => { createWrapper(); expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true); - expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(false); - expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(true); }); - it('renders bottom toolbar component', () => { - createWrapper({ - useBottomToolbar: true, - }); + it('renders footer containing quick actions help text if quick actions docs path is defined', () => { + createWrapper({ quickActionsDocsPath: '/foo/bar' }); - expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true); - expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(true); - expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(false); + expect(findEditorElement().text()).toContain('For quick actions, type /'); + expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar'); + }); + + it('does not render footer containing quick actions help text if quick actions docs path is not defined', () => { + createWrapper(); + + expect(findEditorElement().text()).not.toContain('For quick actions, type /'); + }); + + it('renders an editor mode dropdown', () => { + createWrapper(); + + expect(wrapper.findComponent(EditorModeDropdown).exists()).toBe(true); }); describe('when setting initial content', () => { @@ -124,9 +132,9 @@ describe('ContentEditor', () => { describe('succeeds', () => { beforeEach(async () => { - renderMarkdown.mockResolvedValueOnce('hello world'); + renderMarkdown.mockResolvedValueOnce(''); - createWrapper({ markddown: 'hello world' }); + createWrapper({ markddown: '' }); await nextTick(); }); @@ -138,13 +146,17 @@ describe('ContentEditor', () => { it('emits loadingSuccess event', () => { expect(wrapper.emitted('loadingSuccess')).toHaveLength(1); }); + + it('shows placeholder text', () => { + expect(wrapper.text()).toContain('Enter some text here...'); + }); }); describe('fails', () => { beforeEach(async () => { renderMarkdown.mockRejectedValueOnce(new Error()); - createWrapper({ markddown: 'hello world' }); + createWrapper({ markdown: 'hello world' }); await nextTick(); }); @@ -209,11 +221,17 @@ describe('ContentEditor', () => { expect(findEditorElement().classes()).not.toContain('is-focused'); }); + + it('hides placeholder text', () => { + expect(wrapper.text()).not.toContain('Enter some text here...'); + }); }); describe('when editorStateObserver emits docUpdate event', () => { - it('emits change event with the latest markdown', async () => { - const markdown = 'Loaded content'; + let markdown; + + beforeEach(async () => { + markdown = 'Loaded content'; renderMarkdown.mockResolvedValueOnce(markdown); @@ -223,7 +241,9 @@ describe('ContentEditor', () => { await waitForPromises(); findEditorStateObserver().vm.$emit('docUpdate'); + }); + it('emits change event with the latest markdown', () => { expect(wrapper.emitted('change')).toEqual([ [ { @@ -234,6 +254,10 @@ describe('ContentEditor', () => { ], ]); }); + + it('hides the placeholder text', () => { + expect(wrapper.text()).not.toContain('Enter some text here...'); + }); }); describe('when editorStateObserver emits keydown event', () => { diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js index 9b42f61c98c..80fb20e5258 100644 --- a/spec/frontend/content_editor/components/editor_state_observer_spec.js +++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js @@ -45,10 +45,6 @@ describe('content_editor/components/editor_state_observer', () => { buildEditor(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when editor content changes', () => { it('emits update, selectionUpdate, and transaction events', () => { const content = '<p>My paragraph</p>'; diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js index c4bf21ba813..4a7b7cedf19 100644 --- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js +++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js @@ -1,3 +1,4 @@ +import { GlTabs, GlTab } from '@gitlab/ui'; import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue'; @@ -6,22 +7,23 @@ import { CONTENT_EDITOR_TRACKING_LABEL, } from '~/content_editor/constants'; -describe('content_editor/components/top_toolbar', () => { +describe('content_editor/components/formatting_toolbar', () => { let wrapper; let trackingSpy; const buildWrapper = () => { - wrapper = shallowMountExtended(FormattingToolbar); + wrapper = shallowMountExtended(FormattingToolbar, { + stubs: { + GlTabs, + GlTab, + }, + }); }; beforeEach(() => { trackingSpy = mockTracking(undefined, null, jest.spyOn); }); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` testId | controlProps ${'text-styles'} | ${{}} diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js index 0065103d01b..1b0ffaee6c6 100644 --- a/spec/frontend/content_editor/components/loading_indicator_spec.js +++ b/spec/frontend/content_editor/components/loading_indicator_spec.js @@ -11,10 +11,6 @@ describe('content_editor/components/loading_indicator', () => { wrapper = shallowMountExtended(LoadingIndicator); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when loading content', () => { beforeEach(() => { createWrapper(); diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js index 1f1f7b338c6..1556f761682 100644 --- a/spec/frontend/content_editor/components/toolbar_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_button_spec.js @@ -42,10 +42,6 @@ describe('content_editor/components/toolbar_button', () => { buildEditor(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays tertiary, medium button with a provided label and icon', () => { buildWrapper(); diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js index 5473d43f5a1..0ec950137fc 100644 --- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js @@ -50,7 +50,6 @@ describe('content_editor/components/toolbar_image_button', () => { afterEach(() => { editor.destroy(); - wrapper.destroy(); }); it('sets the image to the value in the URL input when "Insert" button is clicked', async () => { diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js index 40e859e96af..80090c0278f 100644 --- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js @@ -43,7 +43,6 @@ describe('content_editor/components/toolbar_link_button', () => { afterEach(() => { editor.destroy(); - wrapper.destroy(); }); it('renders dropdown component', () => { 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 d4fc47601cf..5af4784f358 100644 --- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js +++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js @@ -9,12 +9,14 @@ import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_ describe('content_editor/components/toolbar_more_dropdown', () => { let wrapper; let tiptapEditor; + let contentEditor; let eventHub; const buildEditor = () => { tiptapEditor = createTestEditor({ extensions: [Diagram, HorizontalRule], }); + contentEditor = { drawioEnabled: true }; eventHub = eventHubFactory(); }; @@ -22,6 +24,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => { wrapper = mountExtended(ToolbarMoreDropdown, { provide: { tiptapEditor, + contentEditor, eventHub, }, propsData, @@ -32,29 +35,27 @@ describe('content_editor/components/toolbar_more_dropdown', () => { beforeEach(() => { buildEditor(); - buildWrapper(); - }); - - afterEach(() => { - wrapper.destroy(); }); 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' }]} - ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]} - ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]} + 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'} | ${[]} + ${'Create or edit diagram'} | ${'drawioDiagram'} | ${'createOrEditDiagram'} | ${[]} `('when option $name is clicked', ({ name, command, contentType, params }) => { let commands; let btn; beforeEach(async () => { + buildWrapper(); + commands = mockChainedCommands(tiptapEditor, [command, 'focus', 'run']); btn = wrapper.findByRole('button', { name }); }); @@ -71,8 +72,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => { }); }); + it('does not show drawio option when drawio is disabled', () => { + contentEditor.drawioEnabled = false; + buildWrapper(); + + expect(wrapper.findByRole('button', { name: 'Create or edit diagram' }).exists()).toBe(false); + }); + describe('a11y tests', () => { it('sets toggleText and text-sr-only properties to the table button dropdown', () => { + buildWrapper(); + expect(findDropdown().props()).toMatchObject({ textSrOnly: true, toggleText: 'More options', diff --git a/spec/frontend/content_editor/components/toolbar_table_button_spec.js b/spec/frontend/content_editor/components/toolbar_table_button_spec.js index aa4604661e5..35741971488 100644 --- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js @@ -30,7 +30,6 @@ describe('content_editor/components/toolbar_table_button', () => { afterEach(() => { editor.destroy(); - wrapper.destroy(); }); it('renders a grid of 5x5 buttons to create a table', () => { diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js index 5a725ac1ca4..31ed13541e6 100644 --- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js +++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js @@ -39,10 +39,6 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => { buildEditor(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all text styles as dropdown items', () => { buildWrapper(); diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js index a5ef19fb8e8..057e50cd0e2 100644 --- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js +++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js @@ -55,10 +55,6 @@ describe('content/components/wrappers/code_block', () => { codeBlockLanguageLoader.findOrCreateLanguageBySyntax.mockReturnValue({ syntax: language }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a node-view-wrapper as a pre element', () => { createWrapper(); diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js index d746b9fa2f1..232c1e9aede 100644 --- a/spec/frontend/content_editor/components/wrappers/details_spec.js +++ b/spec/frontend/content_editor/components/wrappers/details_spec.js @@ -13,10 +13,6 @@ describe('content/components/wrappers/details', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders a node-view-content as a ul element', () => { createWrapper(); diff --git a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js index 1ff750eb2ac..91c6799478e 100644 --- a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js +++ b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js @@ -12,10 +12,6 @@ describe('content/components/wrappers/footnote_definition', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders footnote label as a readyonly element', () => { const label = 'footnote'; diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/label_spec.js index 9e58669b0ea..fa32b746142 100644 --- a/spec/frontend/content_editor/components/wrappers/label_spec.js +++ b/spec/frontend/content_editor/components/wrappers/label_spec.js @@ -11,10 +11,6 @@ describe('content/components/wrappers/label', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it("renders a GlLabel with the node's text and color", () => { createWrapper({ attrs: { color: '#ff0000', text: 'foo bar', originalText: '~"foo bar"' } }); 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 1fdddce3962..d8f34565705 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 '@_ueberdosis/prosemirror-tables'; +import { selectedRect as getSelectedRect } from '@tiptap/pm/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('@_ueberdosis/prosemirror-tables'); +jest.mock('@tiptap/pm/tables'); describe('content/components/wrappers/table_cell_base', () => { let wrapper; @@ -52,10 +52,6 @@ describe('content/components/wrappers/table_cell_base', () => { editor = createTestEditor({}); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a td node-view-wrapper with relative position', () => { createWrapper(); expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative'); diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js index 2aefbc77545..506f442bcc7 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js @@ -22,10 +22,6 @@ describe('content/components/wrappers/table_cell_body', () => { editor = createTestEditor({}); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a TableCellBase component', () => { createWrapper(); expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({ diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js index e48df8734a6..bebe7fb4124 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js @@ -22,10 +22,6 @@ describe('content/components/wrappers/table_cell_header', () => { editor = createTestEditor({}); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a TableCellBase component', () => { createWrapper(); expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({ 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 index bfda89a8b09..4d5911dda0c 100644 --- a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js @@ -70,10 +70,6 @@ describe('content/components/wrappers/table_of_contents', () => { await nextTick(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a node-view-wrapper as a ul element', () => { expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('ul'); }); diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index 6b804b3b4c6..24b75ba6805 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -2,12 +2,13 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import Attachment from '~/content_editor/extensions/attachment'; +import DrawioDiagram from '~/content_editor/extensions/drawio_diagram'; import Image from '~/content_editor/extensions/image'; import Audio from '~/content_editor/extensions/audio'; import Video from '~/content_editor/extensions/video'; import Link from '~/content_editor/extensions/link'; import Loading from '~/content_editor/extensions/loading'; -import { VARIANT_DANGER } from '~/flash'; +import { VARIANT_DANGER } from '~/alert'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, createDocBuilder } from '../test_utils'; @@ -16,6 +17,7 @@ import { PROJECT_WIKI_ATTACHMENT_AUDIO_HTML, PROJECT_WIKI_ATTACHMENT_VIDEO_HTML, PROJECT_WIKI_ATTACHMENT_LINK_HTML, + PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML, } from '../test_constants'; describe('content_editor/extensions/attachment', () => { @@ -24,6 +26,7 @@ describe('content_editor/extensions/attachment', () => { let p; let image; let audio; + let drawioDiagram; let video; let loading; let link; @@ -35,6 +38,7 @@ describe('content_editor/extensions/attachment', () => { const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' }); const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' }); const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' }); + const drawioDiagramFile = new File(['foo'], 'test-file.drawio.svg', { type: 'image/svg+xml' }); const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' }); const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => { @@ -67,12 +71,13 @@ describe('content_editor/extensions/attachment', () => { Image, Audio, Video, + DrawioDiagram, Attachment.configure({ renderMarkdown, uploadsPath, eventHub }), ], }); ({ - builders: { doc, p, image, audio, video, loading, link }, + builders: { doc, p, image, audio, video, loading, link, drawioDiagram }, } = createDocBuilder({ tiptapEditor, names: { @@ -81,6 +86,7 @@ describe('content_editor/extensions/attachment', () => { link: { nodeType: Link.name }, audio: { nodeType: Audio.name }, video: { nodeType: Video.name }, + drawioDiagram: { nodeType: DrawioDiagram.name }, }, })); @@ -113,10 +119,11 @@ describe('content_editor/extensions/attachment', () => { }); describe.each` - nodeType | mimeType | html | file | mediaType - ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)} - ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)} - ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)} + nodeType | mimeType | html | file | mediaType + ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)} + ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)} + ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)} + ${'drawioDiagram'} | ${'image/svg+xml'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${drawioDiagramFile} | ${(attrs) => drawioDiagram(attrs)} `('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => { const base64EncodedFile = `data:${mimeType};base64,Zm9v`; @@ -151,7 +158,7 @@ describe('content_editor/extensions/attachment', () => { mediaType({ canonicalSrc: file.name, src: base64EncodedFile, - alt: 'test-file', + alt: expect.stringContaining('test-file'), uploading: false, }), ), diff --git a/spec/frontend/content_editor/extensions/drawio_diagram_spec.js b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js new file mode 100644 index 00000000000..61dc164c99a --- /dev/null +++ b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js @@ -0,0 +1,103 @@ +import DrawioDiagram from '~/content_editor/extensions/drawio_diagram'; +import Image from '~/content_editor/extensions/image'; +import createAssetResolver from '~/content_editor/services/asset_resolver'; +import { create } from '~/drawio/content_editor_facade'; +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { createTestEditor, createDocBuilder } from '../test_utils'; +import { + PROJECT_WIKI_ATTACHMENT_IMAGE_HTML, + PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML, +} from '../test_constants'; + +jest.mock('~/content_editor/services/asset_resolver'); +jest.mock('~/drawio/content_editor_facade'); +jest.mock('~/drawio/drawio_editor'); + +describe('content_editor/extensions/drawio_diagram', () => { + let tiptapEditor; + let doc; + let paragraph; + let image; + let drawioDiagram; + const uploadsPath = '/uploads'; + const renderMarkdown = () => {}; + + beforeEach(() => { + tiptapEditor = createTestEditor({ + extensions: [Image, DrawioDiagram.configure({ uploadsPath, renderMarkdown })], + }); + const { builders } = createDocBuilder({ + tiptapEditor, + names: { + image: { nodeType: Image.name }, + drawioDiagram: { nodeType: DrawioDiagram.name }, + }, + }); + + doc = builders.doc; + paragraph = builders.paragraph; + image = builders.image; + drawioDiagram = builders.drawioDiagram; + }); + + describe('parsing', () => { + it('distinguishes a drawio diagram from an image', () => { + const expectedDocWithDiagram = doc( + paragraph( + drawioDiagram({ + alt: 'test-file', + canonicalSrc: 'test-file.drawio.svg', + src: '/group1/project1/-/wikis/test-file.drawio.svg', + }), + ), + ); + const expectedDocWithImage = doc( + paragraph( + image({ + alt: 'test-file', + canonicalSrc: 'test-file.png', + src: '/group1/project1/-/wikis/test-file.png', + }), + ), + ); + tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML); + + expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithDiagram.toJSON()); + + tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML); + + expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithImage.toJSON()); + }); + }); + + describe('createOrEditDiagram command', () => { + let editorFacade; + let assetResolver; + + beforeEach(() => { + editorFacade = {}; + assetResolver = {}; + tiptapEditor.commands.createOrEditDiagram(); + + create.mockReturnValueOnce(editorFacade); + createAssetResolver.mockReturnValueOnce(assetResolver); + }); + + it('creates a new instance of asset resolver', () => { + expect(createAssetResolver).toHaveBeenCalledWith({ renderMarkdown }); + }); + + it('creates a new instance of the content_editor_facade', () => { + expect(create).toHaveBeenCalledWith({ + tiptapEditor, + drawioNodeName: DrawioDiagram.name, + uploadsPath, + assetResolver, + }); + }); + + it('calls launchDrawioEditor and provides content_editor_facade', () => { + expect(launchDrawioEditor).toHaveBeenCalledWith({ editorFacade }); + }); + }); +}); diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js index 30e798e8817..8f3a4934e77 100644 --- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js +++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js @@ -3,7 +3,7 @@ import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight import Diagram from '~/content_editor/extensions/diagram'; import Frontmatter from '~/content_editor/extensions/frontmatter'; import Bold from '~/content_editor/extensions/bold'; -import { VARIANT_DANGER } from '~/flash'; +import { VARIANT_DANGER } from '~/alert'; import eventHubFactory from '~/helpers/event_hub_factory'; import { ALERT_EVENT } from '~/content_editor/constants'; import waitForPromises from 'helpers/wait_for_promises'; 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 5df901e0f15..bf29d4bdf23 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 @@ -1,4 +1,4 @@ -import { DOMSerializer } from 'prosemirror-model'; +import { DOMSerializer } from '@tiptap/pm/model'; import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTiptapEditor } from 'jest/content_editor/test_utils'; diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js index e1a30819ac8..00cc628ca72 100644 --- a/spec/frontend/content_editor/services/create_content_editor_spec.js +++ b/spec/frontend/content_editor/services/create_content_editor_spec.js @@ -20,7 +20,7 @@ describe('content_editor/services/create_content_editor', () => { preserveUnchangedMarkdown: false, }, }; - editor = createContentEditor({ renderMarkdown, uploadsPath }); + editor = createContentEditor({ renderMarkdown, uploadsPath, drawioEnabled: true }); }); describe('when preserveUnchangedMarkdown feature is on', () => { @@ -45,10 +45,10 @@ describe('content_editor/services/create_content_editor', () => { }); }); - it('sets gl-outline-0! class selector to the tiptapEditor instance', () => { + it('sets gl-shadow-none! class selector to the tiptapEditor instance', () => { expect(editor.tiptapEditor.options.editorProps).toMatchObject({ attributes: { - class: 'gl-outline-0!', + class: 'gl-shadow-none!', }, }); }); @@ -82,4 +82,14 @@ describe('content_editor/services/create_content_editor', () => { renderMarkdown, }); }); + + it('provides uploadsPath and renderMarkdown function to DrawioDiagram extension', () => { + expect( + editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'drawioDiagram') + .options, + ).toMatchObject({ + uploadsPath, + renderMarkdown, + }); + }); }); diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js index 90d83820c70..8ee37282ee9 100644 --- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js +++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js @@ -35,12 +35,10 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => { beforeEach(async () => { const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); - renderMarkdown.mockResolvedValueOnce( - `<p><strong>${text}</strong></p><pre lang="javascript"></pre><!-- some comment -->`, - ); + renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p><!-- some comment -->`); result = await deserializer.deserialize({ - content: 'content', + markdown: '**Bold text**\n<!-- some comment -->', schema: tiptapEditor.schema, }); }); @@ -53,12 +51,22 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => { }); describe('when the render function returns an empty value', () => { - it('returns an empty object', async () => { - const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); + it('returns an empty prosemirror document', async () => { + const deserializer = createMarkdownDeserializer({ + render: renderMarkdown, + schema: tiptapEditor.schema, + }); renderMarkdown.mockResolvedValueOnce(null); - expect(await deserializer.deserialize({ content: 'content' })).toEqual({}); + const result = await deserializer.deserialize({ + markdown: '', + schema: tiptapEditor.schema, + }); + + const document = doc(p()); + + expect(result.document.toJSON()).toEqual(document.toJSON()); }); }); }); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 2cd8b8a0d6f..c4d302547a5 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -8,6 +8,7 @@ import DescriptionItem from '~/content_editor/extensions/description_item'; import DescriptionList from '~/content_editor/extensions/description_list'; import Details from '~/content_editor/extensions/details'; import DetailsContent from '~/content_editor/extensions/details_content'; +import DrawioDiagram from '~/content_editor/extensions/drawio_diagram'; import Emoji from '~/content_editor/extensions/emoji'; import Figure from '~/content_editor/extensions/figure'; import FigureCaption from '~/content_editor/extensions/figure_caption'; @@ -57,6 +58,7 @@ const { div, descriptionItem, descriptionList, + drawioDiagram, emoji, footnoteDefinition, footnoteReference, @@ -96,6 +98,7 @@ const { detailsContent: { nodeType: DetailsContent.name }, descriptionItem: { nodeType: DescriptionItem.name }, descriptionList: { nodeType: DescriptionList.name }, + drawioDiagram: { nodeType: DrawioDiagram.name }, emoji: { markType: Emoji.name }, figure: { nodeType: Figure.name }, figureCaption: { nodeType: FigureCaption.name }, @@ -397,6 +400,12 @@ this is not really json:table but just trying out whether this case works or not ); }); + it('correctly serializes a drawio_diagram', () => { + expect( + serialize(paragraph(drawioDiagram({ src: 'diagram.drawio.svg', alt: 'Draw.io Diagram' }))), + ).toBe('![Draw.io Diagram](diagram.drawio.svg)'); + }); + it.each` width | height | outputAttributes ${300} | ${undefined} | ${'width=300'} diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js index 45a0e4a8bd1..bd462ecec22 100644 --- a/spec/frontend/content_editor/test_constants.js +++ b/spec/frontend/content_editor/test_constants.js @@ -20,6 +20,12 @@ export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" </span> </p>`; +export const PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML = `<p data-sourcepos="1:1-1:27" dir="auto"> + <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.drawio.svg" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.drawio.svg"> + <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.drawio.svg" data-canonical-src="test-file.drawio.svg"> + </a> +</p>`; + export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto"> <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a> </p>`; diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js index 0fa0e65cd26..16f90a15c24 100644 --- a/spec/frontend/content_editor/test_utils.js +++ b/spec/frontend/content_editor/test_utils.js @@ -17,6 +17,7 @@ import DescriptionList from '~/content_editor/extensions/description_list'; import Details from '~/content_editor/extensions/details'; import DetailsContent from '~/content_editor/extensions/details_content'; import Diagram from '~/content_editor/extensions/diagram'; +import DrawioDiagram from '~/content_editor/extensions/drawio_diagram'; import Emoji from '~/content_editor/extensions/emoji'; import FootnoteDefinition from '~/content_editor/extensions/footnote_definition'; import FootnoteReference from '~/content_editor/extensions/footnote_reference'; @@ -218,6 +219,7 @@ export const createTiptapEditor = (extensions = []) => DescriptionList, Details, DetailsContent, + DrawioDiagram, Diagram, Emoji, FootnoteDefinition, diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap index 2f441f0f747..4b7439f6fd2 100644 --- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap +++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap @@ -53,23 +53,22 @@ exports[`Contributors charts should render charts and a RefSelector when loading Excluding merge commits. Limited to 6,000 commits. </span> - <div> - <glareachart-stub - annotations="" - class="gl-mb-5" - data="[object Object]" - height="264" - includelegendavgmax="true" - legendaveragetext="Avg" - legendcurrenttext="Current" - legendlayout="inline" - legendmaxtext="Max" - legendmintext="Min" - option="[object Object]" - thresholds="" - width="0" - /> - </div> + <glareachart-stub + annotations="" + class="gl-mb-5" + data="[object Object]" + height="264" + includelegendavgmax="true" + legendaveragetext="Avg" + legendcurrenttext="Current" + legendlayout="inline" + legendmaxtext="Max" + legendmintext="Min" + option="[object Object]" + responsive="" + thresholds="" + width="auto" + /> <div class="row" @@ -91,22 +90,21 @@ exports[`Contributors charts should render charts and a RefSelector when loading </p> - <div> - <glareachart-stub - annotations="" - data="[object Object]" - height="216" - includelegendavgmax="true" - legendaveragetext="Avg" - legendcurrenttext="Current" - legendlayout="inline" - legendmaxtext="Max" - legendmintext="Min" - option="[object Object]" - thresholds="" - width="0" - /> - </div> + <glareachart-stub + annotations="" + data="[object Object]" + height="216" + includelegendavgmax="true" + legendaveragetext="Avg" + legendcurrenttext="Current" + legendlayout="inline" + legendmaxtext="Max" + legendmintext="Min" + option="[object Object]" + responsive="" + thresholds="" + width="auto" + /> </div> </div> </div> diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js index 03b1e977548..f915b834aff 100644 --- a/spec/frontend/contributors/component/contributors_spec.js +++ b/spec/frontend/contributors/component/contributors_spec.js @@ -1,5 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ContributorsCharts from '~/contributors/components/contributors.vue'; import { createStore } from '~/contributors/stores'; @@ -16,7 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({ let wrapper; let mock; let store; -const Component = Vue.extend(ContributorsCharts); const endpoint = 'contributors/-/graphs'; const branch = 'main'; const chartData = [ @@ -32,7 +31,7 @@ function factory() { mock.onGet().reply(HTTP_STATUS_OK, chartData); store = createStore(); - wrapper = mountExtended(Component, { + wrapper = mountExtended(ContributorsCharts, { propsData: { endpoint, branch, @@ -60,7 +59,6 @@ describe('Contributors charts', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); it('should fetch chart data when mounted', () => { diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js index b2ebdf2f53c..a15b9ad2978 100644 --- a/spec/frontend/contributors/store/actions_spec.js +++ b/spec/frontend/contributors/store/actions_spec.js @@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/contributors/stores/actions'; import * as types from '~/contributors/stores/mutation_types'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('Contributors store actions', () => { describe('fetchChartData', () => { @@ -38,7 +38,7 @@ describe('Contributors store actions', () => { ); }); - it('should show flash on API error', async () => { + it('should show alert on API error', async () => { mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found'); await testAction( diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js index 50b432943fb..2fb6940a415 100644 --- a/spec/frontend/crm/contact_form_wrapper_spec.js +++ b/spec/frontend/crm/contact_form_wrapper_spec.js @@ -47,7 +47,6 @@ describe('Customer relations contact form wrapper', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js index ec7172434bf..63b64a6c984 100644 --- a/spec/frontend/crm/contacts_root_spec.js +++ b/spec/frontend/crm/contacts_root_spec.js @@ -61,7 +61,6 @@ describe('Customer relations contacts root app', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; router = null; }); diff --git a/spec/frontend/crm/crm_form_spec.js b/spec/frontend/crm/crm_form_spec.js index eabcf5b1b1b..fabf43ceb9d 100644 --- a/spec/frontend/crm/crm_form_spec.js +++ b/spec/frontend/crm/crm_form_spec.js @@ -188,10 +188,6 @@ describe('Reusable form component', () => { }; const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]); - afterEach(() => { - wrapper.destroy(); - }); - describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT))( '%s form save button', (name, { mountFunction }) => { diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js index d795c585622..8408c1920a9 100644 --- a/spec/frontend/crm/organization_form_wrapper_spec.js +++ b/spec/frontend/crm/organization_form_wrapper_spec.js @@ -40,10 +40,6 @@ describe('Customer relations organization form wrapper', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('in edit mode', () => { it('should render organization form with correct props', () => { mountComponent({ isEditMode: true }); diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js index 1fcf6aa8f50..0b26a49a6b3 100644 --- a/spec/frontend/crm/organizations_root_spec.js +++ b/spec/frontend/crm/organizations_root_spec.js @@ -65,7 +65,6 @@ describe('Customer relations organizations root app', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; router = null; }); diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js index 7d9ae548c9a..12fef9d5ddf 100644 --- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js +++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js @@ -42,7 +42,6 @@ describe('custom metrics form fields component', () => { }); afterEach(() => { - wrapper.destroy(); mockAxios.restore(); }); diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js index af56b94f90b..c633583f2cb 100644 --- a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js +++ b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js @@ -26,10 +26,6 @@ describe('CustomMetricsForm', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('Computed', () => { it('Form button and title text indicate the custom metric is being edited', () => { mountComponent({ metricPersisted: true }); diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js index 113e0d8f60d..77118ae140a 100644 --- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js @@ -46,11 +46,6 @@ describe('Deploy freeze modal', () => { wrapper.findComponent(TimezoneDropdown).trigger('input'); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('Basic interactions', () => { it('button is disabled when freeze period is invalid', () => { expect(submitDeployFreezeButton().attributes('disabled')).toBe('true'); diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js index 27d8fea9d5e..883cc6a344a 100644 --- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js @@ -24,11 +24,6 @@ describe('Deploy freeze settings', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('Deploy freeze table contains components', () => { it('contains deploy freeze table', () => { expect(wrapper.findComponent(DeployFreezeTable).exists()).toBe(true); diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js index c2d6eb399bc..6a9e482a184 100644 --- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js @@ -37,11 +37,6 @@ describe('Deploy freeze table', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('dispatches fetchFreezePeriods when mounted', () => { expect(store.dispatch).toHaveBeenCalledWith('fetchFreezePeriods'); }); diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js index 9b96ce5d252..d39577baa59 100644 --- a/spec/frontend/deploy_freeze/store/actions_spec.js +++ b/spec/frontend/deploy_freeze/store/actions_spec.js @@ -4,14 +4,14 @@ import Api from '~/api'; import * as actions from '~/deploy_freeze/store/actions'; import * as types from '~/deploy_freeze/store/mutation_types'; import getInitialState from '~/deploy_freeze/store/state'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import * as logger from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; import { freezePeriodsFixture } from '../helpers'; import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers'; jest.mock('~/api.js'); -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('deploy freeze store actions', () => { const freezePeriodFixture = freezePeriodsFixture[0]; diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js index d11ecf95de6..3dfb828b449 100644 --- a/spec/frontend/deploy_keys/components/app_spec.js +++ b/spec/frontend/deploy_keys/components/app_spec.js @@ -33,7 +33,6 @@ describe('Deploy keys app component', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js index 8599c55c908..5f20d4ad542 100644 --- a/spec/frontend/deploy_keys/components/key_spec.js +++ b/spec/frontend/deploy_keys/components/key_spec.js @@ -26,11 +26,6 @@ describe('Deploy keys key', () => { store.keys = data; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('enabled key', () => { const deployKey = data.enabled_keys[0]; diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js index f5f76d5d493..e0f86aadad4 100644 --- a/spec/frontend/deploy_keys/components/keys_panel_spec.js +++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js @@ -23,11 +23,6 @@ describe('Deploy keys panel', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders list of keys', () => { mountComponent(); expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length); diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js index 46f7b2f3604..a3fdab88270 100644 --- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js +++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js @@ -7,20 +7,12 @@ import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/h import { TEST_HOST } from 'helpers/test_constants'; import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_INFO } from '~/alert'; const createNewTokenPath = `${TEST_HOST}/create`; const deployTokensHelpUrl = `${TEST_HOST}/help`; -jest.mock('~/flash', () => { - const original = jest.requireActual('~/flash'); - - return { - __esModule: true, - ...original, - createAlert: jest.fn(), - }; -}); +jest.mock('~/alert'); describe('New Deploy Token', () => { let wrapper; @@ -43,13 +35,12 @@ describe('New Deploy Token', () => { createNewTokenPath, tokenType, }, + stubs: { + GlFormCheckbox, + }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('without a container registry', () => { beforeEach(() => { wrapper = factory({ containerRegistryEnabled: false }); @@ -69,7 +60,7 @@ describe('New Deploy Token', () => { it('should show the read registry scope', () => { const checkbox = wrapper.findAllComponents(GlFormCheckbox).at(1); - expect(checkbox.text()).toBe('read_registry'); + expect(checkbox.text()).toContain('read_registry'); }); function submitTokenThenCheck() { @@ -91,7 +82,7 @@ describe('New Deploy Token', () => { }); } - it('should flash error message if token creation fails', async () => { + it('should alert error message if token creation fails', async () => { const mockAxios = new MockAdapter(axios); const date = new Date(); @@ -222,4 +213,32 @@ describe('New Deploy Token', () => { return submitTokenThenCheck(); }); }); + + describe('help text for write_package_registry scope', () => { + const findWriteRegistryScopeCheckbox = () => wrapper.findAllComponents(GlFormCheckbox).at(4); + + describe('with project tokenType', () => { + beforeEach(() => { + wrapper = factory(); + }); + + it('should show the correct help text', () => { + expect(findWriteRegistryScopeCheckbox().text()).toContain( + 'Allows read, write and delete access to the package registry.', + ); + }); + }); + + describe('with group tokenType', () => { + beforeEach(() => { + wrapper = factory({ tokenType: 'group' }); + }); + + it('should show the correct help text', () => { + expect(findWriteRegistryScopeCheckbox().text()).toContain( + 'Allows read and write access to the package registry.', + ); + }); + }); + }); }); diff --git a/spec/frontend/deploy_tokens/components/revoke_button_spec.js b/spec/frontend/deploy_tokens/components/revoke_button_spec.js index fa2a7d9b155..6e81205d1c1 100644 --- a/spec/frontend/deploy_tokens/components/revoke_button_spec.js +++ b/spec/frontend/deploy_tokens/components/revoke_button_spec.js @@ -52,10 +52,6 @@ describe('RevokeButton', () => { ); } - afterEach(() => { - wrapper.destroy(); - }); - const findRevokeButton = () => wrapper.findByTestId('revoke-button'); const findModal = () => wrapper.findComponent(GlModal); const findPrimaryModalButton = () => wrapper.findByTestId('primary-revoke-btn'); diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js index 426a61f5a47..81e3b21a910 100644 --- a/spec/frontend/design_management/components/delete_button_spec.js +++ b/spec/frontend/design_management/components/delete_button_spec.js @@ -21,10 +21,6 @@ describe('Batch delete button component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders non-disabled button by default', () => { createComponent(); diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap index 402e55347af..e2f1d6e4b10 100644 --- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap +++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap @@ -70,6 +70,8 @@ exports[`Design note component should match the snapshot 1`] = ` > <!----> + + <!----> </div> </div> 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 2091e1e08dd..56bf0fa60a7 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 @@ -1,18 +1,22 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import { ApolloMutation } from 'vue-apollo'; import { nextTick } from 'vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import DesignNote from '~/design_management/components/design_notes/design_note.vue'; import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue'; -import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql'; import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import destroyNoteMutation from '~/design_management/graphql/mutations/destroy_note.mutation.graphql'; +import { DELETE_NOTE_ERROR_MSG } from '~/design_management/constants'; import mockDiscussion from '../../mock_data/discussion'; import notes from '../../mock_data/notes'; +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); + const defaultMockDiscussion = { id: '0', resolved: false, @@ -23,7 +27,6 @@ const defaultMockDiscussion = { const DEFAULT_TODO_COUNT = 2; describe('Design discussions component', () => { - const originalGon = window.gon; let wrapper; const findDesignNotes = () => wrapper.findAllComponents(DesignNote); @@ -34,18 +37,7 @@ describe('Design discussions component', () => { const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]'); const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]'); - const findApolloMutation = () => wrapper.findComponent(ApolloMutation); - const mutationVariables = { - mutation: createNoteMutation, - variables: { - input: { - noteableId: 'noteable-id', - body: 'test', - discussionId: '0', - }, - }, - }; const registerPath = '/users/sign_up?redirect_to_referer=yes'; const signInPath = '/users/sign_in?redirect_to_referer=yes'; const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } }); @@ -59,7 +51,7 @@ describe('Design discussions component', () => { provider: { clients: { defaultClient: { readQuery } } }, }; - function createComponent(props = {}, data = {}) { + function createComponent({ props = {}, data = {}, apolloConfig = {} } = {}) { wrapper = mount(DesignDiscussion, { propsData: { resolvedDiscussionsExpanded: true, @@ -82,7 +74,10 @@ describe('Design discussions component', () => { issueIid: '1', }, mocks: { - $apollo, + $apollo: { + ...$apollo, + ...apolloConfig, + }, $route: { hash: '#note_1', params: { @@ -101,16 +96,17 @@ describe('Design discussions component', () => { }); afterEach(() => { - wrapper.destroy(); - window.gon = originalGon; + confirmAction.mockReset(); }); describe('when discussion is not resolvable', () => { beforeEach(() => { createComponent({ - discussion: { - ...defaultMockDiscussion, - resolvable: false, + props: { + discussion: { + ...defaultMockDiscussion, + resolvable: false, + }, }, }); }); @@ -171,11 +167,13 @@ describe('Design discussions component', () => { innerText: DEFAULT_TODO_COUNT, }); createComponent({ - discussion: { - ...defaultMockDiscussion, - resolved: true, - resolvedBy: notes[0].author, - resolvedAt: '2020-05-08T07:10:45Z', + props: { + discussion: { + ...defaultMockDiscussion, + resolved: true, + resolvedBy: notes[0].author, + resolvedAt: '2020-05-08T07:10:45Z', + }, }, }); }); @@ -206,10 +204,10 @@ describe('Design discussions component', () => { }); it('emit todo:toggle when discussion is resolved', async () => { - createComponent( - { discussionWithOpenForm: defaultMockDiscussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); + createComponent({ + props: { discussionWithOpenForm: defaultMockDiscussion.id }, + data: { isFormRendered: true }, + }); findResolveButton().trigger('click'); findReplyForm().vm.$emit('submitForm'); @@ -261,32 +259,28 @@ describe('Design discussions component', () => { expect(findReplyForm().exists()).toBe(true); }); - it('calls mutation on submitting form and closes the form', async () => { - createComponent( - { discussionWithOpenForm: defaultMockDiscussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); + it('closes the form when note submit mutation is completed', async () => { + createComponent({ + props: { discussionWithOpenForm: defaultMockDiscussion.id }, + data: { isFormRendered: true }, + }); - findReplyForm().vm.$emit('submit-form'); - expect(mutate).toHaveBeenCalledWith(mutationVariables); + findReplyForm().vm.$emit('note-submit-complete', { data: { createNote: {} } }); - await mutate(); await nextTick(); expect(findReplyForm().exists()).toBe(false); }); it('clears the discussion comment on closing comment form', async () => { - createComponent( - { discussionWithOpenForm: defaultMockDiscussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); + createComponent({ + props: { discussionWithOpenForm: defaultMockDiscussion.id }, + data: { isFormRendered: true }, + }); await nextTick(); findReplyForm().vm.$emit('cancel-form'); - expect(wrapper.vm.discussionComment).toBe(''); - await nextTick(); expect(findReplyForm().exists()).toBe(false); }); @@ -295,15 +289,15 @@ describe('Design discussions component', () => { it.each([notes[0], notes[0].discussion.notes.nodes[1]])( 'applies correct class to all notes in the active discussion', (note) => { - createComponent( - { discussion: mockDiscussion }, - { + createComponent({ + props: { discussion: mockDiscussion }, + data: { activeDiscussion: { id: note.id, source: 'pin', }, }, - ); + }); expect( wrapper @@ -329,10 +323,10 @@ describe('Design discussions component', () => { }); it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => { - createComponent( - { discussionWithOpenForm: defaultMockDiscussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); + createComponent({ + props: { discussionWithOpenForm: defaultMockDiscussion.id }, + data: { isFormRendered: true }, + }); findResolveButton().trigger('click'); findReplyForm().vm.$emit('submitForm'); @@ -359,15 +353,15 @@ describe('Design discussions component', () => { beforeEach(() => { window.gon = { current_user_id: null }; - createComponent( - { + createComponent({ + props: { discussion: { ...defaultMockDiscussion, }, discussionWithOpenForm: defaultMockDiscussion.id, }, - { discussionComment: 'test', isFormRendered: true }, - ); + data: { isFormRendered: true }, + }); }); it('does not render resolve discussion button', () => { @@ -378,10 +372,6 @@ describe('Design discussions component', () => { expect(findReplyPlaceholder().exists()).toBe(false); }); - it('does not render apollo-mutation component', () => { - expect(findApolloMutation().exists()).toBe(false); - }); - it('renders design-note-signed-out component', () => { expect(findDesignNoteSignedOut().exists()).toBe(true); expect(findDesignNoteSignedOut().props()).toMatchObject({ @@ -390,4 +380,64 @@ describe('Design discussions component', () => { }); }); }); + + it('should open confirmation modal when the note emits `delete-note` event', async () => { + createComponent(); + + findDesignNotes().at(0).vm.$emit('delete-note', { id: '1' }); + expect(confirmAction).toHaveBeenCalled(); + }); + + describe('when confirmation modal is opened', () => { + const noteId = 'note-test-id'; + + it('sends the mutation with correct variables', async () => { + confirmAction.mockResolvedValueOnce(true); + const destroyNoteMutationSuccess = jest.fn().mockResolvedValue({ + data: { destroyNote: { note: null, __typename: 'DestroyNote', errors: [] } }, + }); + createComponent({ apolloConfig: { mutate: destroyNoteMutationSuccess } }); + + findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId }); + + expect(confirmAction).toHaveBeenCalled(); + + await waitForPromises(); + + expect(destroyNoteMutationSuccess).toHaveBeenCalledWith({ + update: expect.any(Function), + mutation: destroyNoteMutation, + variables: { + input: { + id: noteId, + }, + }, + optimisticResponse: { + destroyNote: { + note: null, + errors: [], + __typename: 'DestroyNotePayload', + }, + }, + }); + }); + + it('emits `delete-note-error` event if GraphQL mutation fails', async () => { + confirmAction.mockResolvedValueOnce(true); + const destroyNoteMutationError = jest.fn().mockRejectedValue(new Error('GraphQL error')); + createComponent({ apolloConfig: { mutate: destroyNoteMutationError } }); + + findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId }); + + await waitForPromises(); + + expect(destroyNoteMutationError).toHaveBeenCalled(); + + await waitForPromises(); + + expect(wrapper.emitted()).toEqual({ + 'delete-note-error': [[DELETE_NOTE_ERROR_MSG]], + }); + }); + }); }); diff --git a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js index e71bb5ab520..95b08b89809 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js @@ -18,10 +18,6 @@ function createComponent(isAddDiscussion = false) { describe('DesignNoteSignedOut', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - it('renders message containing register and sign-in links while user wants to reply to a discussion', () => { wrapper = createComponent(); 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 df511586c10..82848bd1a19 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 @@ -1,6 +1,6 @@ import { ApolloMutation } from 'vue-apollo'; import { nextTick } from 'vue'; -import { GlAvatar, GlAvatarLink } from '@gitlab/ui'; +import { GlAvatar, GlAvatarLink, GlDropdown } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import DesignNote from '~/design_management/components/design_notes/design_note.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; @@ -38,6 +38,8 @@ describe('Design note component', () => { const findReplyForm = () => wrapper.findComponent(DesignReplyForm); const findEditButton = () => wrapper.findByTestId('note-edit'); const findNoteContent = () => wrapper.findByTestId('note-text'); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-button"]'); function createComponent(props = {}, data = { isEditing: false }) { wrapper = shallowMountExtended(DesignNote, { @@ -63,10 +65,6 @@ describe('Design note component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('should match the snapshot', () => { createComponent({ note, @@ -112,6 +110,14 @@ describe('Design note component', () => { expect(findEditButton().exists()).toBe(false); }); + it('should not display a dropdown if user does not have a permission to delete note', () => { + createComponent({ + note, + }); + + expect(findDropdown().exists()).toBe(false); + }); + describe('when user has a permission to edit note', () => { it('should open an edit form on edit button click', async () => { createComponent({ @@ -158,15 +164,47 @@ describe('Design note component', () => { expect(findNoteContent().exists()).toBe(true); }); - it('calls a mutation on submit-form event and hides a form', async () => { - findReplyForm().vm.$emit('submit-form'); - expect(mutate).toHaveBeenCalled(); + it('hides a form after update mutation is completed', async () => { + findReplyForm().vm.$emit('note-submit-complete', { data: { updateNote: { errors: [] } } }); - await mutate(); await nextTick(); expect(findReplyForm().exists()).toBe(false); expect(findNoteContent().exists()).toBe(true); }); }); }); + + describe('when user has a permission to delete note', () => { + it('should display a dropdown', () => { + createComponent({ + note: { + ...note, + userPermissions: { + adminNote: true, + }, + }, + }); + + expect(findDropdown().exists()).toBe(true); + }); + }); + + it('should emit `delete-note` event with proper payload when delete note button is clicked', async () => { + const payload = { + ...note, + userPermissions: { + adminNote: true, + }, + }; + + createComponent({ + note: { + ...payload, + }, + }); + + findDeleteNoteButton().vm.$emit('click'); + + expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] }); + }); }); 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 f4d4f9cf896..db1cfb4f504 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 @@ -1,46 +1,96 @@ +import { GlAlert } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import Autosave from '~/autosave'; +import waitForPromises from 'helpers/wait_for_promises'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; +import { + ADD_DISCUSSION_COMMENT_ERROR, + ADD_IMAGE_DIFF_NOTE_ERROR, + UPDATE_IMAGE_DIFF_NOTE_ERROR, + UPDATE_NOTE_ERROR, +} from '~/design_management/utils/error_messages'; +import { + mockNoteSubmitSuccessMutationResponse, + mockNoteSubmitFailureMutationResponse, +} from '../../mock_data/apollo_mock'; jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); jest.mock('~/autosave'); describe('Design reply form component', () => { let wrapper; - let originalGon; const findTextarea = () => wrapper.find('textarea'); const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' }); const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' }); - - function createComponent(props = {}, mountOptions = {}) { + const findAlert = () => wrapper.findComponent(GlAlert); + + const mockNoteableId = 'gid://gitlab/DesignManagement::Design/6'; + const mockComment = 'New comment'; + const mockDiscussionId = 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8'; + const createNoteMutationData = { + mutation: createNoteMutation, + update: expect.anything(), + variables: { + input: { + noteableId: mockNoteableId, + discussionId: mockDiscussionId, + body: mockComment, + }, + }, + }; + + const ctrlKey = { + ctrlKey: true, + }; + const metaKey = { + metaKey: true, + }; + const mutationHandler = jest.fn().mockResolvedValue(); + + function createComponent({ + props = {}, + mountOptions = {}, + data = {}, + mutation = mutationHandler, + } = {}) { wrapper = mount(DesignReplyForm, { propsData: { + designNoteMutation: createNoteMutation, + noteableId: mockNoteableId, + markdownDocsPath: 'path/to/markdown/docs', + markdownPreviewPath: 'path/to/markdown/preview', value: '', - isSaving: false, - noteableId: 'gid://gitlab/DesignManagement::Design/6', ...props, }, ...mountOptions, + mocks: { + $apollo: { + mutate: mutation, + }, + }, + data() { + return { + ...data, + }; + }, }); } beforeEach(() => { - originalGon = window.gon; window.gon.current_user_id = 1; }); afterEach(() => { - wrapper.destroy(); - window.gon = originalGon; confirmAction.mockReset(); }); it('textarea has focus after component mount', () => { // We need to attach to document, so that `document.activeElement` is properly set in jsdom - createComponent({}, { attachTo: document.body }); + createComponent({ mountOptions: { attachTo: document.body } }); expect(findTextarea().element).toEqual(document.activeElement); }); @@ -64,7 +114,7 @@ describe('Design reply form component', () => { }); it('renders button text as "Save comment" when creating a comment', () => { - createComponent({ isNewComment: false }); + createComponent({ props: { isNewComment: false } }); expect(findSubmitButton().html()).toMatchSnapshot(); }); @@ -76,7 +126,7 @@ describe('Design reply form component', () => { `( 'initializes autosave support on discussion with proper key', async ({ discussionId, shortDiscussionId }) => { - createComponent({ discussionId }); + createComponent({ props: { discussionId } }); await nextTick(); expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [ @@ -88,32 +138,24 @@ describe('Design reply form component', () => { ); describe('when form has no text', () => { - beforeEach(() => { - createComponent({ - value: '', - }); + beforeEach(async () => { + createComponent(); + await nextTick(); }); it('submit button is disabled', () => { expect(findSubmitButton().attributes().disabled).toBe('disabled'); }); - it('does not emit submitForm event on textarea ctrl+enter keydown', async () => { - findTextarea().trigger('keydown.enter', { - ctrlKey: true, - }); - - await nextTick(); - expect(wrapper.emitted('submit-form')).toBeUndefined(); - }); - - it('does not emit submitForm event on textarea meta+enter keydown', async () => { - findTextarea().trigger('keydown.enter', { - metaKey: true, - }); + it.each` + key | keyData + ${'ctrl'} | ${ctrlKey} + ${'meta'} | ${metaKey} + `('does not perform mutation on textarea $key+enter keydown', async ({ keyData }) => { + findTextarea().trigger('keydown.enter', keyData); await nextTick(); - expect(wrapper.emitted('submit-form')).toBeUndefined(); + expect(mutationHandler).not.toHaveBeenCalled(); }); it('emits cancelForm event on pressing escape button on textarea', () => { @@ -129,118 +171,159 @@ describe('Design reply form component', () => { }); }); - describe('when form has text', () => { - beforeEach(() => { - createComponent({ - value: 'test', - }); - }); - + describe('when the form has text', () => { it('submit button is enabled', () => { + createComponent({ props: { value: mockComment } }); expect(findSubmitButton().attributes().disabled).toBeUndefined(); }); - it('emits submitForm event on Comment button click', async () => { - const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); + it('calls a mutation on submit button click event', async () => { + const mockMutationVariables = { + noteableId: mockNoteableId, + discussionId: mockDiscussionId, + }; + const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse); + createComponent({ + props: { + designNoteMutation: createNoteMutation, + mutationVariables: mockMutationVariables, + value: mockComment, + }, + mutation: successfulMutation, + }); findSubmitButton().vm.$emit('click'); await nextTick(); - expect(wrapper.emitted('submit-form')).toHaveLength(1); - expect(autosaveResetSpy).toHaveBeenCalled(); - }); + expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData); - it('emits submitForm event on textarea ctrl+enter keydown', async () => { - const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); + await waitForPromises(); + expect(wrapper.emitted('note-submit-complete')).toEqual([ + [mockNoteSubmitSuccessMutationResponse], + ]); + }); - findTextarea().trigger('keydown.enter', { - ctrlKey: true, + it.each` + key | keyData + ${'ctrl'} | ${ctrlKey} + ${'meta'} | ${metaKey} + `('does perform mutation on textarea $key+enter keydown', async ({ keyData }) => { + const mockMutationVariables = { + noteableId: mockNoteableId, + discussionId: mockDiscussionId, + }; + const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse); + createComponent({ + props: { + designNoteMutation: createNoteMutation, + mutationVariables: mockMutationVariables, + value: mockComment, + }, + mutation: successfulMutation, }); + findTextarea().trigger('keydown.enter', keyData); + await nextTick(); - expect(wrapper.emitted('submit-form')).toHaveLength(1); - expect(autosaveResetSpy).toHaveBeenCalled(); - }); + expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData); - it('emits submitForm event on textarea meta+enter keydown', async () => { - const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); + await waitForPromises(); + expect(wrapper.emitted('note-submit-complete')).toEqual([ + [mockNoteSubmitSuccessMutationResponse], + ]); + }); - findTextarea().trigger('keydown.enter', { - metaKey: true, + it('shows error message when mutation fails', async () => { + const failedMutation = jest.fn().mockRejectedValue(mockNoteSubmitFailureMutationResponse); + createComponent({ + props: { + designNoteMutation: createNoteMutation, + value: mockComment, + }, + mutation: failedMutation, + data: { + errorMessage: 'error', + }, }); - await nextTick(); - expect(wrapper.emitted('submit-form')).toHaveLength(1); - expect(autosaveResetSpy).toHaveBeenCalled(); - }); - - it('emits input event on changing textarea content', async () => { - findTextarea().setValue('test2'); + findSubmitButton().vm.$emit('click'); - await nextTick(); - expect(wrapper.emitted('input')).toEqual([['test2']]); + await waitForPromises(); + expect(findAlert().exists()).toBe(true); }); + it.each` + isDiscussion | isNewComment | errorMessage + ${true} | ${true} | ${ADD_IMAGE_DIFF_NOTE_ERROR} + ${true} | ${false} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} + ${false} | ${true} | ${ADD_DISCUSSION_COMMENT_ERROR} + ${false} | ${false} | ${UPDATE_NOTE_ERROR} + `( + 'return proper error message on error in case of isDiscussion is $isDiscussion and isNewComment is $isNewComment', + async ({ isDiscussion, isNewComment, errorMessage }) => { + createComponent({ props: { isDiscussion, isNewComment } }); + + expect(wrapper.vm.getErrorMessage()).toBe(errorMessage); + }, + ); + it('emits cancelForm event on Escape key if text was not changed', () => { + createComponent(); + findTextarea().trigger('keyup.esc'); expect(wrapper.emitted('cancel-form')).toHaveLength(1); }); it('opens confirmation modal on Escape key when text has changed', async () => { - wrapper.setProps({ value: 'test2' }); + createComponent(); + + findTextarea().setValue(mockComment); await nextTick(); findTextarea().trigger('keyup.esc'); - expect(confirmAction).toHaveBeenCalled(); - }); - - it('emits cancelForm event on Cancel button click if text was not changed', () => { - findCancelButton().trigger('click'); - expect(wrapper.emitted('cancel-form')).toHaveLength(1); - }); - - it('opens confirmation modal on Cancel button click when text has changed', async () => { - wrapper.setProps({ value: 'test2' }); - - await nextTick(); - findCancelButton().trigger('click'); expect(confirmAction).toHaveBeenCalled(); }); it('emits cancelForm event when confirmed', async () => { confirmAction.mockResolvedValueOnce(true); - const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); - wrapper.setProps({ value: 'test3' }); - await nextTick(); + createComponent({ props: { value: mockComment } }); + findTextarea().setValue('Comment changed'); - findTextarea().trigger('keyup.esc'); await nextTick(); + findTextarea().trigger('keyup.esc'); expect(confirmAction).toHaveBeenCalled(); - await nextTick(); + await waitForPromises(); expect(wrapper.emitted('cancel-form')).toHaveLength(1); - expect(autosaveResetSpy).toHaveBeenCalled(); }); - it("doesn't emit cancelForm event when not confirmed", async () => { + it('does not emit cancelForm event when not confirmed', async () => { confirmAction.mockResolvedValueOnce(false); - const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); - wrapper.setProps({ value: 'test3' }); + createComponent({ props: { value: mockComment } }); + findTextarea().setValue('Comment changed'); await nextTick(); findTextarea().trigger('keyup.esc'); await nextTick(); expect(confirmAction).toHaveBeenCalled(); - await nextTick(); + await waitForPromises(); expect(wrapper.emitted('cancel-form')).toBeUndefined(); - expect(autosaveResetSpy).not.toHaveBeenCalled(); + }); + }); + + describe('when component is destroyed', () => { + it('calls autosave.reset', async () => { + const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); + createComponent(); + await wrapper.destroy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); }); }); 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 41129e2b58d..eaa5a620fa6 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 @@ -23,10 +23,6 @@ describe('Toggle replies widget component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('when replies are collapsed', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js index 4a339899473..fdcea6d88c0 100644 --- a/spec/frontend/design_management/components/design_presentation_spec.js +++ b/spec/frontend/design_management/components/design_presentation_spec.js @@ -15,7 +15,6 @@ const mockOverlayData = { }; describe('Design management design presentation component', () => { - const originalGon = window.gon; let wrapper; function createComponent( @@ -114,11 +113,6 @@ describe('Design management design presentation component', () => { window.gon = { current_user_id: 1 }; }); - afterEach(() => { - wrapper.destroy(); - window.gon = originalGon; - }); - it('renders image and overlay when image provided', async () => { createComponent( { diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js index e1a66cea329..62a26a8f5dd 100644 --- a/spec/frontend/design_management/components/design_scaler_spec.js +++ b/spec/frontend/design_management/components/design_scaler_spec.js @@ -25,11 +25,6 @@ describe('Design management design scaler component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when `scale` value is greater than 1', () => { beforeEach(async () => { setScale(1.6); diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js index af995f75ddc..90424175417 100644 --- a/spec/frontend/design_management/components/design_sidebar_spec.js +++ b/spec/frontend/design_management/components/design_sidebar_spec.js @@ -29,7 +29,6 @@ const $route = { const mutate = jest.fn().mockResolvedValue(); describe('Design management design sidebar component', () => { - const originalGon = window.gon; let wrapper; const findDiscussions = () => wrapper.findAllComponents(DesignDiscussion); @@ -67,11 +66,6 @@ describe('Design management design sidebar component', () => { window.gon = { current_user_id: 1 }; }); - afterEach(() => { - wrapper.destroy(); - window.gon = originalGon; - }); - it('renders participants', () => { createComponent(); @@ -143,8 +137,8 @@ describe('Design management design sidebar component', () => { expect(findResolvedCommentsToggle().props('visible')).toBe(true); }); - it('sends a mutation to set an active discussion when clicking on a discussion', () => { - findFirstDiscussion().trigger('click'); + it('emits correct event to send a mutation to set an active discussion when clicking on a discussion', () => { + findFirstDiscussion().vm.$emit('update-active-discussion'); expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables); }); 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 ac26873b692..f713203c0ee 100644 --- a/spec/frontend/design_management/components/design_todo_button_spec.js +++ b/spec/frontend/design_management/components/design_todo_button_spec.js @@ -51,8 +51,6 @@ describe('Design management design todo button', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; jest.clearAllMocks(); }); diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js index 95d2ad504de..53abcc559d8 100644 --- a/spec/frontend/design_management/components/image_spec.js +++ b/spec/frontend/design_management/components/image_spec.js @@ -20,10 +20,6 @@ describe('Design management large image component', () => { stubPerformanceWebAPI(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders loading state', () => { createComponent({ isLoading: true, diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js index e907e2e4ac5..4a0ad5a045b 100644 --- a/spec/frontend/design_management/components/list/item_spec.js +++ b/spec/frontend/design_management/components/list/item_spec.js @@ -54,10 +54,6 @@ describe('Design management list item component', () => { ); } - afterEach(() => { - wrapper.destroy(); - }); - describe('when item is not in view', () => { it('image is not rendered', () => { createComponent(); diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js index 38a7fadee79..8427d83ceee 100644 --- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js +++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js @@ -34,10 +34,6 @@ describe('Design management pagination component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('hides components when designs are empty', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js index 1776405ece9..764ad73805f 100644 --- a/spec/frontend/design_management/components/toolbar/index_spec.js +++ b/spec/frontend/design_management/components/toolbar/index_spec.js @@ -1,12 +1,18 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql'; import DeleteButton from '~/design_management/components/delete_button.vue'; import Toolbar from '~/design_management/components/toolbar/index.vue'; import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants'; +import { getPermissionsQueryResponse } from '../../mock_data/apollo_mock'; Vue.use(VueRouter); +Vue.use(VueApollo); const router = new VueRouter(); const RouterLinkStub = { @@ -27,7 +33,12 @@ describe('Design management toolbar component', () => { const updatedAt = new Date(); updatedAt.setHours(updatedAt.getHours() - 1); + const mockApollo = createMockApollo([ + [permissionsQuery, jest.fn().mockResolvedValue(getPermissionsQueryResponse(createDesign))], + ]); + wrapper = shallowMount(Toolbar, { + apolloProvider: mockApollo, router, propsData: { id: '1', @@ -46,31 +57,20 @@ describe('Design management toolbar component', () => { 'router-link': RouterLinkStub, }, }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - permissions: { - createDesign, - }, - }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders design and updated data', async () => { createComponent(); - await nextTick(); + await waitForPromises(); + expect(wrapper.element).toMatchSnapshot(); }); it('links back to designs list', async () => { createComponent(); - await nextTick(); + await waitForPromises(); const link = wrapper.find('a'); expect(link.props('to')).toEqual({ @@ -84,35 +84,41 @@ describe('Design management toolbar component', () => { it('renders delete button on latest designs version with logged in user', async () => { createComponent(); - await nextTick(); + await waitForPromises(); + 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(); + await waitForPromises(); + 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(); + await waitForPromises(); + expect(wrapper.findComponent(DeleteButton).exists()).toBe(false); }); it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => { createComponent(); - await nextTick(); + await waitForPromises(); + wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs'); expect(wrapper.emitted().delete).toHaveLength(1); }); - it('renders download button with correct link', () => { + it('renders download button with correct link', async () => { createComponent(); + await waitForPromises(); + 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 59821218ab8..ceae7920e0d 100644 --- a/spec/frontend/design_management/components/upload/button_spec.js +++ b/spec/frontend/design_management/components/upload/button_spec.js @@ -14,10 +14,6 @@ describe('Design management upload button component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders upload design button', () => { createComponent(); 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 6ad10e707ab..cdfff61ba4f 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 @@ -42,10 +42,6 @@ describe('Design management design version dropdown component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem); const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index); diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js index 2a43b5debee..2b99dcf14da 100644 --- a/spec/frontend/design_management/mock_data/apollo_mock.js +++ b/spec/frontend/design_management/mock_data/apollo_mock.js @@ -91,7 +91,7 @@ export const designUploadMutationUpdatedResponse = { }, }; -export const permissionsQueryResponse = { +export const getPermissionsQueryResponse = (createDesign = true) => ({ data: { project: { __typename: 'Project', @@ -99,11 +99,11 @@ export const permissionsQueryResponse = { issue: { __typename: 'Issue', id: 'issue-1', - userPermissions: { __typename: 'UserPermissions', createDesign: true }, + userPermissions: { __typename: 'UserPermissions', createDesign }, }, }, }, -}; +}); export const reorderedDesigns = [ { @@ -211,3 +211,109 @@ export const getDesignQueryResponse = { }, }, }; + +export const mockNoteSubmitSuccessMutationResponse = [ + { + data: { + createNote: { + note: { + id: 'gid://gitlab/DiffNote/468', + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + body: 'New comment', + bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>", + createdAt: '2023-02-24T06:49:20Z', + resolved: false, + position: { + diffRefs: { + baseSha: 'f63ae53ed82d8765477c191383e1e6a000c10375', + startSha: 'f63ae53ed82d8765477c191383e1e6a000c10375', + headSha: 'f348c652f1a737151fc79047895e695fbe81464c', + __typename: 'DiffRefs', + }, + x: 441, + y: 128, + height: 152, + width: 695, + __typename: 'DiffPosition', + }, + userPermissions: { + adminNote: true, + repositionNote: true, + __typename: 'NotePermissions', + }, + discussion: { + id: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8', + notes: { + nodes: [ + { + id: 'gid://gitlab/DiffNote/459', + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + __typename: 'Note', + }, + errors: [], + __typename: 'CreateNotePayload', + }, + }, + }, +]; + +export const mockNoteSubmitFailureMutationResponse = [ + { + errors: [ + { + message: + 'Variable $input of type CreateNoteInput! was provided invalid value for bodyaa (Field is not defined on CreateNoteInput), body (Expected value to not be null)', + locations: [ + { + line: 1, + column: 21, + }, + ], + extensions: { + value: { + noteableId: 'gid://gitlab/DesignManagement::Design/10', + discussionId: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8', + bodyaa: 'df', + }, + problems: [ + { + path: ['bodyaa'], + explanation: 'Field is not defined on CreateNoteInput', + }, + { + path: ['body'], + explanation: 'Expected value to not be null', + }, + ], + }, + }, + ], + }, +]; + +export const mockCreateImageNoteDiffResponse = { + data: { + createImageDiffNote: { + note: { + author: { + username: '', + }, + discussion: {}, + }, + }, + }, +}; diff --git a/spec/frontend/design_management/mock_data/project.js b/spec/frontend/design_management/mock_data/project.js new file mode 100644 index 00000000000..e1c2057d8d1 --- /dev/null +++ b/spec/frontend/design_management/mock_data/project.js @@ -0,0 +1,17 @@ +import design from './design'; + +export default { + project: { + issue: { + designCollection: { + designs: { + nodes: [ + { + ...design, + }, + ], + }, + }, + }, + }, +}; diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js index a11463ab663..6cec4036d40 100644 --- a/spec/frontend/design_management/pages/design/index_spec.js +++ b/spec/frontend/design_management/pages/design/index_spec.js @@ -1,15 +1,14 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; -import { ApolloMutation } from 'vue-apollo'; import VueRouter from 'vue-router'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import Api from '~/api'; import DesignPresentation from '~/design_management/components/design_presentation.vue'; import DesignSidebar from '~/design_management/components/design_sidebar.vue'; import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants'; -import createImageDiffNoteMutation from '~/design_management/graphql/mutations/create_image_diff_note.mutation.graphql'; import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql'; +import getDesignQuery from '~/design_management/graphql/queries/get_design.query.graphql'; import DesignIndex from '~/design_management/pages/design/index.vue'; import createRouter from '~/design_management/router'; import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants'; @@ -23,16 +22,23 @@ import { DESIGN_SNOWPLOW_EVENT_TYPES, DESIGN_SERVICE_PING_EVENT_TYPES, } from '~/design_management/utils/tracking'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; +import * as cacheUpdate from '~/design_management/utils/cache_update'; import mockAllVersions from '../../mock_data/all_versions'; import design from '../../mock_data/design'; +import mockProject from '../../mock_data/project'; import mockResponseWithDesigns from '../../mock_data/designs'; import mockResponseNoDesigns from '../../mock_data/no_designs'; +import { mockCreateImageNoteDiffResponse } from '../../mock_data/apollo_mock'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/api.js'); const focusInput = jest.fn(); +const mockCacheObject = { + readQuery: jest.fn().mockReturnValue(mockProject), + writeQuery: jest.fn(), +}; const mutate = jest.fn().mockResolvedValue(); const mockPageLayoutElement = { classList: { @@ -52,32 +58,13 @@ const mockDesignNoDiscussions = { nodes: [], }, }; -const newComment = 'new comment'; + const annotationCoordinates = { x: 10, y: 10, width: 100, height: 100, }; -const createDiscussionMutationVariables = { - mutation: createImageDiffNoteMutation, - update: expect.anything(), - variables: { - input: { - body: newComment, - noteableId: design.id, - position: { - headSha: 'headSha', - baseSha: 'baseSha', - startSha: 'startSha', - paths: { - newPath: 'full-design-path', - }, - ...annotationCoordinates, - }, - }, - }, -}; Vue.use(VueRouter); @@ -85,7 +72,7 @@ describe('Design management design index page', () => { let wrapper; let router; - const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm); + const findDesignReplyForm = () => wrapper.findComponent(DesignReplyForm); const findSidebar = () => wrapper.findComponent(DesignSidebar); const findDesignPresentation = () => wrapper.findComponent(DesignPresentation); @@ -95,7 +82,7 @@ describe('Design management design index page', () => { data = {}, intialRouteOptions = {}, provide = {}, - stubs = { ApolloMutation, DesignSidebar, DesignReplyForm }, + stubs = { DesignSidebar, DesignReplyForm }, } = {}, ) { const $apollo = { @@ -105,6 +92,11 @@ describe('Design management design index page', () => { }, }, mutate, + getClient() { + return { + cache: mockCacheObject, + }; + }, }; router = createRouter(); @@ -133,10 +125,6 @@ describe('Design management design index page', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('when navigating to component', () => { it('applies fullscreen layout class', () => { jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement); @@ -216,7 +204,7 @@ describe('Design management design index page', () => { findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 }); await nextTick(); - expect(findDiscussionForm().exists()).toBe(true); + expect(findDesignReplyForm().exists()).toBe(true); }); it('keeps new discussion form focused', () => { @@ -235,24 +223,36 @@ describe('Design management design index page', () => { expect(focusInput).toHaveBeenCalled(); }); - it('sends a mutation on submitting form and closes form', async () => { + it('sends a update and closes the form when mutation is completed', async () => { createComponent( { loading: false }, { data: { design, annotationCoordinates, - comment: newComment, }, }, ); - findDiscussionForm().vm.$emit('submit-form'); - expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables); + const addImageDiffNoteToStore = jest.spyOn(cacheUpdate, 'updateStoreAfterAddImageDiffNote'); + + const mockDesignVariables = { + fullPath: 'project-path', + iid: '1', + filenames: ['gid::/gitlab/Design/1'], + atVersion: null, + }; + + findDesignReplyForm().vm.$emit('note-submit-complete', mockCreateImageNoteDiffResponse); await nextTick(); - await mutate({ variables: createDiscussionMutationVariables }); - expect(findDiscussionForm().exists()).toBe(false); + expect(addImageDiffNoteToStore).toHaveBeenCalledWith( + mockCacheObject, + mockCreateImageNoteDiffResponse.data.createImageDiffNote, + getDesignQuery, + mockDesignVariables, + ); + expect(findDesignReplyForm().exists()).toBe(false); }); it('closes the form and clears the comment on canceling form', async () => { @@ -262,17 +262,14 @@ describe('Design management design index page', () => { data: { design, annotationCoordinates, - comment: newComment, }, }, ); - findDiscussionForm().vm.$emit('cancel-form'); - - expect(wrapper.vm.comment).toBe(''); + findDesignReplyForm().vm.$emit('cancel-form'); await nextTick(); - expect(findDiscussionForm().exists()).toBe(false); + expect(findDesignReplyForm().exists()).toBe(false); }); describe('with error', () => { diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index 76ece922ded..1ddf757eb19 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -29,19 +29,19 @@ import { DESIGN_TRACKING_PAGE_NAME, DESIGN_SNOWPLOW_EVENT_TYPES, } from '~/design_management/utils/tracking'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; import { designListQueryResponse, designUploadMutationCreatedResponse, designUploadMutationUpdatedResponse, - permissionsQueryResponse, + getPermissionsQueryResponse, moveDesignMutationResponse, reorderedDesigns, moveDesignMutationResponseWithErrors, } from '../mock_data/apollo_mock'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); const mockPageEl = { classList: { remove: jest.fn(), @@ -181,7 +181,7 @@ describe('Design management index page', () => { const requestHandlers = [ [getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)], - [permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)], + [permissionsQuery, jest.fn().mockResolvedValue(getPermissionsQueryResponse())], [moveDesignMutation, moveDesignHandler], ]; @@ -197,11 +197,6 @@ describe('Design management index page', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('designs', () => { it('renders loading icon', () => { createComponent({ loading: true }); @@ -800,7 +795,7 @@ describe('Design management index page', () => { expect(draggableAttributes().disabled).toBe(false); }); - it('displays flash if mutation had a recoverable error', async () => { + it('displays alert if mutation had a recoverable error', async () => { createComponentWithApollo({ moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors), }); diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js index b9edde559c8..3503725f741 100644 --- a/spec/frontend/design_management/router_spec.js +++ b/spec/frontend/design_management/router_spec.js @@ -11,8 +11,6 @@ import '~/commons/bootstrap'; function factory(routeArg) { Vue.use(VueRouter); - window.gon = { sprite_icons: '' }; - const router = createRouter('/'); if (routeArg !== undefined) { router.push(routeArg); @@ -36,10 +34,6 @@ function factory(routeArg) { } describe('Design management router', () => { - afterEach(() => { - window.location.hash = ''; - }); - describe.each([['/'], [{ name: DESIGNS_ROUTE_NAME }]])('root route', (routeArg) => { it('pushes home component', () => { const wrapper = factory(routeArg); diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js index 42777adfd58..e89dfe9f860 100644 --- a/spec/frontend/design_management/utils/cache_update_spec.js +++ b/spec/frontend/design_management/utils/cache_update_spec.js @@ -10,10 +10,10 @@ import { ADD_IMAGE_DIFF_NOTE_ERROR, UPDATE_IMAGE_DIFF_NOTE_ERROR, } from '~/design_management/utils/error_messages'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import design from '../mock_data/design'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('Design Management cache update', () => { const mockErrors = ['code red!']; diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 513e67ea247..06995706a2b 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -59,6 +59,7 @@ describe('diffs/components/app', () => { endpoint: TEST_ENDPOINT, endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, endpointBatch: `${TEST_HOST}/diff/endpointBatch`, + endpointDiffForPath: TEST_ENDPOINT, endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`, endpointCodequality: '', projectPath: 'namespace/project', @@ -71,12 +72,6 @@ describe('diffs/components/app', () => { }, provide, store, - stubs: { - DynamicScroller: { - template: `<div><slot :item="$store.state.diffs.diffFiles[0]"></slot></div>`, - }, - DynamicScrollerItem: true, - }, }); } @@ -265,7 +260,7 @@ describe('diffs/components/app', () => { it('sets width of tree list', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; + state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } }; }); expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px'); @@ -294,13 +289,14 @@ describe('diffs/components/app', () => { it('does not render empty state when diff files exist', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ - id: 1, - }); + state.diffs.diffFiles = ['anything']; + state.diffs.treeEntries['1'] = { type: 'blob', id: 1 }; }); expect(wrapper.findComponent(NoChanges).exists()).toBe(false); - expect(wrapper.findAllComponents(DiffFile).length).toBe(1); + expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')).toBe( + store.state.diffs.diffFiles, + ); }); }); @@ -388,19 +384,15 @@ describe('diffs/components/app', () => { beforeEach(() => { createComponent({}, () => { - store.state.diffs.diffFiles = [ - { file_hash: '111', file_path: '111.js' }, - { file_hash: '222', file_path: '222.js' }, - { file_hash: '333', file_path: '333.js' }, + store.state.diffs.treeEntries = [ + { type: 'blob', fileHash: '111', path: '111.js' }, + { type: 'blob', fileHash: '222', path: '222.js' }, + { type: 'blob', fileHash: '333', path: '333.js' }, ]; }); spy = jest.spyOn(store, 'dispatch'); }); - afterEach(() => { - wrapper.destroy(); - }); - it('jumps to next and previous files in the list', async () => { await nextTick(); @@ -507,7 +499,6 @@ describe('diffs/components/app', () => { describe('diffs', () => { it('should render compare versions component', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; state.diffs.mergeRequestDiffs = diffsMockData; state.diffs.targetBranchName = 'target-branch'; state.diffs.mergeRequestDiff = mergeRequestDiff; @@ -578,10 +569,18 @@ describe('diffs/components/app', () => { it('should display diff file if there are diff files', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; + state.diffs.treeEntries = { + 111: { type: 'blob', fileHash: '111', path: '111.js' }, + 123: { type: 'blob', fileHash: '123', path: '123.js' }, + 312: { type: 'blob', fileHash: '312', path: '312.js' }, + }; }); - expect(wrapper.findComponent(DiffFile).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'DynamicScroller' }).exists()).toBe(true); + expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')).toBe( + store.state.diffs.diffFiles, + ); }); it("doesn't render tree list when no changes exist", () => { @@ -592,7 +591,7 @@ describe('diffs/components/app', () => { it('should render tree list', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; + state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } }; }); expect(wrapper.findComponent(TreeList).exists()).toBe(true); @@ -606,7 +605,7 @@ describe('diffs/components/app', () => { it('calls setShowTreeList when only 1 file', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.treeEntries = { 123: { type: 'blob', fileHash: '123' } }; }); jest.spyOn(store, 'dispatch'); wrapper.vm.setTreeDisplay(); @@ -617,10 +616,12 @@ describe('diffs/components/app', () => { }); }); - it('calls setShowTreeList with true when more than 1 file is in diffs array', () => { + it('calls setShowTreeList with true when more than 1 file is in tree entries map', () => { createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); - state.diffs.diffFiles.push({ sha: '124' }); + state.diffs.treeEntries = { + 111: { type: 'blob', fileHash: '111', path: '111.js' }, + 123: { type: 'blob', fileHash: '123', path: '123.js' }, + }; }); jest.spyOn(store, 'dispatch'); @@ -640,7 +641,7 @@ describe('diffs/components/app', () => { localStorage.setItem('mr_tree_show', showTreeList); createComponent({}, ({ state }) => { - state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.treeEntries['123'] = { sha: '123' }; }); jest.spyOn(store, 'dispatch'); @@ -656,7 +657,10 @@ describe('diffs/components/app', () => { describe('file-by-file', () => { it('renders a single diff', async () => { createComponent({ fileByFileUserPreference: true }, ({ state }) => { - state.diffs.diffFiles.push({ file_hash: '123' }); + state.diffs.treeEntries = { + 123: { type: 'blob', fileHash: '123' }, + 312: { type: 'blob', fileHash: '312' }, + }; state.diffs.diffFiles.push({ file_hash: '312' }); }); @@ -671,7 +675,10 @@ describe('diffs/components/app', () => { it('sets previous button as disabled', async () => { createComponent({ fileByFileUserPreference: true }, ({ state }) => { - state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' }); + state.diffs.treeEntries = { + 123: { type: 'blob', fileHash: '123' }, + 312: { type: 'blob', fileHash: '312' }, + }; }); await nextTick(); @@ -682,7 +689,10 @@ describe('diffs/components/app', () => { it('sets next button as disabled', async () => { createComponent({ fileByFileUserPreference: true }, ({ state }) => { - state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' }); + state.diffs.treeEntries = { + 123: { type: 'blob', fileHash: '123' }, + 312: { type: 'blob', fileHash: '312' }, + }; state.diffs.currentDiffFileId = '312'; }); @@ -694,7 +704,7 @@ describe('diffs/components/app', () => { it("doesn't display when there's fewer than 2 files", async () => { createComponent({ fileByFileUserPreference: true }, ({ state }) => { - state.diffs.diffFiles.push({ file_hash: '123' }); + state.diffs.treeEntries = { 123: { type: 'blob', fileHash: '123' } }; state.diffs.currentDiffFileId = '123'; }); @@ -711,7 +721,10 @@ describe('diffs/components/app', () => { 'calls navigateToDiffFileIndex with $index when $link is clicked', async ({ currentDiffFileId, targetFile }) => { createComponent({ fileByFileUserPreference: true }, ({ state }) => { - state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' }); + state.diffs.treeEntries = { + 123: { type: 'blob', fileHash: '123' }, + 312: { type: 'blob', fileHash: '312' }, + }; state.diffs.currentDiffFileId = currentDiffFileId; }); diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js index eca5b536a35..ae40f6c898d 100644 --- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js +++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js @@ -45,10 +45,6 @@ describe('CollapsedFilesWarning', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when there is more than one file', () => { it.each` present | dismissed diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js index 08be3fa2745..4b4b6351d3f 100644 --- a/spec/frontend/diffs/components/commit_item_spec.js +++ b/spec/frontend/diffs/components/commit_item_spec.js @@ -41,11 +41,6 @@ describe('diffs/components/commit_item', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('default state', () => { beforeEach(() => { mountComponent(); diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js index 09128b04caa..785ff537777 100644 --- a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js +++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js @@ -38,11 +38,6 @@ describe('CompareDropdownLayout', () => { isActive: listItem.classes().includes('is-active'), })); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with versions', () => { beforeEach(() => { const versions = [ diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js index 21f3ee26bf8..23da1a3601b 100644 --- a/spec/frontend/diffs/components/compare_versions_spec.js +++ b/spec/frontend/diffs/components/compare_versions_spec.js @@ -58,11 +58,6 @@ describe('CompareVersions', () => { store.state.diffs.mergeRequestDiffs = diffsMockData; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('template', () => { beforeEach(() => { createWrapper({}, {}, false); diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js index 7bd9afab648..e5ca90eb7c8 100644 --- a/spec/frontend/diffs/components/diff_code_quality_spec.js +++ b/spec/frontend/diffs/components/diff_code_quality_spec.js @@ -11,10 +11,6 @@ const findIcon = () => wrapper.findComponent(GlIcon); const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`); describe('DiffCodeQuality', () => { - afterEach(() => { - wrapper.destroy(); - }); - const createWrapper = (codeQuality, mountFunction = mountExtended) => { return mountFunction(DiffCodeQuality, { propsData: { diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js index 0bce6451ce4..3524973278c 100644 --- a/spec/frontend/diffs/components/diff_content_spec.js +++ b/spec/frontend/diffs/components/diff_content_spec.js @@ -93,11 +93,6 @@ describe('DiffContent', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with text based files', () => { afterEach(() => { [isParallelViewGetterMock, isInlineViewGetterMock].forEach((m) => m.mockRestore()); diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js index bf4a1a1c1f7..348439d6006 100644 --- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js +++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js @@ -26,10 +26,6 @@ describe('DiffDiscussionReply', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('if user can reply', () => { beforeEach(() => { getters = { diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js index 5092ae6ab6e..73d9f2d6d45 100644 --- a/spec/frontend/diffs/components/diff_discussions_spec.js +++ b/spec/frontend/diffs/components/diff_discussions_spec.js @@ -25,10 +25,6 @@ describe('DiffDiscussions', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('should have notes list', () => { createComponent(); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index c23eb2f3d24..4515a8e8926 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -72,8 +72,6 @@ describe('DiffFileHeader component', () => { diffHasExpandedDiscussionsResultMock, ...Object.values(mockStoreConfig.modules.diffs.actions), ].forEach((mock) => mock.mockReset()); - - wrapper.destroy(); }); const findHeader = () => wrapper.findComponent({ ref: 'header' }); diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js index c5b76551fcc..66ee4e955b8 100644 --- a/spec/frontend/diffs/components/diff_file_row_spec.js +++ b/spec/frontend/diffs/components/diff_file_row_spec.js @@ -13,10 +13,6 @@ describe('Diff File Row component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders file row component', () => { const sharedProps = { level: 4, diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index ccfc36f8f16..93698396450 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -129,8 +129,6 @@ describe('DiffFile', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; axiosMock.restore(); }); @@ -222,21 +220,10 @@ describe('DiffFile', () => { describe('computed', () => { describe('showLocalFileReviews', () => { - let gon; - function setLoggedIn(bool) { window.gon.current_user_id = bool; } - beforeAll(() => { - gon = window.gon; - window.gon = {}; - }); - - afterEach(() => { - window.gon = gon; - }); - it.each` loggedIn | bool ${true} | ${true} diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js index f13988fc11f..5f2b1a81b91 100644 --- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js +++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js @@ -21,10 +21,6 @@ describe('DiffGutterAvatars', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when expanded', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js index a7a95ed2f35..356c7ef925a 100644 --- a/spec/frontend/diffs/components/diff_row_spec.js +++ b/spec/frontend/diffs/components/diff_row_spec.js @@ -89,10 +89,6 @@ describe('DiffRow', () => { }; afterEach(() => { - wrapper.destroy(); - wrapper = null; - - window.gon = {}; showCommentForm.mockReset(); enterdragging.mockReset(); stopdragging.mockReset(); diff --git a/spec/frontend/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js index bbd4f5faeec..d9359fb3c7b 100644 --- a/spec/frontend/diffs/components/hidden_files_warning_spec.js +++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js @@ -23,10 +23,6 @@ describe('HiddenFilesWarning', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('has a correct plain diff URL', () => { const plainDiffLink = wrapper.findAllComponents(GlButton).at(0); diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js index ccf942bdcef..18901781587 100644 --- a/spec/frontend/diffs/components/image_diff_overlay_spec.js +++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js @@ -36,10 +36,6 @@ describe('Diffs image diff overlay component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders comment badges', () => { createComponent(); diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js index 4e47249f5b4..715912b361f 100644 --- a/spec/frontend/diffs/components/merge_conflict_warning_spec.js +++ b/spec/frontend/diffs/components/merge_conflict_warning_spec.js @@ -25,10 +25,6 @@ describe('MergeConflictWarning', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` present | resolutionPath ${false} | ${''} diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js index dbfe9770e07..e637b1dd43d 100644 --- a/spec/frontend/diffs/components/no_changes_spec.js +++ b/spec/frontend/diffs/components/no_changes_spec.js @@ -34,11 +34,6 @@ describe('Diff no changes empty state', () => { store.state.diffs.mergeRequestDiffs = diffsMockData; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findMessage = () => wrapper.find('[data-testid="no-changes-message"]'); it('prevents XSS', () => { diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js index 2ec11ba86fd..3d2bbe43746 100644 --- a/spec/frontend/diffs/components/settings_dropdown_spec.js +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -39,7 +39,6 @@ describe('Diff settings dropdown component', () => { afterEach(() => { store.dispatch.mockRestore(); - wrapper.destroy(); }); describe('tree view buttons', () => { diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js index 1656eaf8ba0..87c638d065a 100644 --- a/spec/frontend/diffs/components/tree_list_spec.js +++ b/spec/frontend/diffs/components/tree_list_spec.js @@ -1,20 +1,36 @@ -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import TreeList from '~/diffs/components/tree_list.vue'; import createStore from '~/diffs/store/modules'; -import FileTree from '~/vue_shared/components/file_tree.vue'; +import DiffFileRow from '~/diffs/components//diff_file_row.vue'; +import { stubComponent } from 'helpers/stub_component'; describe('Diffs tree list component', () => { let wrapper; let store; - const getFileRows = () => wrapper.findAll('.file-row'); + const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' }); + const getFileRow = () => wrapper.findComponent(DiffFileRow); Vue.use(Vuex); - const createComponent = (mountFn = mount) => { - wrapper = mountFn(TreeList, { + const createComponent = () => { + wrapper = shallowMount(TreeList, { store, propsData: { hideFileStats: false }, + stubs: { + // eslint will fail if we import the real component + RecycleScroller: stubComponent( + { + name: 'RecycleScroller', + props: { + items: null, + }, + }, + { + template: '<div><slot :item="{ tree: [] }"></slot></div>', + }, + ), + }, }); }; @@ -80,10 +96,6 @@ describe('Diffs tree list component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); @@ -101,26 +113,32 @@ describe('Diffs tree list component', () => { }); describe('search by file extension', () => { + it('hides scroller for no matches', async () => { + wrapper.find('[data-testid="diff-tree-search"]').setValue('*.md'); + + await nextTick(); + + expect(getScroller().exists()).toBe(false); + expect(wrapper.text()).toContain('No files found'); + }); + it.each` extension | itemSize - ${'*.md'} | ${0} - ${'*.js'} | ${1} - ${'index.js'} | ${1} - ${'app/*.js'} | ${1} - ${'*.js, *.rb'} | ${2} + ${'*.js'} | ${2} + ${'index.js'} | ${2} + ${'app/*.js'} | ${2} + ${'*.js, *.rb'} | ${3} `('returns $itemSize item for $extension', async ({ extension, itemSize }) => { wrapper.find('[data-testid="diff-tree-search"]').setValue(extension); await nextTick(); - expect(getFileRows()).toHaveLength(itemSize); + expect(getScroller().props('items')).toHaveLength(itemSize); }); }); it('renders tree', () => { - expect(getFileRows()).toHaveLength(2); - expect(getFileRows().at(0).html()).toContain('index.js'); - expect(getFileRows().at(1).html()).toContain('app'); + expect(getScroller().props('items')).toHaveLength(2); }); it('hides file stats', async () => { @@ -133,33 +151,16 @@ describe('Diffs tree list component', () => { it('calls toggleTreeOpen when clicking folder', () => { jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined); - getFileRows().at(1).trigger('click'); + getFileRow().vm.$emit('toggleTreeOpen', 'app'); expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app'); }); - it('calls scrollToFile when clicking blob', () => { - jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined); - - wrapper.find('.file-row').trigger('click'); - - expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', { - path: 'app/index.js', - }); - }); - - it('renders as file list when renderTreeList is false', async () => { - wrapper.vm.$store.state.diffs.renderTreeList = false; - - await nextTick(); - expect(getFileRows()).toHaveLength(2); - }); - - it('renders file paths when renderTreeList is false', async () => { + it('renders when renderTreeList is false', async () => { wrapper.vm.$store.state.diffs.renderTreeList = false; await nextTick(); - expect(wrapper.find('.file-row').html()).toContain('index.js'); + expect(getScroller().props('items')).toHaveLength(3); }); }); @@ -172,12 +173,10 @@ describe('Diffs tree list component', () => { }); it('passes the viewedDiffFileIds to the FileTree', async () => { - createComponent(shallowMount); + createComponent(); await nextTick(); - // Have to use $attrs['viewed-files'] because we are passing down an object - // and attributes('') stringifies values (e.g. [object])... - expect(wrapper.findComponent(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds); + expect(wrapper.findComponent(DiffFileRow).props('viewedFiles')).toBe(viewedDiffFileIds); }); }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 78765204322..b00076504e3 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -13,7 +13,7 @@ import * as diffActions from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; import * as utils from '~/diffs/store/utils'; import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import { @@ -26,7 +26,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '~/notes/event_hub'; import { diffMetadata } from '../mock_data/diff_metadata'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('DiffsStoreActions', () => { let mock; @@ -69,6 +69,7 @@ describe('DiffsStoreActions', () => { const endpoint = '/diffs/set/endpoint'; const endpointMetadata = '/diffs/set/endpoint/metadata'; const endpointBatch = '/diffs/set/endpoint/batch'; + const endpointDiffForPath = '/diffs/set/endpoint/path'; const endpointCoverage = '/diffs/set/coverage_reports'; const projectPath = '/root/project'; const dismissEndpoint = '/-/user_callouts'; @@ -83,6 +84,7 @@ describe('DiffsStoreActions', () => { { endpoint, endpointBatch, + endpointDiffForPath, endpointMetadata, endpointCoverage, projectPath, @@ -93,6 +95,7 @@ describe('DiffsStoreActions', () => { { endpoint: '', endpointBatch: '', + endpointDiffForPath: '', endpointMetadata: '', endpointCoverage: '', projectPath: '', @@ -106,6 +109,7 @@ describe('DiffsStoreActions', () => { endpoint, endpointMetadata, endpointBatch, + endpointDiffForPath, endpointCoverage, projectPath, dismissEndpoint, @@ -236,13 +240,17 @@ describe('DiffsStoreActions', () => { it('should show no warning on any other status code', async () => { mock.onGet(endpointMetadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - await testAction( - diffActions.fetchDiffFilesMeta, - {}, - { endpointMetadata, diffViewType: 'inline', showWhitespace: true }, - [{ type: types.SET_LOADING, payload: true }], - [], - ); + try { + await testAction( + diffActions.fetchDiffFilesMeta, + {}, + { endpointMetadata, diffViewType: 'inline', showWhitespace: true }, + [{ type: types.SET_LOADING, payload: true }], + [], + ); + } catch (error) { + expect(error.response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR); + } expect(createAlert).not.toHaveBeenCalled(); }); @@ -265,7 +273,7 @@ describe('DiffsStoreActions', () => { ); }); - it('should show flash on API error', async () => { + it('should show alert on API error', async () => { mock.onGet(endpointCoverage).reply(HTTP_STATUS_BAD_REQUEST); await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []); @@ -389,7 +397,7 @@ describe('DiffsStoreActions', () => { return testAction( diffActions.assignDiscussionsToDiff, [], - { diffFiles: [] }, + { diffFiles: [], flatBlobsList: [] }, [], [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }], ); @@ -1007,20 +1015,14 @@ describe('DiffsStoreActions', () => { describe('setShowWhitespace', () => { const endpointUpdateUser = 'user/prefs'; let putSpy; - let gon; beforeEach(() => { putSpy = jest.spyOn(axios, 'put'); - gon = window.gon; mock.onPut(endpointUpdateUser).reply(HTTP_STATUS_OK, {}); jest.spyOn(eventHub, '$emit').mockImplementation(); }); - afterEach(() => { - window.gon = gon; - }); - it('commits SET_SHOW_WHITESPACE', () => { return testAction( diffActions.setShowWhitespace, @@ -1393,39 +1395,38 @@ describe('DiffsStoreActions', () => { describe('setCurrentDiffFileIdFromNote', () => { it('commits SET_CURRENT_DIFF_FILE', () => { const commit = jest.fn(); - const state = { diffFiles: [{ file_hash: '123' }] }; + const getters = { flatBlobsList: [{ fileHash: '123' }] }; const rootGetters = { getDiscussion: () => ({ diff_file: { file_hash: '123' } }), notesById: { 1: { discussion_id: '2' } }, }; - diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1'); + diffActions.setCurrentDiffFileIdFromNote({ commit, getters, rootGetters }, '1'); expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, '123'); }); it('does not commit SET_CURRENT_DIFF_FILE when discussion has no diff_file', () => { const commit = jest.fn(); - const state = { diffFiles: [{ file_hash: '123' }] }; const rootGetters = { getDiscussion: () => ({ id: '1' }), notesById: { 1: { discussion_id: '2' } }, }; - diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1'); + diffActions.setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1'); expect(commit).not.toHaveBeenCalled(); }); it('does not commit SET_CURRENT_DIFF_FILE when diff file does not exist', () => { const commit = jest.fn(); - const state = { diffFiles: [{ file_hash: '123' }] }; + const getters = { flatBlobsList: [{ fileHash: '123' }] }; const rootGetters = { getDiscussion: () => ({ diff_file: { file_hash: '124' } }), notesById: { 1: { discussion_id: '2' } }, }; - diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1'); + diffActions.setCurrentDiffFileIdFromNote({ commit, getters, rootGetters }, '1'); expect(commit).not.toHaveBeenCalled(); }); @@ -1436,7 +1437,7 @@ describe('DiffsStoreActions', () => { return testAction( diffActions.navigateToDiffFileIndex, 0, - { diffFiles: [{ file_hash: '123' }] }, + { flatBlobsList: [{ fileHash: '123' }] }, [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }], [], ); diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js index 2e3a66d5b01..ed7b6699e2c 100644 --- a/spec/frontend/diffs/store/getters_spec.js +++ b/spec/frontend/diffs/store/getters_spec.js @@ -288,6 +288,19 @@ describe('Diffs Module Getters', () => { }); }); + describe('isTreePathLoaded', () => { + it.each` + desc | loaded | path | bool + ${'the file exists and has been loaded'} | ${true} | ${'path/tofile'} | ${true} + ${'the file exists and has not been loaded'} | ${false} | ${'path/tofile'} | ${false} + ${'the file does not exist'} | ${false} | ${'tofile/path'} | ${false} + `('returns $bool when $desc', ({ loaded, path, bool }) => { + localState.treeEntries['path/tofile'] = { diffLoaded: loaded }; + + expect(getters.isTreePathLoaded(localState)(path)).toBe(bool); + }); + }); + describe('allBlobs', () => { it('returns an array of blobs', () => { localState.treeEntries = { @@ -328,7 +341,11 @@ describe('Diffs Module Getters', () => { describe('currentDiffIndex', () => { it('returns index of currently selected diff in diffList', () => { - localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }]; + localState.treeEntries = [ + { type: 'blob', fileHash: '111' }, + { type: 'blob', fileHash: '222' }, + { type: 'blob', fileHash: '333' }, + ]; localState.currentDiffFileId = '222'; expect(getters.currentDiffIndex(localState)).toEqual(1); @@ -339,7 +356,11 @@ describe('Diffs Module Getters', () => { }); it('returns 0 if no diff is selected yet or diff is not found', () => { - localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }]; + localState.treeEntries = [ + { type: 'blob', fileHash: '111' }, + { type: 'blob', fileHash: '222' }, + { type: 'blob', fileHash: '333' }, + ]; localState.currentDiffFileId = ''; expect(getters.currentDiffIndex(localState)).toEqual(0); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index 031e4fe2be2..ed8d7397bbc 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -93,15 +93,20 @@ describe('DiffsStoreMutations', () => { describe('SET_DIFF_DATA_BATCH_DATA', () => { it('should set diff data batch type properly', () => { - const state = { diffFiles: [] }; + const mockFile = getDiffFileMock(); + const state = { + diffFiles: [], + treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } }, + }; const diffMock = { - diff_files: [getDiffFileMock()], + diff_files: [mockFile], }; mutations[types.SET_DIFF_DATA_BATCH](state, diffMock); expect(state.diffFiles[0].renderIt).toEqual(true); expect(state.diffFiles[0].collapsed).toEqual(false); + expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true); }); }); diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index b5c44b084d8..4760a8b7166 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -892,4 +892,61 @@ describe('DiffsStoreUtils', () => { expect(files[6].right).toBeNull(); }); }); + + describe('isUrlHashNoteLink', () => { + it.each` + input | bool + ${'#note_12345'} | ${true} + ${'#12345'} | ${false} + ${'note_12345'} | ${true} + ${'12345'} | ${false} + `('returns $bool for $input', ({ bool, input }) => { + expect(utils.isUrlHashNoteLink(input)).toBe(bool); + }); + }); + + describe('isUrlHashFileHeader', () => { + it.each` + input | bool + ${'#diff-content-12345'} | ${true} + ${'#12345'} | ${false} + ${'diff-content-12345'} | ${true} + ${'12345'} | ${false} + `('returns $bool for $input', ({ bool, input }) => { + expect(utils.isUrlHashFileHeader(input)).toBe(bool); + }); + }); + + describe('parseUrlHashAsFileHash', () => { + it.each` + input | currentDiffId | resultId + ${'#note_12345'} | ${'1A2B3C'} | ${'1A2B3C'} + ${'note_12345'} | ${'1A2B3C'} | ${'1A2B3C'} + ${'#note_12345'} | ${undefined} | ${null} + ${'note_12345'} | ${undefined} | ${null} + ${'#diff-content-12345'} | ${undefined} | ${'12345'} + ${'diff-content-12345'} | ${undefined} | ${'12345'} + ${'#diff-content-12345'} | ${'98765'} | ${'12345'} + ${'diff-content-12345'} | ${'98765'} | ${'12345'} + ${'#e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'} + ${'e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'} + ${'#Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null} + ${'Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null} + `('returns $resultId for $input and $currentDiffId', ({ input, currentDiffId, resultId }) => { + expect(utils.parseUrlHashAsFileHash(input, currentDiffId)).toBe(resultId); + }); + }); + + describe('markTreeEntriesLoaded', () => { + it.each` + desc | entries | loaded | outcome + ${'marks an existing entry as loaded'} | ${{ abc: {} }} | ${[{ new_path: 'abc' }]} | ${{ abc: { diffLoaded: true } }} + ${'does nothing if the new file is not found in the tree entries'} | ${{ abc: {} }} | ${[{ new_path: 'def' }]} | ${{ abc: {} }} + ${'leaves entries unmodified if they are not in the loaded files'} | ${{ abc: {}, def: { diffLoaded: true }, ghi: {} }} | ${[{ new_path: 'ghi' }]} | ${{ abc: {}, def: { diffLoaded: true }, ghi: { diffLoaded: true } }} + `('$desc', ({ entries, loaded, outcome }) => { + expect(utils.markTreeEntriesLoaded({ priorEntries: entries, loadedFiles: loaded })).toEqual( + outcome, + ); + }); + }); }); diff --git a/spec/frontend/diffs/utils/tree_worker_utils_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js index 4df5fe75004..b8bd4fcd081 100644 --- a/spec/frontend/diffs/utils/tree_worker_utils_spec.js +++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js @@ -75,8 +75,13 @@ describe('~/diffs/utils/tree_worker_utils', () => { { addedLines: 0, changed: true, + diffLoaded: false, deleted: false, fileHash: 'test', + filePaths: { + new: 'app/index.js', + old: undefined, + }, key: 'app/index.js', name: 'index.js', parentPath: 'app/', @@ -97,8 +102,13 @@ describe('~/diffs/utils/tree_worker_utils', () => { { addedLines: 0, changed: true, + diffLoaded: false, deleted: false, fileHash: 'test', + filePaths: { + new: 'app/test/index.js', + old: undefined, + }, key: 'app/test/index.js', name: 'index.js', parentPath: 'app/test/', @@ -112,8 +122,13 @@ describe('~/diffs/utils/tree_worker_utils', () => { { addedLines: 0, changed: true, + diffLoaded: false, deleted: false, fileHash: 'test', + filePaths: { + new: 'app/test/filepathneedstruncating.js', + old: undefined, + }, key: 'app/test/filepathneedstruncating.js', name: 'filepathneedstruncating.js', parentPath: 'app/test/', @@ -138,8 +153,13 @@ describe('~/diffs/utils/tree_worker_utils', () => { { addedLines: 42, changed: true, + diffLoaded: false, deleted: false, fileHash: 'test', + filePaths: { + new: 'constructor/test/aFile.js', + old: undefined, + }, key: 'constructor/test/aFile.js', name: 'aFile.js', parentPath: 'constructor/test/', @@ -160,10 +180,15 @@ describe('~/diffs/utils/tree_worker_utils', () => { name: 'submodule @ abcdef123', type: 'blob', changed: true, + diffLoaded: false, tempFile: true, submodule: true, deleted: false, fileHash: 'test', + filePaths: { + new: 'submodule @ abcdef123', + old: undefined, + }, addedLines: 1, removedLines: 0, tree: [], @@ -175,10 +200,15 @@ describe('~/diffs/utils/tree_worker_utils', () => { name: 'package.json', type: 'blob', changed: true, + diffLoaded: false, tempFile: false, submodule: undefined, deleted: true, fileHash: 'test', + filePaths: { + new: 'package.json', + old: undefined, + }, addedLines: 0, removedLines: 0, tree: [], diff --git a/spec/frontend/drawio/content_editor_facade_spec.js b/spec/frontend/drawio/content_editor_facade_spec.js new file mode 100644 index 00000000000..673968bac9f --- /dev/null +++ b/spec/frontend/drawio/content_editor_facade_spec.js @@ -0,0 +1,138 @@ +import AxiosMockAdapter from 'axios-mock-adapter'; +import { create } from '~/drawio/content_editor_facade'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import DrawioDiagram from '~/content_editor/extensions/drawio_diagram'; +import axios from '~/lib/utils/axios_utils'; +import { PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML } from '../content_editor/test_constants'; +import { createTestEditor } from '../content_editor/test_utils'; + +describe('drawio/contentEditorFacade', () => { + let tiptapEditor; + let axiosMock; + let contentEditorFacade; + let assetResolver; + const imageURL = '/group1/project1/-/wikis/test-file.drawio.svg'; + const diagramSvg = '<svg></svg>'; + const contentType = 'image/svg+xml'; + const filename = 'test-file.drawio.svg'; + const uploadsPath = '/uploads'; + const canonicalSrc = '/new-diagram.drawio.svg'; + const src = `/uploads${canonicalSrc}`; + + beforeEach(() => { + assetResolver = { + resolveUrl: jest.fn(), + }; + tiptapEditor = createTestEditor({ extensions: [DrawioDiagram] }); + contentEditorFacade = create({ + tiptapEditor, + drawioNodeName: DrawioDiagram.name, + uploadsPath, + assetResolver, + }); + }); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + tiptapEditor.destroy(); + }); + + describe('getDiagram', () => { + describe('when there is a selected diagram', () => { + beforeEach(() => { + tiptapEditor + .chain() + .setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML) + .setNodeSelection(1) + .run(); + axiosMock + .onGet(imageURL) + .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType }); + }); + + it('returns diagram information', async () => { + const diagram = await contentEditorFacade.getDiagram(); + + expect(diagram).toEqual({ + diagramURL: imageURL, + filename, + diagramSvg, + contentType, + }); + }); + }); + + describe('when there is not a selected diagram', () => { + beforeEach(() => { + tiptapEditor.chain().setContent('<p>text</p>').setNodeSelection(1).run(); + }); + + it('returns null', async () => { + const diagram = await contentEditorFacade.getDiagram(); + + expect(diagram).toBe(null); + }); + }); + }); + + describe('updateDiagram', () => { + beforeEach(() => { + tiptapEditor + .chain() + .setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML) + .setNodeSelection(1) + .run(); + + assetResolver.resolveUrl.mockReturnValueOnce(src); + contentEditorFacade.updateDiagram({ uploadResults: { file_path: canonicalSrc } }); + }); + + it('updates selected diagram diagram node src and canonicalSrc', () => { + tiptapEditor.commands.setNodeSelection(1); + expect(tiptapEditor.state.selection.node.attrs).toMatchObject({ + src, + canonicalSrc, + }); + }); + }); + + describe('insertDiagram', () => { + beforeEach(() => { + tiptapEditor.chain().setContent('<p></p>').run(); + + assetResolver.resolveUrl.mockReturnValueOnce(src); + contentEditorFacade.insertDiagram({ uploadResults: { file_path: canonicalSrc } }); + }); + + it('inserts a new draw.io diagram in the document', () => { + tiptapEditor.commands.setNodeSelection(1); + expect(tiptapEditor.state.selection.node.attrs).toMatchObject({ + src, + canonicalSrc, + }); + }); + }); + + describe('uploadDiagram', () => { + it('sends a post request to the uploadsPath containing the diagram svg', async () => { + const link = { markdown: '![](diagram.drawio.svg)' }; + const blob = new Blob([diagramSvg], { type: 'image/svg+xml' }); + const formData = new FormData(); + + formData.append('file', blob, filename); + + axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, { + data: { + link, + }, + }); + + const response = await contentEditorFacade.uploadDiagram({ diagramSvg, filename }); + + expect(response).not.toBe(link); + }); + }); +}); diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js new file mode 100644 index 00000000000..5ef26c04204 --- /dev/null +++ b/spec/frontend/drawio/drawio_editor_spec.js @@ -0,0 +1,479 @@ +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { + DRAWIO_EDITOR_URL, + DRAWIO_FRAME_ID, + DIAGRAM_BACKGROUND_COLOR, + DRAWIO_IFRAME_TIMEOUT, + DIAGRAM_MAX_SIZE, +} from '~/drawio/constants'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; + +jest.mock('~/alert'); + +jest.useFakeTimers(); + +describe('drawio/drawio_editor', () => { + let editorFacade; + let drawioIFrameReceivedMessages; + const diagramURL = `${window.location.origin}/uploads/diagram.drawio.svg`; + const testSvg = '<svg></svg>'; + const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`; + const filename = 'diagram.drawio.svg'; + + const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID); + const waitForDrawioIFrameMessage = ({ messageNumber = 1 } = {}) => + new Promise((resolve) => { + let messageCounter = 0; + const iframe = findDrawioIframe(); + + iframe?.contentWindow.addEventListener('message', (event) => { + drawioIFrameReceivedMessages.push(event); + + messageCounter += 1; + + if (messageCounter === messageNumber) { + resolve(); + } + }); + }); + const expectDrawioIframeMessage = ({ expectation, messageNumber = 1 }) => { + expect(drawioIFrameReceivedMessages).toHaveLength(messageNumber); + expect(JSON.parse(drawioIFrameReceivedMessages[messageNumber - 1].data)).toEqual(expectation); + }; + const postMessageToParentWindow = (data) => { + const event = new Event('message'); + + Object.setPrototypeOf(event, { + source: findDrawioIframe().contentWindow, + data: JSON.stringify(data), + }); + + window.dispatchEvent(event); + }; + + beforeEach(() => { + editorFacade = { + getDiagram: jest.fn(), + uploadDiagram: jest.fn(), + insertDiagram: jest.fn(), + updateDiagram: jest.fn(), + }; + drawioIFrameReceivedMessages = []; + }); + + afterEach(() => { + jest.clearAllMocks(); + findDrawioIframe()?.remove(); + }); + + describe('initializing', () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + }); + + it('creates the drawio editor iframe and attaches it to the body', () => { + expect(findDrawioIframe().getAttribute('src')).toBe(DRAWIO_EDITOR_URL); + }); + + it('sets drawio-editor classname to the iframe', () => { + expect(findDrawioIframe().classList).toContain('drawio-editor'); + }); + }); + + describe(`when parent window does not receive configure event after ${DRAWIO_IFRAME_TIMEOUT} ms`, () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + }); + + it('disposes draw.io iframe', () => { + expect(findDrawioIframe()).not.toBe(null); + jest.runAllTimers(); + expect(findDrawioIframe()).toBe(null); + }); + + it('displays an alert indicating that the draw.io editor could not be loaded', () => { + jest.runAllTimers(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'The diagrams.net editor could not be loaded.', + }); + }); + }); + + describe('when parent window receives configure event', () => { + beforeEach(async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'configure' }); + + await waitForDrawioIFrameMessage(); + }); + + it('sends configure action to the draw.io iframe', async () => { + expectDrawioIframeMessage({ + expectation: { + action: 'configure', + config: { + darkColor: '#202020', + settingsName: 'gitlab', + }, + colorSchemeMeta: false, + }, + }); + }); + + it('does not remove the iframe after the load error timeouts run', async () => { + jest.runAllTimers(); + + expect(findDrawioIframe()).not.toBe(null); + }); + }); + + describe('when parent window receives init event', () => { + describe('when there isn’t a diagram selected', () => { + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce(null); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('sends load action to the draw.io iframe with empty svg and title', async () => { + await waitForDrawioIFrameMessage(); + + expectDrawioIframeMessage({ + expectation: { + action: 'load', + xml: null, + border: 8, + background: DIAGRAM_BACKGROUND_COLOR, + dark: false, + title: null, + }, + }); + }); + }); + + describe('when there is a diagram selected', () => { + const diagramSvg = '<svg></svg>'; + + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce({ + diagramURL, + diagramSvg, + filename, + contentType: 'image/svg+xml', + }); + + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'init' }); + }); + + it('sends load action to the draw.io iframe with the selected diagram svg and filename', async () => { + await waitForDrawioIFrameMessage(); + + // Step 5: The draw.io editor will send the downloaded diagram to the iframe + expectDrawioIframeMessage({ + expectation: { + action: 'load', + xml: diagramSvg, + border: 8, + background: DIAGRAM_BACKGROUND_COLOR, + dark: false, + title: filename, + }, + }); + }); + + it('sets the drawio iframe as visible and resets cursor', async () => { + await waitForDrawioIFrameMessage(); + + expect(findDrawioIframe().style.visibility).toBe('visible'); + expect(findDrawioIframe().style.cursor).toBe(''); + }); + + it('scrolls window to the top', async () => { + await waitForDrawioIFrameMessage(); + + expect(window.scrollX).toBe(0); + }); + }); + + describe.each` + description | errorMessage | diagram + ${'when there is an image selected that is not an svg file'} | ${'The selected image is not a valid SVG diagram'} | ${{ + diagramURL, + contentType: 'image/png', + filename: 'image.png', +}} + ${'when the selected image is not an asset upload'} | ${'The selected image is not an asset uploaded in the application'} | ${{ + diagramSvg: '<svg></svg>', + filename, + contentType: 'image/svg+xml', + diagramURL: 'https://example.com/image.drawio.svg', +}} + ${'when the selected image is too large'} | ${'The selected image is too large.'} | ${{ + diagramSvg: 'x'.repeat(DIAGRAM_MAX_SIZE + 1), + filename, + contentType: 'image/svg+xml', + diagramURL, +}} + `('$description', ({ errorMessage, diagram }) => { + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce(diagram); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('displays an error alert indicating that the image is not a diagram', async () => { + expect(createAlert).toHaveBeenCalledWith({ + message: errorMessage, + error: expect.any(Error), + }); + }); + + it('disposes the draw.io diagram iframe', () => { + expect(findDrawioIframe()).toBe(null); + }); + }); + + describe('when loading a diagram fails', () => { + beforeEach(() => { + editorFacade.getDiagram.mockRejectedValueOnce(new Error()); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('displays an error alert indicating the failure', async () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'Cannot load the diagram into the diagrams.net editor', + error: expect.any(Error), + }); + }); + + it('disposes the draw.io diagram iframe', () => { + expect(findDrawioIframe()).toBe(null); + }); + }); + }); + + describe('when parent window receives prompt event', () => { + describe('when the filename is empty', () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'prompt', value: '' }); + }); + + it('sends prompt action to the draw.io iframe requesting a filename', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 1 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'prompt', + titleKey: 'filename', + okKey: 'save', + defaultValue: 'diagram.drawio.svg', + }, + messageNumber: 1, + }); + }); + + it('sends dialog action to the draw.io iframe indicating that the filename cannot be empty', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 2 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'dialog', + titleKey: 'error', + messageKey: 'filenameShort', + buttonKey: 'ok', + }, + messageNumber: 2, + }); + }); + }); + + describe('when the event data is not empty', () => { + beforeEach(async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'prompt', value: 'diagram.drawio.svg' }); + + await waitForDrawioIFrameMessage(); + }); + + it('starts the saving file process', () => { + expectDrawioIframeMessage({ + expectation: { + action: 'spinner', + show: true, + messageKey: 'saving', + }, + }); + }); + }); + }); + + describe('when parent receives export event', () => { + beforeEach(() => { + editorFacade.uploadDiagram.mockResolvedValueOnce({}); + }); + + it('reloads diagram in the draw.io editor', async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage(); + + expectDrawioIframeMessage({ + expectation: expect.objectContaining({ + action: 'load', + xml: expect.stringContaining(testSvg), + }), + }); + }); + + it('marks the diagram as modified in the draw.io editor', async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 2 }); + + expectDrawioIframeMessage({ + expectation: expect.objectContaining({ + action: 'status', + modified: true, + }), + messageNumber: 2, + }); + }); + + describe('when the diagram filename is set', () => { + const TEST_FILENAME = 'diagram.drawio.svg'; + + beforeEach(() => { + launchDrawioEditor({ editorFacade, filename: TEST_FILENAME }); + }); + + it('displays loading spinner in the draw.io editor', async () => { + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'spinner', + show: true, + messageKey: 'saving', + }, + messageNumber: 3, + }); + }); + + it('uploads exported diagram', async () => { + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expect(editorFacade.uploadDiagram).toHaveBeenCalledWith({ + filename: TEST_FILENAME, + diagramSvg: expect.stringContaining(testSvg), + }); + }); + + describe('when uploading the exported diagram succeeds', () => { + it('displays an alert indicating that the diagram was uploaded successfully', async () => { + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expect(createAlert).toHaveBeenCalledWith({ + message: expect.any(String), + variant: VARIANT_SUCCESS, + fadeTransition: true, + }); + }); + + it('disposes iframe', () => { + jest.runAllTimers(); + + expect(findDrawioIframe()).toBe(null); + }); + }); + + describe('when uploading the exported diagram fails', () => { + const uploadError = new Error(); + + beforeEach(() => { + editorFacade.uploadDiagram.mockReset(); + editorFacade.uploadDiagram.mockRejectedValue(uploadError); + + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + }); + + it('hides loading indicator in the draw.io editor', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 4 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'spinner', + show: false, + }, + messageNumber: 4, + }); + }); + + it('displays an error dialog in the draw.io editor', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 5 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'dialog', + titleKey: 'error', + modified: true, + buttonKey: 'close', + messageKey: 'errorSavingFile', + }, + messageNumber: 5, + }); + }); + }); + }); + + describe('when diagram filename is not set', () => { + it('sends prompt action to the draw.io iframe', async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expect(drawioIFrameReceivedMessages[2].data).toEqual( + JSON.stringify({ + action: 'prompt', + titleKey: 'filename', + okKey: 'save', + defaultValue: 'diagram.drawio.svg', + }), + ); + }); + }); + }); + + describe('when parent window receives exit event', () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + }); + + it('disposes the the draw.io iframe', () => { + expect(findDrawioIframe()).not.toBe(null); + + postMessageToParentWindow({ event: 'exit' }); + + expect(findDrawioIframe()).toBe(null); + }); + }); +}); diff --git a/spec/frontend/drawio/markdown_field_editor_facade_spec.js b/spec/frontend/drawio/markdown_field_editor_facade_spec.js new file mode 100644 index 00000000000..e3eafc63839 --- /dev/null +++ b/spec/frontend/drawio/markdown_field_editor_facade_spec.js @@ -0,0 +1,148 @@ +import AxiosMockAdapter from 'axios-mock-adapter'; +import { create } from '~/drawio/markdown_field_editor_facade'; +import * as textMarkdown from '~/lib/utils/text_markdown'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; + +jest.mock('~/lib/utils/text_markdown'); + +describe('drawio/textareaMarkdownEditor', () => { + let textArea; + let textareaMarkdownEditor; + let axiosMock; + + const markdownPreviewPath = '/markdown/preview'; + const imageURL = '/assets/image.png'; + const diagramMarkdown = '![](image.png)'; + const diagramSvg = '<svg></svg>'; + const contentType = 'image/svg+xml'; + const filename = 'image.png'; + const newDiagramMarkdown = '![](newdiagram.svg)'; + const uploadsPath = '/uploads'; + + beforeEach(() => { + textArea = document.createElement('textarea'); + textareaMarkdownEditor = create({ textArea, markdownPreviewPath, uploadsPath }); + + document.body.appendChild(textArea); + }); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + textArea.remove(); + }); + + describe('getDiagram', () => { + describe('when there is a selected diagram', () => { + beforeEach(() => { + textMarkdown.resolveSelectedImage.mockReturnValueOnce({ + imageURL, + imageMarkdown: diagramMarkdown, + filename, + }); + axiosMock + .onGet(imageURL) + .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType }); + }); + + it('returns diagram information', async () => { + const diagram = await textareaMarkdownEditor.getDiagram(); + + expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith( + textArea, + markdownPreviewPath, + ); + + expect(diagram).toEqual({ + diagramURL: imageURL, + diagramMarkdown, + filename, + diagramSvg, + contentType, + }); + }); + }); + + describe('when there is not a selected diagram', () => { + beforeEach(() => { + textMarkdown.resolveSelectedImage.mockReturnValueOnce(null); + }); + + it('returns null', async () => { + const diagram = await textareaMarkdownEditor.getDiagram(); + + expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith( + textArea, + markdownPreviewPath, + ); + + expect(diagram).toBe(null); + }); + }); + }); + + describe('updateDiagram', () => { + beforeEach(() => { + jest.spyOn(textArea, 'focus'); + jest.spyOn(textArea, 'dispatchEvent'); + + textArea.value = `diagram ${diagramMarkdown}`; + + textareaMarkdownEditor.updateDiagram({ + diagramMarkdown, + uploadResults: { link: { markdown: newDiagramMarkdown } }, + }); + }); + + it('focuses the textarea', () => { + expect(textArea.focus).toHaveBeenCalled(); + }); + + it('replaces previous diagram markdown with new diagram markdown', () => { + expect(textArea.value).toBe(`diagram ${newDiagramMarkdown}`); + }); + + it('dispatches input event in the textarea', () => { + expect(textArea.dispatchEvent).toHaveBeenCalledWith(new Event('input')); + }); + }); + + describe('insertDiagram', () => { + it('inserts markdown text and replaces any selected markdown in the textarea', () => { + textArea.value = `diagram ${diagramMarkdown}`; + textArea.setSelectionRange(0, 8); + + textareaMarkdownEditor.insertDiagram({ + uploadResults: { link: { markdown: newDiagramMarkdown } }, + }); + + expect(textMarkdown.insertMarkdownText).toHaveBeenCalledWith({ + textArea, + text: textArea.value, + tag: newDiagramMarkdown, + selected: textArea.value.substring(0, 8), + }); + }); + }); + + describe('uploadDiagram', () => { + it('sends a post request to the uploadsPath containing the diagram svg', async () => { + const link = { markdown: '![](diagram.drawio.svg)' }; + const blob = new Blob([diagramSvg], { type: 'image/svg+xml' }); + const formData = new FormData(); + + formData.append('file', blob, filename); + + axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, { + link, + }); + + const response = await textareaMarkdownEditor.uploadDiagram({ diagramSvg, filename }); + + expect(response).toEqual({ link }); + }); + }); +}); diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js index 12f90390c18..5cc66dd2ae0 100644 --- a/spec/frontend/editor/components/helpers.js +++ b/spec/frontend/editor/components/helpers.js @@ -1,4 +1,3 @@ -import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants'; import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql'; import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql'; @@ -9,7 +8,7 @@ export const buildButton = (id = 'foo-bar-btn', options = {}) => { label: options.label || 'Foo Bar Button', icon: options.icon || 'check', selected: options.selected || false, - group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP, + group: options.group, onClick: options.onClick || (() => {}), category: options.category || 'primary', selectedLabel: options.selectedLabel || 'smth', diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js index ff377494312..79692ab4557 100644 --- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js +++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js @@ -21,11 +21,6 @@ describe('Source Editor Toolbar button', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('default', () => { const defaultProps = { category: 'primary', diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js index bead39ca744..f737340a317 100644 --- a/spec/frontend/editor/components/source_editor_toolbar_spec.js +++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js @@ -5,7 +5,7 @@ import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue'; import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue'; -import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants'; +import { EDITOR_TOOLBAR_BUTTON_GROUPS } from '~/editor/constants'; import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql'; import { buildButton } from './helpers'; @@ -40,24 +40,24 @@ describe('Source Editor Toolbar', () => { }; afterEach(() => { - wrapper.destroy(); mockApollo = null; }); describe('groups', () => { it.each` - group | expectedGroup - ${EDITOR_TOOLBAR_LEFT_GROUP} | ${EDITOR_TOOLBAR_LEFT_GROUP} - ${EDITOR_TOOLBAR_RIGHT_GROUP} | ${EDITOR_TOOLBAR_RIGHT_GROUP} - ${undefined} | ${EDITOR_TOOLBAR_RIGHT_GROUP} - ${'non-existing'} | ${EDITOR_TOOLBAR_RIGHT_GROUP} + group | expectedGroup + ${EDITOR_TOOLBAR_BUTTON_GROUPS.file} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.file} + ${EDITOR_TOOLBAR_BUTTON_GROUPS.edit} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.edit} + ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings} + ${undefined} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings} + ${'non-existing'} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings} `('puts item with group="$group" into $expectedGroup group', ({ group, expectedGroup }) => { const item = buildButton('first', { group, }); createComponentWithApollo([item]); expect(findButtons()).toHaveLength(1); - [EDITOR_TOOLBAR_RIGHT_GROUP, EDITOR_TOOLBAR_LEFT_GROUP].forEach((g) => { + Object.keys(EDITOR_TOOLBAR_BUTTON_GROUPS).forEach((g) => { if (g === expectedGroup) { expect(wrapper.vm.getGroupItems(g)).toEqual([expect.objectContaining({ id: 'first' })]); } else { @@ -70,7 +70,7 @@ describe('Source Editor Toolbar', () => { describe('buttons update', () => { it('properly updates buttons on Apollo cache update', async () => { const item = buildButton('first', { - group: EDITOR_TOOLBAR_RIGHT_GROUP, + group: EDITOR_TOOLBAR_BUTTON_GROUPS.edit, }); createComponentWithApollo(); @@ -95,12 +95,15 @@ describe('Source Editor Toolbar', () => { describe('click handler', () => { it('emits the "click" event when a button is clicked', () => { const item1 = buildButton('first', { - group: EDITOR_TOOLBAR_LEFT_GROUP, + group: EDITOR_TOOLBAR_BUTTON_GROUPS.file, }); const item2 = buildButton('second', { - group: EDITOR_TOOLBAR_RIGHT_GROUP, + group: EDITOR_TOOLBAR_BUTTON_GROUPS.edit, }); - createComponentWithApollo([item1, item2]); + const item3 = buildButton('third', { + group: EDITOR_TOOLBAR_BUTTON_GROUPS.settings, + }); + createComponentWithApollo([item1, item2, item3]); jest.spyOn(wrapper.vm, '$emit'); expect(wrapper.vm.$emit).not.toHaveBeenCalled(); @@ -110,7 +113,10 @@ describe('Source Editor Toolbar', () => { findButtons().at(1).vm.$emit('click'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2); - expect(wrapper.vm.$emit.mock.calls).toHaveLength(2); + findButtons().at(2).vm.$emit('click'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item3); + + expect(wrapper.vm.$emit.mock.calls).toHaveLength(3); }); }); }); diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index c822a0bfeaf..87208ec7aa8 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -33,6 +33,7 @@ import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml'; import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml'; import HooksYaml from './yaml_tests/positive_tests/hooks.yml'; import SecretsYaml from './yaml_tests/positive_tests/secrets.yml'; +import ServicesYaml from './yaml_tests/positive_tests/services.yml'; // YAML NEGATIVE TEST import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml'; @@ -52,6 +53,7 @@ import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variabl import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml'; import HooksNegative from './yaml_tests/negative_tests/hooks.yml'; import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml'; +import ServicesNegativeYaml from './yaml_tests/negative_tests/services.yml'; const ajv = new Ajv({ strictTypes: false, @@ -89,6 +91,7 @@ describe('positive tests', () => { VariablesYaml, ProjectPathYaml, IdTokensYaml, + ServicesYaml, SecretsYaml, }), )('schema validates %s', (_, input) => { @@ -113,10 +116,12 @@ describe('negative tests', () => { // YAML ArtifactsNegativeYaml, CacheKeyNeative, + HooksNegative, IdTokensNegativeYaml, IncludeNegativeYaml, JobWhenNegativeYaml, RulesNegativeYaml, + TriggerNegative, VariablesInvalidOptionsYaml, VariablesInvalidSyntaxDescYaml, VariablesWrongSyntaxUsageExpand, @@ -126,8 +131,7 @@ describe('negative tests', () => { ProjectPathIncludeNoSlashYaml, ProjectPathIncludeTailSlashYaml, SecretsNegativeYaml, - TriggerNegative, - HooksNegative, + ServicesNegativeYaml, }), )('schema validates %s', (_, input) => { // We construct a new "JSON" from each main key that is inside a diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml new file mode 100644 index 00000000000..6761a603a0a --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml @@ -0,0 +1,38 @@ +empty_services: + services: + +without_name: + services: + - alias: db-postgres + entrypoint: ["/usr/local/bin/db-postgres"] + command: ["start"] + +invalid_entrypoint: + services: + - name: my-postgres:11.7 + alias: db-postgres + entrypoint: "/usr/local/bin/db-postgres" # must be array + +empty_entrypoint: + services: + - name: my-postgres:11.7 + alias: db-postgres + entrypoint: [] + +invalid_command: + services: + - name: my-postgres:11.7 + alias: db-postgres + command: "start" # must be array + +empty_command: + services: + - name: my-postgres:11.7 + alias: db-postgres + command: [] + +empty_pull_policy: + script: echo "Multiple pull policies." + services: + - name: postgres:11.6 + pull_policy: [] diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml new file mode 100644 index 00000000000..8a0f59d1dfd --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml @@ -0,0 +1,31 @@ +valid_services_list: + services: + - php:7 + - node:latest + - golang:1.10 + +valid_services_object: + services: + - name: my-postgres:11.7 + alias: db-postgres + entrypoint: ["/usr/local/bin/db-postgres"] + command: ["start"] + +services_with_variables: + services: + - name: bitnami/nginx + alias: nginx + variables: + NGINX_HTTP_PORT_NUMBER: ${NGINX_HTTP_PORT_NUMBER} + +pull_policy_string: + script: echo "A single pull policy." + services: + - name: postgres:11.6 + pull_policy: if-not-present + +pull_policy_array: + script: echo "Multiple pull policies." + services: + - name: postgres:11.6 + pull_policy: [always, if-not-present] diff --git a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js index 21f8979f1a9..e515285601b 100644 --- a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js +++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js @@ -17,7 +17,6 @@ describe('~/editor/editor_ci_config_ext', () => { let editor; let instance; let editorEl; - let originalGitlabUrl; const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => { setHTMLFixture('<div id="editor"></div>'); @@ -31,16 +30,8 @@ describe('~/editor/editor_ci_config_ext', () => { instance.use({ definition: CiSchemaExtension }); }; - beforeAll(() => { - originalGitlabUrl = gon.gitlab_url; - gon.gitlab_url = TEST_HOST; - }); - - afterAll(() => { - gon.gitlab_url = originalGitlabUrl; - }); - beforeEach(() => { + gon.gitlab_url = TEST_HOST; createMockEditor(); }); diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js index eab39ccaba1..b1b8173188c 100644 --- a/spec/frontend/editor/source_editor_extension_base_spec.js +++ b/spec/frontend/editor/source_editor_extension_base_spec.js @@ -7,6 +7,7 @@ import { EDITOR_TYPE_DIFF, EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS, EXTENSION_BASE_LINE_NUMBERS_CLASS, + EXTENSION_SOFTWRAP_ID, } from '~/editor/constants'; import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base'; import EditorInstance from '~/editor/source_editor_instance'; @@ -35,8 +36,18 @@ describe('The basis for an Source Editor extension', () => { }, }; }; - const createInstance = (baseInstance = {}) => { - return new EditorInstance(baseInstance); + const baseInstance = { + getOption: jest.fn(), + }; + + const createInstance = (base = baseInstance) => { + return new EditorInstance(base); + }; + + const toolbar = { + addItems: jest.fn(), + updateItem: jest.fn(), + removeItems: jest.fn(), }; beforeEach(() => { @@ -49,6 +60,66 @@ describe('The basis for an Source Editor extension', () => { resetHTMLFixture(); }); + describe('onSetup callback', () => { + let instance; + beforeEach(() => { + instance = createInstance(); + + instance.toolbar = toolbar; + }); + + it('adds correct buttons to the toolbar', () => { + instance.use({ definition: SourceEditorExtension }); + expect(instance.toolbar.addItems).toHaveBeenCalledWith([ + expect.objectContaining({ + id: EXTENSION_SOFTWRAP_ID, + }), + ]); + }); + + it('does not fail if toolbar is not available', () => { + instance.toolbar = null; + expect(() => instance.use({ definition: SourceEditorExtension })).not.toThrow(); + }); + + it.each` + optionValue | expectSelected + ${'on'} | ${true} + ${'off'} | ${false} + ${'foo'} | ${false} + ${undefined} | ${false} + ${null} | ${false} + `( + 'correctly sets the initial state of the button when wordWrap option is "$optionValue"', + ({ optionValue, expectSelected }) => { + instance.getOption.mockReturnValue(optionValue); + instance.use({ definition: SourceEditorExtension }); + expect(instance.toolbar.addItems).toHaveBeenCalledWith([ + expect.objectContaining({ + selected: expectSelected, + }), + ]); + }, + ); + }); + + describe('onBeforeUnuse', () => { + let instance; + let extension; + + beforeEach(() => { + instance = createInstance(); + + instance.toolbar = toolbar; + extension = instance.use({ definition: SourceEditorExtension }); + }); + it('removes the registered buttons from the toolbar', () => { + expect(instance.toolbar.removeItems).not.toHaveBeenCalled(); + instance.unuse(extension); + expect(instance.toolbar.removeItems).toHaveBeenCalledWith([EXTENSION_SOFTWRAP_ID]); + }); + }); + describe('onUse callback', () => { it('initializes the line highlighting', () => { const instance = createInstance(); @@ -66,6 +137,7 @@ describe('The basis for an Source Editor extension', () => { '$description the line linking for $instanceType instance', ({ instanceType, shouldBeCalled }) => { const instance = createInstance({ + ...baseInstance, getEditorType: jest.fn().mockReturnValue(instanceType), onMouseMove: jest.fn(), onMouseDown: jest.fn(), @@ -82,10 +154,44 @@ describe('The basis for an Source Editor extension', () => { ); }); + describe('toggleSoftwrap', () => { + let instance; + + beforeEach(() => { + instance = createInstance(); + + instance.toolbar = toolbar; + instance.use({ definition: SourceEditorExtension }); + }); + + it.each` + currentWordWrap | newWordWrap | expectSelected + ${'on'} | ${'off'} | ${false} + ${'off'} | ${'on'} | ${true} + ${'foo'} | ${'on'} | ${true} + ${undefined} | ${'on'} | ${true} + ${null} | ${'on'} | ${true} + `( + 'correctly updates wordWrap option in editor and the state of the button when currentWordWrap is "$currentWordWrap"', + ({ currentWordWrap, newWordWrap, expectSelected }) => { + instance.getOption.mockReturnValue(currentWordWrap); + instance.updateOptions = jest.fn(); + instance.toggleSoftwrap(); + expect(instance.updateOptions).toHaveBeenCalledWith({ + wordWrap: newWordWrap, + }); + expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, { + selected: expectSelected, + }); + }, + ); + }); + describe('highlightLines', () => { const revealSpy = jest.fn(); const decorationsSpy = jest.fn(); const instance = createInstance({ + ...baseInstance, revealLineInCenter: revealSpy, deltaDecorations: decorationsSpy, }); @@ -174,6 +280,7 @@ describe('The basis for an Source Editor extension', () => { beforeEach(() => { instance = createInstance({ + ...baseInstance, deltaDecorations: decorationsSpy, lineDecorations, }); @@ -188,6 +295,7 @@ describe('The basis for an Source Editor extension', () => { describe('setupLineLinking', () => { const instance = { + ...baseInstance, onMouseMove: jest.fn(), onMouseDown: jest.fn(), deltaDecorations: jest.fn(), diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js index 33e4b4bfc8e..b226ef03b33 100644 --- a/spec/frontend/editor/source_editor_markdown_ext_spec.js +++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js @@ -17,6 +17,7 @@ describe('Markdown Extension for Source Editor', () => { const thirdLine = 'string with some **markup**'; const text = `${firstLine}\n${secondLine}\n${thirdLine}`; const markdownPath = 'foo.md'; + let extensions; const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => { const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn); @@ -38,7 +39,10 @@ describe('Markdown Extension for Source Editor', () => { blobPath: markdownPath, blobContent: text, }); - instance.use([{ definition: ToolbarExtension }, { definition: EditorMarkdownExtension }]); + extensions = instance.use([ + { definition: ToolbarExtension }, + { definition: EditorMarkdownExtension }, + ]); }); afterEach(() => { @@ -59,6 +63,25 @@ describe('Markdown Extension for Source Editor', () => { }); }); + describe('markdown keystrokes', () => { + it('registers all keystrokes as actions', () => { + EXTENSION_MARKDOWN_BUTTONS.forEach((button) => { + if (button.data.mdShortcuts) { + expect(instance.getAction(button.id)).toBeDefined(); + } + }); + }); + + it('disposes all keystrokes on unuse', () => { + instance.unuse(extensions[1]); + EXTENSION_MARKDOWN_BUTTONS.forEach((button) => { + if (button.data.mdShortcuts) { + expect(instance.getAction(button.id)).toBeNull(); + } + }); + }); + }); + describe('getSelectedText', () => { it('does not fail if there is no selection and returns the empty string', () => { jest.spyOn(instance, 'getSelection'); diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js index c42ac28c498..895eb87833d 100644 --- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js +++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js @@ -12,14 +12,14 @@ import { } from '~/editor/constants'; import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext'; import SourceEditor from '~/editor/source_editor'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import syntaxHighlight from '~/syntax_highlight'; import { spyOnApi } from './helpers'; jest.mock('~/syntax_highlight'); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Markdown Live Preview Extension for Source Editor', () => { let editor; diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js index f418eab668a..7e4079c17f7 100644 --- a/spec/frontend/editor/source_editor_webide_ext_spec.js +++ b/spec/frontend/editor/source_editor_webide_ext_spec.js @@ -13,7 +13,6 @@ describe('Source Editor Web IDE Extension', () => { editorEl = document.getElementById('editor'); editor = new SourceEditor(); }); - afterEach(() => {}); describe('onSetup', () => { it.each` diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js index e561cad1086..13b8a9804b0 100644 --- a/spec/frontend/editor/utils_spec.js +++ b/spec/frontend/editor/utils_spec.js @@ -54,19 +54,17 @@ describe('Source Editor utils', () => { describe('getBlobLanguage', () => { it.each` - path | expectedLanguage - ${'foo.js'} | ${'javascript'} - ${'foo.js.rb'} | ${'ruby'} - ${'foo.bar'} | ${'plaintext'} - ${undefined} | ${'plaintext'} - `( - 'sets the $expectedThemeName theme when $themeName is set in the user preference', - ({ path, expectedLanguage }) => { - const language = utils.getBlobLanguage(path); + path | expectedLanguage + ${'foo.js'} | ${'javascript'} + ${'foo.js.rb'} | ${'ruby'} + ${'foo.bar'} | ${'plaintext'} + ${undefined} | ${'plaintext'} + ${'foo/bar/foo.js'} | ${'javascript'} + `(`returns '$expectedLanguage' for '$path' path`, ({ path, expectedLanguage }) => { + const language = utils.getBlobLanguage(path); - expect(language).toEqual(expectedLanguage); - }, - ); + expect(language).toEqual(expectedLanguage); + }); }); describe('setupCodeSnipet', () => { diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js index 90816f28d5b..272c1a09a69 100644 --- a/spec/frontend/emoji/components/category_spec.js +++ b/spec/frontend/emoji/components/category_spec.js @@ -9,11 +9,12 @@ function factory(propsData = {}) { wrapper = shallowMount(Category, { propsData }); } -describe('Emoji category component', () => { - afterEach(() => { - wrapper.destroy(); - }); +const triggerGlIntersectionObserver = () => { + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); + return nextTick(); +}; +describe('Emoji category component', () => { beforeEach(() => { factory({ category: 'Activity', @@ -26,25 +27,19 @@ describe('Emoji category component', () => { }); it('renders group', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - await wrapper.setData({ renderGroup: true }); + await triggerGlIntersectionObserver(); expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true'); }); it('renders group on appear', async () => { - wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); - - await nextTick(); + await triggerGlIntersectionObserver(); expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true'); }); it('emits appear event on appear', async () => { - wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); - - await nextTick(); + await triggerGlIntersectionObserver(); expect(wrapper.emitted().appear[0]).toEqual(['Activity']); }); diff --git a/spec/frontend/emoji/components/emoji_group_spec.js b/spec/frontend/emoji/components/emoji_group_spec.js index 1aca2fbb8fc..75397ce25ff 100644 --- a/spec/frontend/emoji/components/emoji_group_spec.js +++ b/spec/frontend/emoji/components/emoji_group_spec.js @@ -15,10 +15,6 @@ function factory(propsData = {}) { } describe('Emoji group component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('does not render any buttons', () => { factory({ emojis: [], diff --git a/spec/frontend/emoji/components/emoji_list_spec.js b/spec/frontend/emoji/components/emoji_list_spec.js index a72ba614d9f..f6f6062f8e8 100644 --- a/spec/frontend/emoji/components/emoji_list_spec.js +++ b/spec/frontend/emoji/components/emoji_list_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import EmojiList from '~/emoji/components/emoji_list.vue'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/emoji', () => ({ initEmojiMap: jest.fn(() => Promise.resolve()), @@ -14,7 +14,8 @@ jest.mock('~/emoji', () => ({ })); let wrapper; -async function factory(render, propsData = { searchValue: '' }) { + +function factory(propsData = { searchValue: '' }) { wrapper = extendedWrapper( shallowMount(EmojiList, { propsData, @@ -23,35 +24,23 @@ async function factory(render, propsData = { searchValue: '' }) { }, }), ); - - // Wait for categories to be set - await nextTick(); - - if (render) { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ render: true }); - - // Wait for component to render - await nextTick(); - } } const findDefaultSlot = () => wrapper.findByTestId('default-slot'); describe('Emoji list component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('does not render until render is set', async () => { - await factory(false); + factory(); expect(findDefaultSlot().exists()).toBe(false); + await waitForPromises(); + expect(findDefaultSlot().exists()).toBe(true); }); it('renders with none filtered list', async () => { - await factory(true); + factory(); + + await waitForPromises(); expect(JSON.parse(findDefaultSlot().text())).toEqual({ activity: { @@ -63,7 +52,9 @@ describe('Emoji list component', () => { }); it('renders filtered list of emojis', async () => { - await factory(true, { searchValue: 'smile' }); + factory({ searchValue: 'smile' }); + + await waitForPromises(); expect(JSON.parse(findDefaultSlot().text())).toEqual({ search: { diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index 82e3b50aeb8..7b160c48501 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -8,6 +8,7 @@ const { setGlobalDateToRealDate, } = require('./__helpers__/fake_date/fake_date'); const { TEST_HOST } = require('./__helpers__/test_constants'); +const { createGon } = require('./__helpers__/gon_helper'); const ROOT_PATH = path.resolve(__dirname, '../..'); @@ -40,11 +41,12 @@ class CustomEnvironment extends TestEnvironment { }); const { IS_EE } = projectConfig.testEnvironmentOptions; - this.global.gon = { - ee: IS_EE, - }; + this.global.IS_EE = IS_EE; + // Set up global `gon` object + this.global.gon = createGon(IS_EE); + // Set up global `gl` object this.global.gl = {}; diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js index 340740e6499..17ecd93361f 100644 --- a/spec/frontend/environments/canary_ingress_spec.js +++ b/spec/frontend/environments/canary_ingress_spec.js @@ -23,7 +23,7 @@ describe('/environments/components/canary_ingress.vue', () => { ...props, }, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, ...options, }); @@ -37,8 +37,6 @@ describe('/environments/components/canary_ingress.vue', () => { if (wrapper) { wrapper.destroy(); } - - wrapper = null; }); describe('stable weight', () => { diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js index 31b1770da59..a101ed4e00a 100644 --- a/spec/frontend/environments/canary_update_modal_spec.js +++ b/spec/frontend/environments/canary_update_modal_spec.js @@ -34,8 +34,6 @@ describe('/environments/components/canary_update_modal.vue', () => { if (wrapper) { wrapper.destroy(); } - - wrapper = null; }); beforeEach(() => { @@ -47,7 +45,7 @@ describe('/environments/components/canary_update_modal.vue', () => { modalId: 'confirm-canary-change', actionPrimary: { text: 'Change ratio', - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, }, actionCancel: { text: 'Cancel' }, }); diff --git a/spec/frontend/environments/delete_environment_modal_spec.js b/spec/frontend/environments/delete_environment_modal_spec.js index cc18bf754eb..96f6ce52a9c 100644 --- a/spec/frontend/environments/delete_environment_modal_spec.js +++ b/spec/frontend/environments/delete_environment_modal_spec.js @@ -6,10 +6,10 @@ import { s__, sprintf } from '~/locale'; import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { resolvedEnvironment } from './graphql/mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); describe('~/environments/components/delete_environment_modal.vue', () => { @@ -67,7 +67,7 @@ describe('~/environments/components/delete_environment_modal.vue', () => { ); }); - it('should flash a message on error', async () => { + it('should alert a message on error', async () => { createComponent({ apolloProvider: mockApollo }); deleteResolver.mockRejectedValue(); diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js index fb1a8b8c00a..34f338fabe6 100644 --- a/spec/frontend/environments/edit_environment_spec.js +++ b/spec/frontend/environments/edit_environment_spec.js @@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import EditEnvironment from '~/environments/components/edit_environment.vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; jest.mock('~/lib/utils/url_utility'); -jest.mock('~/flash'); +jest.mock('~/alert'); const DEFAULT_OPTS = { provide: { @@ -37,7 +37,6 @@ describe('~/environments/components/edit.vue', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); const findNameInput = () => wrapper.findByLabelText('Name'); diff --git a/spec/frontend/environments/empty_state_spec.js b/spec/frontend/environments/empty_state_spec.js index 02cf2dc3c68..d067c4c80e0 100644 --- a/spec/frontend/environments/empty_state_spec.js +++ b/spec/frontend/environments/empty_state_spec.js @@ -29,10 +29,6 @@ describe('~/environments/components/empty_state.vue', () => { provide: { newEnvironmentPath: NEW_PATH }, }); - afterEach(() => { - wrapper.destroy(); - }); - it('shows an empty state for available environments', () => { wrapper = createWrapper(); diff --git a/spec/frontend/environments/enable_review_app_modal_spec.js b/spec/frontend/environments/enable_review_app_modal_spec.js index 7939bd600dc..ee728775980 100644 --- a/spec/frontend/environments/enable_review_app_modal_spec.js +++ b/spec/frontend/environments/enable_review_app_modal_spec.js @@ -18,10 +18,6 @@ describe('Enable Review App Modal', () => { const findInstructionAt = (i) => wrapper.findAll('ol li').at(i); const findCopyString = () => wrapper.find(`#${EXPECTED_COPY_PRE_ID}`); - afterEach(() => { - wrapper.destroy(); - }); - describe('renders the modal', () => { beforeEach(() => { wrapper = extendedWrapper( diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js index 48483152f7a..3c9b4144e45 100644 --- a/spec/frontend/environments/environment_actions_spec.js +++ b/spec/frontend/environments/environment_actions_spec.js @@ -36,7 +36,7 @@ describe('EnvironmentActions Component', () => { wrapper = mountFn(EnvironmentActions, { propsData: { actions: [], ...props }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, ...options, }); @@ -52,7 +52,6 @@ describe('EnvironmentActions Component', () => { }; afterEach(() => { - wrapper.destroy(); confirmAction.mockReset(); }); diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js index a37515bc3f7..279ff32a13d 100644 --- a/spec/frontend/environments/environment_folder_spec.js +++ b/spec/frontend/environments/environment_folder_spec.js @@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => { ...propsData, }, stubs: { transition: stubTransition() }, - provide: { helpPagePath: '/help' }, + provide: { helpPagePath: '/help', projectId: '1' }, }); beforeEach(async () => { diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js index b9b34bee80f..50e4e637aa3 100644 --- a/spec/frontend/environments/environment_form_spec.js +++ b/spec/frontend/environments/environment_form_spec.js @@ -15,19 +15,16 @@ const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings describe('~/environments/components/form.vue', () => { let wrapper; - const createWrapper = (propsData = {}) => + const createWrapper = (propsData = {}, options = {}) => mountExtended(EnvironmentForm, { provide: PROVIDE, + ...options, propsData: { ...DEFAULT_PROPS, ...propsData, }, }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { wrapper = createWrapper(); @@ -105,6 +102,7 @@ describe('~/environments/components/form.vue', () => { wrapper = createWrapper({ loading: true }); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); + describe('when a new environment is being created', () => { beforeEach(() => { wrapper = createWrapper({ @@ -133,6 +131,18 @@ describe('~/environments/components/form.vue', () => { }); }); + describe('when no protected environment link is provided', () => { + beforeEach(() => { + wrapper = createWrapper({ + provide: {}, + }); + }); + + it('does not show protected environment documentation', () => { + expect(wrapper.findByRole('link', { name: 'Protected environments' }).exists()).toBe(false); + }); + }); + describe('when an existing environment is being edited', () => { beforeEach(() => { wrapper = createWrapper({ diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js index dd909cf4473..59e94dfd662 100644 --- a/spec/frontend/environments/environment_item_spec.js +++ b/spec/frontend/environments/environment_item_spec.js @@ -56,10 +56,6 @@ describe('Environment item', () => { findUpcomingDeployment().findComponent(GlAvatarLink); const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar); - afterEach(() => { - wrapper.destroy(); - }); - describe('when item is not folder', () => { it('should render environment name', () => { expect(wrapper.find('.environment-name').text()).toContain(environment.name); @@ -390,10 +386,6 @@ describe('Environment item', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render folder icon and name', () => { expect(wrapper.find('.folder-name').text()).toContain(folder.name); expect(wrapper.find('.folder-icon')).toBeDefined(); diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js index 170036b5b00..2f38dea2833 100644 --- a/spec/frontend/environments/environment_pin_spec.js +++ b/spec/frontend/environments/environment_pin_spec.js @@ -31,10 +31,6 @@ describe('Pin Component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render the component with descriptive text', () => { expect(wrapper.text()).toBe('Prevent auto-stopping'); }); @@ -64,10 +60,6 @@ describe('Pin Component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render the component with descriptive text', () => { expect(wrapper.text()).toBe('Prevent auto-stopping'); }); diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js index a86cfdd56ba..652b0f807fe 100644 --- a/spec/frontend/environments/environment_table_spec.js +++ b/spec/frontend/environments/environment_table_spec.js @@ -34,10 +34,6 @@ describe('Environment table', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('Should render a table', async () => { const mockItem = { name: 'review', diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js index 986ecca4e84..a843f801da5 100644 --- a/spec/frontend/environments/environments_app_spec.js +++ b/spec/frontend/environments/environments_app_spec.js @@ -96,10 +96,6 @@ describe('~/environments/components/environments_app.vue', () => { paginationMock = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should request available environments if the scope is invalid', async () => { await createWrapperWithMocked({ environmentsApp: resolvedEnvironmentsApp, diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js index 1f233c05fbf..8574743919f 100644 --- a/spec/frontend/environments/environments_detail_header_spec.js +++ b/spec/frontend/environments/environments_detail_header_spec.js @@ -47,7 +47,7 @@ describe('Environments detail header component', () => { TimeAgo, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { canAdminEnvironment: false, @@ -59,10 +59,6 @@ describe('Environments detail header component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('default state with minimal access', () => { beforeEach(() => { createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } }); diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js index a87060f83d8..75fb3a31120 100644 --- a/spec/frontend/environments/environments_folder_view_spec.js +++ b/spec/frontend/environments/environments_folder_view_spec.js @@ -24,7 +24,6 @@ describe('Environments Folder View', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('successful request', () => { diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index 5ea0be41614..b5435990042 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -798,3 +798,9 @@ export const resolvedDeploymentDetails = { }, }, }; + +export const agent = { + project: 'agent-project', + id: '1', + name: 'agent-name', +}; diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js new file mode 100644 index 00000000000..4a6e2a7373b --- /dev/null +++ b/spec/frontend/environments/kubernetes_agent_info_spec.js @@ -0,0 +1,126 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; +import { TOKEN_STATUS_ACTIVE } from '~/clusters/agents/constants'; +import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql'; + +Vue.use(VueApollo); + +const propsData = { + agentName: 'my-agent', + agentId: '1', + agentProjectPath: 'path/to/agent-config-project', +}; + +const mockClusterAgent = { + id: '1', + name: 'token-1', + webPath: 'path/to/agent-page', +}; + +const connectedTimeNow = new Date(); +const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME); + +describe('~/environments/components/kubernetes_agent_info.vue', () => { + let wrapper; + let agentQueryResponse; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAgentLink = () => wrapper.findComponent(GlLink); + const findAgentStatus = () => wrapper.findByTestId('agent-status'); + const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon); + const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date'); + const findAlert = () => wrapper.findComponent(GlAlert); + + const createWrapper = ({ tokens = [], queryResponse = null } = {}) => { + const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } }; + + agentQueryResponse = + queryResponse || + jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } }); + const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]); + + wrapper = extendedWrapper( + shallowMount(KubernetesAgentInfo, { + apolloProvider, + propsData, + stubs: { TimeAgoTooltip, GlSprintf }, + }), + ); + }; + + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); + + it('shows loading icon while fetching the agent details', async () => { + expect(findLoadingIcon().exists()).toBe(true); + await waitForPromises(); + expect(findLoadingIcon().exists()).toBe(false); + }); + + it('sends expected params', async () => { + await waitForPromises(); + + const variables = { + agentName: propsData.agentName, + projectPath: propsData.agentProjectPath, + tokenStatus: TOKEN_STATUS_ACTIVE, + }; + + expect(agentQueryResponse).toHaveBeenCalledWith(variables); + }); + + it('renders the agent name with the link', async () => { + await waitForPromises(); + + expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath); + expect(findAgentLink().text()).toContain(mockClusterAgent.id); + }); + }); + + describe.each` + lastUsedAt | status | lastUsedText + ${null} | ${'unused'} | ${KubernetesAgentInfo.i18n.neverConnectedText} + ${connectedTimeNow} | ${'active'} | ${'just now'} + ${connectedTimeInactive} | ${'inactive'} | ${'8 minutes ago'} + `('when agent connection status is "$status"', ({ lastUsedAt, status, lastUsedText }) => { + beforeEach(async () => { + const tokens = [{ id: 'token-id', lastUsedAt }]; + createWrapper({ tokens }); + await waitForPromises(); + }); + + it('displays correct status text', () => { + expect(findAgentStatus().text()).toBe(AGENT_STATUSES[status].name); + }); + + it('displays correct status icon', () => { + expect(findAgentStatusIcon().props('name')).toBe(AGENT_STATUSES[status].icon); + expect(findAgentStatusIcon().attributes('class')).toBe(AGENT_STATUSES[status].class); + }); + + it('displays correct last used date status', () => { + expect(findAgentLastUsedDate().text()).toBe(lastUsedText); + }); + }); + + describe('when the agent query has errored', () => { + beforeEach(() => { + createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() }); + return waitForPromises(); + }); + + it('displays an alert message', () => { + expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError); + }); + }); +}); diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js new file mode 100644 index 00000000000..8673c657760 --- /dev/null +++ b/spec/frontend/environments/kubernetes_overview_spec.js @@ -0,0 +1,84 @@ +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlCollapse, GlButton } from '@gitlab/ui'; +import KubernetesOverview from '~/environments/components/kubernetes_overview.vue'; +import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; + +const agent = { + project: 'agent-project', + id: '1', + name: 'agent-name', +}; + +const propsData = { + agentId: agent.id, + agentName: agent.name, + agentProjectPath: agent.project, +}; + +describe('~/environments/components/kubernetes_overview.vue', () => { + let wrapper; + + const findCollapse = () => wrapper.findComponent(GlCollapse); + const findCollapseButton = () => wrapper.findComponent(GlButton); + const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo); + + const createWrapper = () => { + wrapper = shallowMount(KubernetesOverview, { + propsData, + }); + }; + + const toggleCollapse = async () => { + findCollapseButton().vm.$emit('click'); + await nextTick(); + }; + + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders the kubernetes overview title', () => { + expect(wrapper.text()).toBe(KubernetesOverview.i18n.sectionTitle); + }); + }); + + describe('collapse', () => { + beforeEach(() => { + createWrapper(); + }); + + it('is collapsed by default', () => { + expect(findCollapse().props('visible')).toBeUndefined(); + expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.expand); + expect(findCollapseButton().props('icon')).toBe('chevron-right'); + }); + + it("doesn't render components when the collapse is not visible", () => { + expect(findAgentInfo().exists()).toBe(false); + }); + + it('opens on click', async () => { + findCollapseButton().vm.$emit('click'); + await nextTick(); + + expect(findCollapse().attributes('visible')).toBe('true'); + expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.collapse); + expect(findCollapseButton().props('icon')).toBe('chevron-down'); + }); + }); + + describe('when section is expanded', () => { + it('renders kubernetes agent info', async () => { + createWrapper(); + await toggleCollapse(); + + expect(findAgentInfo().props()).toEqual({ + agentName: agent.name, + agentId: agent.id, + agentProjectPath: agent.project, + }); + }); + }); +}); diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js index 76cd09cfb4e..c04ff896794 100644 --- a/spec/frontend/environments/new_environment_item_spec.js +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -9,7 +9,8 @@ import { __, s__, sprintf } from '~/locale'; import EnvironmentItem from '~/environments/components/new_environment_item.vue'; import Deployment from '~/environments/components/deployment.vue'; import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue'; -import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data'; +import KubernetesOverview from '~/environments/components/kubernetes_overview.vue'; +import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data'; Vue.use(VueApollo); @@ -20,15 +21,16 @@ describe('~/environments/components/new_environment_item.vue', () => { return createMockApollo(); }; - const createWrapper = ({ propsData = {}, apolloProvider } = {}) => + const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) => mountExtended(EnvironmentItem, { apolloProvider, propsData: { environment: resolvedEnvironment, ...propsData }, - provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' }, + provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1', ...provideData }, stubs: { transition: stubTransition() }, }); const findDeployment = () => wrapper.findComponent(Deployment); + const findKubernetesOverview = () => wrapper.findComponent(KubernetesOverview); const expandCollapsedSection = async () => { const button = wrapper.findByRole('button', { name: __('Expand') }); @@ -37,10 +39,6 @@ describe('~/environments/components/new_environment_item.vue', () => { return button; }; - afterEach(() => { - wrapper?.destroy(); - }); - it('displays the name when not in a folder', () => { wrapper = createWrapper({ apolloProvider: createApolloProvider() }); @@ -157,7 +155,7 @@ describe('~/environments/components/new_environment_item.vue', () => { }); describe('stop', () => { - it('shows a buton to stop the environment if the environment is available', () => { + it('shows a button to stop the environment if the environment is available', () => { wrapper = createWrapper({ apolloProvider: createApolloProvider() }); const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') }); @@ -165,7 +163,7 @@ describe('~/environments/components/new_environment_item.vue', () => { expect(stop.exists()).toBe(true); }); - it('does not show a buton to stop the environment if the environment is stopped', () => { + it('does not show a button to stop the environment if the environment is stopped', () => { wrapper = createWrapper({ propsData: { environment: { ...resolvedEnvironment, canStop: false } }, apolloProvider: createApolloProvider(), @@ -515,4 +513,71 @@ describe('~/environments/components/new_environment_item.vue', () => { expect(deployBoard.exists()).toBe(false); }); }); + + describe('kubernetes overview', () => { + const environmentWithAgent = { + ...resolvedEnvironment, + agent, + }; + + it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => { + wrapper = createWrapper({ + propsData: { environment: environmentWithAgent }, + provideData: { + glFeatures: { + kasUserAccessProject: true, + }, + }, + apolloProvider: createApolloProvider(), + }); + + expandCollapsedSection(); + + expect(findKubernetesOverview().props()).toMatchObject({ + agentProjectPath: agent.project, + agentName: agent.name, + agentId: agent.id, + }); + }); + + it('should not render if the feature flag is not enabled', () => { + wrapper = createWrapper({ + propsData: { environment: environmentWithAgent }, + apolloProvider: createApolloProvider(), + }); + + expandCollapsedSection(); + + expect(findKubernetesOverview().exists()).toBe(false); + }); + + it('should not render if the environment has no agent object', () => { + wrapper = createWrapper({ + apolloProvider: createApolloProvider(), + }); + + expandCollapsedSection(); + + expect(findKubernetesOverview().exists()).toBe(false); + }); + + it('should not render if the environment has an agent object without agent id specified', () => { + const environment = { + ...resolvedEnvironment, + agent: { + project: agent.project, + name: agent.name, + }, + }; + + wrapper = createWrapper({ + propsData: { environment }, + apolloProvider: createApolloProvider(), + }); + + expandCollapsedSection(); + + expect(findKubernetesOverview().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js index a8cc05b297b..743f4ad6786 100644 --- a/spec/frontend/environments/new_environment_spec.js +++ b/spec/frontend/environments/new_environment_spec.js @@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import NewEnvironment from '~/environments/components/new_environment.vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; jest.mock('~/lib/utils/url_utility'); -jest.mock('~/flash'); +jest.mock('~/alert'); const DEFAULT_OPTS = { provide: { @@ -41,7 +41,6 @@ describe('~/environments/components/new.vue', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists(); diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js index a2ab4f707b5..ddf6670db12 100644 --- a/spec/frontend/environments/stop_stale_environments_modal_spec.js +++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js @@ -18,7 +18,6 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => { let wrapper; let mock; let before; - let originalGon; const createWrapper = (opts = {}) => shallowMount(StopStaleEnvironmentsModal, { @@ -28,8 +27,7 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => { }); beforeEach(() => { - originalGon = window.gon; - window.gon = { api_version: 'v4' }; + window.gon.api_version = 'v4'; mock = new MockAdapter(axios); jest.spyOn(axios, 'post'); @@ -39,9 +37,7 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); jest.resetAllMocks(); - window.gon = originalGon; }); it('sets the correct min and max dates', async () => { diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index 9d6e46be8c4..3bfade12d27 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -18,11 +18,11 @@ import { trackErrorDetailsViewsOptions, trackErrorStatusUpdateOptions, } from '~/error_tracking/utils'; -import { createAlert, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_WARNING } from '~/alert'; import { __ } from '~/locale'; import Tracking from '~/tracking'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(Vuex); @@ -148,7 +148,7 @@ describe('ErrorDetails', () => { expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled(); }); - it('when timeout is hit and no apollo result, stops loading and shows flash', async () => { + it('when timeout is hit and no apollo result, stops loading and shows alert', async () => { Date.now.mockReturnValue(endTime + 1); wrapper.vm.onNoApolloResult(); diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js index 3ec43010d80..44db4780ba9 100644 --- a/spec/frontend/error_tracking/store/actions_spec.js +++ b/spec/frontend/error_tracking/store/actions_spec.js @@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/error_tracking/store/actions'; import * as types from '~/error_tracking/store/mutation_types'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); let mock; diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js index 383d8aaeb20..0aeb8b19a9e 100644 --- a/spec/frontend/error_tracking/store/details/actions_spec.js +++ b/spec/frontend/error_tracking/store/details/actions_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/error_tracking/store/details/actions'; import * as types from '~/error_tracking/store/details/mutation_types'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, @@ -14,7 +14,7 @@ import Poll from '~/lib/utils/poll'; let mockedAdapter; let mockedRestart; -jest.mock('~/flash.js'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); describe('Sentry error details store actions', () => { @@ -48,7 +48,7 @@ describe('Sentry error details store actions', () => { ); }); - it('should show flash on API error', async () => { + it('should show alert on API error', async () => { mockedAdapter.onGet().reply(HTTP_STATUS_BAD_REQUEST); await testAction( diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js index 590983bd93d..24a26476455 100644 --- a/spec/frontend/error_tracking/store/list/actions_spec.js +++ b/spec/frontend/error_tracking/store/list/actions_spec.js @@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/error_tracking/store/list/actions'; import * as types from '~/error_tracking/store/list/mutation_types'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('error tracking actions', () => { let mock; @@ -38,7 +38,7 @@ describe('error tracking actions', () => { ); }); - it('should show flash on API error', async () => { + it('should show alert on API error', async () => { mock.onGet().reply(HTTP_STATUS_BAD_REQUEST); await testAction( diff --git a/spec/frontend/experimentation/components/gitlab_experiment_spec.js b/spec/frontend/experimentation/components/gitlab_experiment_spec.js index f52ebf0f3c4..73db4b9503c 100644 --- a/spec/frontend/experimentation/components/gitlab_experiment_spec.js +++ b/spec/frontend/experimentation/components/gitlab_experiment_spec.js @@ -9,7 +9,6 @@ const defaultSlots = { }; describe('ExperimentComponent', () => { - const oldGon = window.gon; let wrapper; const createComponent = (propsData = defaultProps, slots = defaultSlots) => { @@ -20,12 +19,6 @@ describe('ExperimentComponent', () => { window.gon = { experiment: { experiment_name: { variant: expectedVariant } } }; }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - window.gon = oldGon; - }); - describe('when variant and experiment is set', () => { it('renders control when it is the active variant', () => { mockVariant('control'); diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js index 0d663fd055e..6d9c9dfe65a 100644 --- a/spec/frontend/experimentation/utils_spec.js +++ b/spec/frontend/experimentation/utils_spec.js @@ -10,18 +10,15 @@ describe('experiment Utilities', () => { const ABC_KEY = 'abc'; const DEF_KEY = 'def'; - let origGon; let origGl; beforeEach(() => { - origGon = window.gon; origGl = window.gl; window.gon.experiment = {}; window.gl.experiments = {}; }); afterEach(() => { - window.gon = origGon; window.gl = origGl; }); 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 c1051a14a08..b06e0340991 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 @@ -42,7 +42,6 @@ describe('Configure Feature Flags Modal', () => { wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'danger'); describe('idle', () => { - afterEach(() => wrapper.destroy()); beforeEach(factory); it('should have Primary and Secondary actions', () => { @@ -51,7 +50,7 @@ describe('Configure Feature Flags Modal', () => { }); it('should default disable the primary action', () => { - const [{ disabled }] = findSecondaryAction().attributes; + const { disabled } = findSecondaryAction().attributes; expect(disabled).toBe(true); }); @@ -112,19 +111,17 @@ describe('Configure Feature Flags Modal', () => { }); describe('verified', () => { - afterEach(() => wrapper.destroy()); beforeEach(factory); it('should enable the secondary action', async () => { findProjectNameInput().vm.$emit('input', provide.projectName); await nextTick(); - const [{ disabled }] = findSecondaryAction().attributes; + const { disabled } = findSecondaryAction().attributes; expect(disabled).toBe(false); }); }); describe('cannot rotate token', () => { - afterEach(() => wrapper.destroy()); beforeEach(factory.bind(null, { canUserRotateToken: false })); it('should not display the primary action', () => { @@ -141,7 +138,6 @@ describe('Configure Feature Flags Modal', () => { }); describe('has rotate error', () => { - afterEach(() => wrapper.destroy()); beforeEach(() => { factory({ hasRotateError: true }); }); @@ -153,7 +149,6 @@ describe('Configure Feature Flags Modal', () => { }); describe('is rotating', () => { - afterEach(() => wrapper.destroy()); beforeEach(factory.bind(null, { isRotating: true })); it('should disable the project name input', async () => { diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js index cf4605e21ea..c26fd80865d 100644 --- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js +++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js @@ -53,7 +53,6 @@ describe('Edit feature flag form', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js index e3cc6f703c4..d983332f7c1 100644 --- a/spec/frontend/feature_flags/components/empty_state_spec.js +++ b/spec/frontend/feature_flags/components/empty_state_spec.js @@ -48,8 +48,6 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { if (wrapper?.destroy) { wrapper.destroy(); } - - wrapper = null; }); describe('alerts', () => { diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js index a4738fed37e..9fc0119a6c8 100644 --- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js @@ -27,7 +27,6 @@ describe('Feature flags > Environments dropdown', () => { const findDropdownMenu = () => wrapper.find('.dropdown-menu'); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js index e80f9c559c4..23e86d0eb2f 100644 --- a/spec/frontend/feature_flags/components/feature_flags_spec.js +++ b/spec/frontend/feature_flags/components/feature_flags_spec.js @@ -65,8 +65,6 @@ describe('Feature flags', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); - wrapper = null; }); describe('when limit exceeded', () => { diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js index 7dd7c709c94..f66e25698e6 100644 --- a/spec/frontend/feature_flags/components/form_spec.js +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -42,10 +42,6 @@ describe('feature flag form', () => { Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [] }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render provided submitText', () => { factory(requiredProps); 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 300d0e47082..46c9118cbd9 100644 --- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js +++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js @@ -46,10 +46,6 @@ describe('New feature flag form', () => { factory(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('with error', () => { it('should render the error', async () => { store.dispatch('receiveCreateFeatureFlagError', { message: ['The name is required'] }); 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 70a9156b5a9..5feaf094701 100644 --- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js @@ -24,8 +24,6 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { if (wrapper?.destroy) { wrapper.destroy(); } - - wrapper = null; }); describe('with valid percentage', () => { 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 23ad0d3a08d..365f1e534b5 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 @@ -28,8 +28,6 @@ describe('~/feature_flags/strategies/parameter_form_group.vue', () => { if (wrapper?.destroy) { wrapper.destroy(); } - - wrapper = null; }); it('should display the default 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 cb422a018f9..b20061c12a2 100644 --- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js @@ -22,8 +22,6 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { if (wrapper?.destroy) { wrapper.destroy(); } - - wrapper = null; }); describe('with valid percentage', () => { 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 0a72714c22a..ae489f3a6e6 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 @@ -22,8 +22,6 @@ describe('~/feature_flags/components/users_with_id.vue', () => { if (wrapper?.destroy) { wrapper.destroy(); } - - wrapper = null; }); it('should display the current value of the parameters', () => { diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js index d0f1f7d0e2a..cd8270f1801 100644 --- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js +++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js @@ -32,8 +32,6 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => { if (wrapper?.destroy) { wrapper.destroy(); } - - wrapper = null; }); describe.each` diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js index 4d5cb26810e..4609bfc23d7 100644 --- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js +++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js @@ -1,10 +1,10 @@ import MockAdapter from 'axios-mock-adapter'; import { dismiss } from '~/feature_highlight/feature_highlight_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('feature highlight helper', () => { describe('dismiss', () => { @@ -26,7 +26,7 @@ describe('feature highlight helper', () => { await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything()); }); - it('triggers flash when dismiss request fails', async () => { + it('triggers an alert when dismiss request fails', async () => { mockAxios .onPost(endpoint, { feature_name: highlightId }) .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js index 650f9eb1bbc..66ea22cece3 100644 --- a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js +++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js @@ -29,11 +29,6 @@ describe('feature_highlight/feature_highlight_popover', () => { buildWrapper(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders popover target', () => { expect(findPopoverTarget().exists()).toBe(true); }); diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js index ebed477fa2f..5f0e928e1fe 100644 --- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -22,11 +22,6 @@ describe('Recent Searches Dropdown Content', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when local storage is not available', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js index 26f12673f68..02ef813883f 100644 --- a/spec/frontend/filtered_search/dropdown_user_spec.js +++ b/spec/frontend/filtered_search/dropdown_user_spec.js @@ -68,10 +68,6 @@ describe('Dropdown User', () => { '/gitlab_directory/-/autocomplete/users.json', ); }); - - afterEach(() => { - window.gon = {}; - }); }); describe('hideCurrentUser', () => { diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js index 26af7af701b..8c16ff100eb 100644 --- a/spec/frontend/filtered_search/filtered_search_manager_spec.js +++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js @@ -8,11 +8,11 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes'; import { visitUrl, getParameterByName } from '~/lib/utils/url_utility'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), getParameterByName: jest.fn(), diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js index d3fa8fae9ab..138a4e183a9 100644 --- a/spec/frontend/filtered_search/visual_token_value_spec.js +++ b/spec/frontend/filtered_search/visual_token_value_spec.js @@ -5,11 +5,11 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper'; import { TEST_HOST } from 'helpers/test_constants'; import DropdownUtils from '~/filtered_search/dropdown_utils'; import VisualTokenValue from '~/filtered_search/visual_token_value'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Filtered Search Visual Tokens', () => { const findElements = (tokenElement) => { diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb index d8c8737b125..ad0fb9be8dc 100644 --- a/spec/frontend/fixtures/abuse_reports.rb +++ b/spec/frontend/fixtures/abuse_reports.rb @@ -14,6 +14,8 @@ RSpec.describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :co render_views before do + stub_feature_flags(abuse_reports_list: false) + sign_in(admin) enable_admin_mode!(admin) end diff --git a/spec/frontend/fixtures/api_deploy_keys.rb b/spec/frontend/fixtures/api_deploy_keys.rb index 5ffc726f086..8c926296817 100644 --- a/spec/frontend/fixtures/api_deploy_keys.rb +++ b/spec/frontend/fixtures/api_deploy_keys.rb @@ -7,6 +7,7 @@ RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do include JavaScriptFixturesHelpers let_it_be(:admin) { create(:admin) } + let_it_be(:path) { "/deploy_keys" } let_it_be(:project) { create(:project) } let_it_be(:project2) { create(:project) } let_it_be(:deploy_key) { create(:deploy_key, public: true) } @@ -17,8 +18,10 @@ RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do let_it_be(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key2) } let_it_be(:deploy_keys_project4) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key2) } + it_behaves_like 'GET request permissions for admin mode' + it 'api/deploy_keys/index.json' do - get api("/deploy_keys", admin) + get api("/deploy_keys", admin, admin_mode: true) expect(response).to be_successful end diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb index 6d452bf1bff..3583beb83c2 100644 --- a/spec/frontend/fixtures/jobs.rb +++ b/spec/frontend/fixtures/jobs.rb @@ -93,4 +93,26 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end end + + describe 'get_jobs_count.query.graphql', type: :request do + let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) } + let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) } + let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) } + + fixtures_path = 'graphql/jobs/' + get_jobs_count_query = 'get_jobs_count.query.graphql' + full_path = 'frontend-fixtures/builds-project' + + let_it_be(:query) do + get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_count_query}") + end + + it "#{fixtures_path}#{get_jobs_count_query}.json" do + post_graphql(query, current_user: user, variables: { + fullPath: full_path + }) + + expect_graphql_errors_to_be_empty + end + end end diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index 7ee89ca3694..b6f6d149756 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -151,7 +151,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: context 'merge request with no approvals' do base_input_path = 'vue_merge_request_widget/components/approvals/queries/' base_output_path = 'graphql/merge_requests/approvals/' - query_name = 'approved_by.query.graphql' + query_name = 'approvals.query.graphql' it "#{base_output_path}#{query_name}_no_approvals.json" do query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?) @@ -165,7 +165,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: context 'merge request approved by current user' do base_input_path = 'vue_merge_request_widget/components/approvals/queries/' base_output_path = 'graphql/merge_requests/approvals/' - query_name = 'approved_by.query.graphql' + query_name = 'approvals.query.graphql' it "#{base_output_path}#{query_name}.json" do merge_request.approved_by_users << user @@ -181,7 +181,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: context 'merge request approved by multiple users' do base_input_path = 'vue_merge_request_widget/components/approvals/queries/' base_output_path = 'graphql/merge_requests/approvals/' - query_name = 'approved_by.query.graphql' + query_name = 'approvals.query.graphql' it "#{base_output_path}#{query_name}_multiple_users.json" do merge_request.approved_by_users << user diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb index f60e4991292..1581bc58289 100644 --- a/spec/frontend/fixtures/runner.rb +++ b/spec/frontend/fixtures/runner.rb @@ -145,6 +145,40 @@ RSpec.describe 'Runner (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end end + + describe 'runner_for_registration.query.graphql', :freeze_time, type: :request do + runner_for_registration_query = 'register/runner_for_registration.query.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{runner_for_registration_query}") + end + + it "#{fixtures_path}#{runner_for_registration_query}.json" do + post_graphql(query, current_user: admin, variables: { + id: runner.to_global_id.to_s + }) + + expect_graphql_errors_to_be_empty + end + end + + describe 'runner_create.mutation.graphql', type: :request do + runner_create_mutation = 'new/runner_create.mutation.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{runner_create_mutation}") + end + + it "#{fixtures_path}#{runner_create_mutation}.json" do + post_graphql(query, current_user: admin, variables: { + input: { + description: 'My dummy runner' + } + }) + + expect_graphql_errors_to_be_empty + end + end end describe 'as group owner', GraphQL::Query do diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb index c80ba06bca1..613e4a1b447 100644 --- a/spec/frontend/fixtures/saved_replies.rb +++ b/spec/frontend/fixtures/saved_replies.rb @@ -43,4 +43,32 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d expect_graphql_errors_to_be_empty end end + + context 'when user creates saved reply' do + base_input_path = 'saved_replies/queries/' + base_output_path = 'graphql/saved_replies/' + query_name = 'create_saved_reply.mutation.graphql' + + it "#{base_output_path}#{query_name}.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: current_user, variables: { name: "Test", content: "Test content" }) + + expect_graphql_errors_to_be_empty + end + end + + context 'when user creates saved reply and it errors' do + base_input_path = 'saved_replies/queries/' + base_output_path = 'graphql/saved_replies/' + query_name = 'create_saved_reply.mutation.graphql' + + it "#{base_output_path}create_saved_reply_with_errors.mutation.graphql.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: current_user, variables: { name: nil, content: nil }) + + expect(flattened_errors).not_to be_empty + end + end end diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb index bd2d63a1827..18a4aa58c00 100644 --- a/spec/frontend/fixtures/startup_css.rb +++ b/spec/frontend/fixtures/startup_css.rb @@ -16,7 +16,6 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do before do # We want vNext badge to be included and com/canary don't remove/hide any other elements. # This is why we're turning com and canary on by default for now. - allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:canary?).and_return(true) sign_in(user) end @@ -55,13 +54,28 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do expect(response).to be_successful end + + # This Feature Flag is off by default + # This ensures that the correct css is generated for super sidebar + # When the feature flag is off, the general startup will capture it + it "startup_css/project-#{type}-super-sidebar.html" do + stub_feature_flags(super_sidebar_nav: true) + user.update!(use_new_navigation: true) + + get :show, params: { + namespace_id: project.namespace.to_param, + id: project + } + + expect(response).to be_successful + end end - describe ProjectsController, '(Startup CSS fixtures)', type: :controller do + describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do it_behaves_like 'startup css project fixtures', 'general' end - describe ProjectsController, '(Startup CSS fixtures)', type: :controller do + describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do before do user.update!(theme_id: 11) end diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb deleted file mode 100644 index 96820c9ae80..00000000000 --- a/spec/frontend/fixtures/u2f.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.context 'U2F' do - include JavaScriptFixturesHelpers - - let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') } - - before do - stub_feature_flags(webauthn: false) - end - - describe SessionsController, '(JavaScript fixtures)', type: :controller do - include DeviseHelpers - - render_views - - before do - set_devise_mapping(context: @request) - end - - it 'u2f/authenticate.html' do - allow(controller).to receive(:find_user).and_return(user) - - post :create, params: { user: { login: user.username, password: user.password } } - - expect(response).to be_successful - end - end - - describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do - render_views - - before do - sign_in(user) - allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance| - allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') - end - end - - it 'u2f/register.html' do - get :show - - expect(response).to be_successful - end - end -end diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb new file mode 100644 index 00000000000..6271aa87b9a --- /dev/null +++ b/spec/frontend/fixtures/users.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Users (GraphQL fixtures)', feature_category: :user_profile do + describe GraphQL::Query, type: :request do + include ApiHelpers + include GraphqlHelpers + include JavaScriptFixturesHelpers + + let_it_be(:user) { create(:user) } + + context 'for user achievements' do + let_it_be(:group) { create(:group, :public) } + let_it_be(:achievement1) { create(:achievement, namespace: group) } + let_it_be(:achievement2) { create(:achievement, namespace: group) } + let_it_be(:achievement3) { create(:achievement, namespace: group) } + let_it_be(:achievement_with_avatar_and_description) do + create(:achievement, + namespace: group, + description: 'Description', + avatar: File.new(Rails.root.join('db/fixtures/development/rocket.jpg'), 'r')) + end + + let(:user_achievements_query_path) { 'profile/components/graphql/get_user_achievements.query.graphql' } + let(:query) { get_graphql_query_as_string(user_achievements_query_path) } + + before_all do + group.add_guest(user) + end + + it "graphql/get_user_achievements_empty_response.json" do + post_graphql(query, current_user: user, variables: { id: user.to_global_id }) + + expect_graphql_errors_to_be_empty + end + + it "graphql/get_user_achievements_with_avatar_and_description_response.json" do + create(:user_achievement, user: user, achievement: achievement_with_avatar_and_description) + + post_graphql(query, current_user: user, variables: { id: user.to_global_id }) + + expect_graphql_errors_to_be_empty + end + + it "graphql/get_user_achievements_without_avatar_or_description_response.json" do + create(:user_achievement, user: user, achievement: achievement1) + + post_graphql(query, current_user: user, variables: { id: user.to_global_id }) + + expect_graphql_errors_to_be_empty + end + + it "graphql/get_user_achievements_long_response.json" do + [achievement1, achievement2, achievement3, achievement_with_avatar_and_description].each do |achievement| + create(:user_achievement, user: user, achievement: achievement) + end + + post_graphql(query, current_user: user, variables: { id: user.to_global_id }) + + expect_graphql_errors_to_be_empty + end + end + end +end diff --git a/spec/frontend/fixtures/webauthn.rb b/spec/frontend/fixtures/webauthn.rb index c6e9b41b584..ed6180118f0 100644 --- a/spec/frontend/fixtures/webauthn.rb +++ b/spec/frontend/fixtures/webauthn.rb @@ -32,6 +32,7 @@ RSpec.context 'WebAuthn' do allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance| allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') end + stub_feature_flags(webauthn_without_totp: false) end it 'webauthn/register.html' do diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js index e1890555de0..4f5788dcb77 100644 --- a/spec/frontend/frequent_items/components/app_spec.js +++ b/spec/frontend/frequent_items/components/app_spec.js @@ -69,7 +69,6 @@ describe('Frequent Items App Component', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('default', () => { diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js index c54a2a1d039..7c8592fdf0c 100644 --- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js @@ -59,8 +59,6 @@ describe('FrequentItemsListItemComponent', () => { afterEach(() => { unmockTracking(); - wrapper.destroy(); - wrapper = null; }); describe('computed', () => { diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js index d024925f62b..87f8e131b77 100644 --- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js @@ -29,10 +29,6 @@ describe('FrequentItemsListComponent', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('isListEmpty', () => { it('should return `true` or `false` representing whether if `items` is empty or not with projects', async () => { diff --git a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js index 685b5144a95..b1adc3f794a 100644 --- a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js +++ b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js @@ -50,10 +50,6 @@ describe('PagesPipelineWizard', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('shows the pipeline wizard', () => { expect(findPipelineWizardWrapper().exists()).toBe(true); }); diff --git a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js index 949bcf71ff5..e87f7e950cd 100644 --- a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js +++ b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js @@ -25,7 +25,6 @@ describe('GitlabVersionCheckBadge', () => { afterEach(() => { unmockTracking(); - wrapper.destroy(); }); const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper'); diff --git a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js index 4809ea37045..a0c988830ed 100644 --- a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js +++ b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js @@ -15,10 +15,6 @@ describe('google_cloud/components/google_cloud_menu', () => { wrapper = mountExtended(GoogleCloudMenu, { propsData: props }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains active configuration link', () => { const link = wrapper.findByTestId('configurationLink'); expect(link.text()).toBe(GoogleCloudMenu.i18n.configuration.title); diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js index 09a4d92dca2..92bc39bdff9 100644 --- a/spec/frontend/google_cloud/components/incubation_banner_spec.js +++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js @@ -15,10 +15,6 @@ describe('google_cloud/components/incubation_banner', () => { wrapper = mount(IncubationBanner); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains alert', () => { expect(findAlert().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/components/revoke_oauth_spec.js b/spec/frontend/google_cloud/components/revoke_oauth_spec.js index faaec07fc35..2b39bb9ca74 100644 --- a/spec/frontend/google_cloud/components/revoke_oauth_spec.js +++ b/spec/frontend/google_cloud/components/revoke_oauth_spec.js @@ -20,10 +20,6 @@ describe('google_cloud/components/revoke_oauth', () => { wrapper = shallowMount(RevokeOauth, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains title', () => { const title = findTitle(); expect(title.text()).toContain('Revoke authorizations'); diff --git a/spec/frontend/google_cloud/configuration/panel_spec.js b/spec/frontend/google_cloud/configuration/panel_spec.js index 79eb4cb4918..dd85b4c90a7 100644 --- a/spec/frontend/google_cloud/configuration/panel_spec.js +++ b/spec/frontend/google_cloud/configuration/panel_spec.js @@ -25,10 +25,6 @@ describe('google_cloud/configuration/panel', () => { wrapper = shallowMountExtended(Panel, { propsData: props }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains incubation banner', () => { const target = wrapper.findComponent(IncubationBanner); expect(target.exists()).toBe(true); diff --git a/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js index 48e4b0ca1ad..6e2d3147a54 100644 --- a/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js +++ b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js @@ -25,10 +25,6 @@ describe('google_cloud/databases/cloudsql/create_instance_form', () => { wrapper = shallowMountExtended(InstanceForm, { propsData, stubs: { GlFormCheckbox } }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains header', () => { expect(findHeader().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js index a5736d0a524..a2ee75f9fbf 100644 --- a/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js +++ b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js @@ -8,10 +8,6 @@ describe('google_cloud/databases/cloudsql/instance_table', () => { const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findTable = () => wrapper.findComponent(GlTable); - afterEach(() => { - wrapper.destroy(); - }); - describe('when there are no instances', () => { beforeEach(() => { const propsData = { diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js index e6a0d74f348..779258bbdbb 100644 --- a/spec/frontend/google_cloud/databases/panel_spec.js +++ b/spec/frontend/google_cloud/databases/panel_spec.js @@ -23,10 +23,6 @@ describe('google_cloud/databases/panel', () => { wrapper = shallowMountExtended(Panel, { propsData: props }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains incubation banner', () => { const target = wrapper.findComponent(IncubationBanner); expect(target.exists()).toBe(true); diff --git a/spec/frontend/google_cloud/databases/service_table_spec.js b/spec/frontend/google_cloud/databases/service_table_spec.js index 4a622e544e1..4594e1758ad 100644 --- a/spec/frontend/google_cloud/databases/service_table_spec.js +++ b/spec/frontend/google_cloud/databases/service_table_spec.js @@ -19,10 +19,6 @@ describe('google_cloud/databases/service_table', () => { wrapper = mountExtended(ServiceTable, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should contain a table', () => { expect(findTable().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/deployments/panel_spec.js b/spec/frontend/google_cloud/deployments/panel_spec.js index 729db1707a7..0748d8f9377 100644 --- a/spec/frontend/google_cloud/deployments/panel_spec.js +++ b/spec/frontend/google_cloud/deployments/panel_spec.js @@ -19,10 +19,6 @@ describe('google_cloud/deployments/panel', () => { wrapper = shallowMountExtended(Panel, { propsData: props }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains incubation banner', () => { const target = wrapper.findComponent(IncubationBanner); expect(target.exists()).toBe(true); diff --git a/spec/frontend/google_cloud/deployments/service_table_spec.js b/spec/frontend/google_cloud/deployments/service_table_spec.js index 8faad64e313..49220a6007e 100644 --- a/spec/frontend/google_cloud/deployments/service_table_spec.js +++ b/spec/frontend/google_cloud/deployments/service_table_spec.js @@ -18,10 +18,6 @@ describe('google_cloud/deployments/service_table', () => { wrapper = mount(DeploymentsServiceTable, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should contain a table', () => { expect(findTable().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/gcp_regions/form_spec.js b/spec/frontend/google_cloud/gcp_regions/form_spec.js index 1030e9c8a18..be37ff092f0 100644 --- a/spec/frontend/google_cloud/gcp_regions/form_spec.js +++ b/spec/frontend/google_cloud/gcp_regions/form_spec.js @@ -16,10 +16,6 @@ describe('google_cloud/gcp_regions/form', () => { wrapper = shallowMount(GcpRegionsForm, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains header', () => { expect(findHeader().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/gcp_regions/list_spec.js b/spec/frontend/google_cloud/gcp_regions/list_spec.js index 6d8c389e5a1..74a54b93183 100644 --- a/spec/frontend/google_cloud/gcp_regions/list_spec.js +++ b/spec/frontend/google_cloud/gcp_regions/list_spec.js @@ -18,10 +18,6 @@ describe('google_cloud/gcp_regions/list', () => { wrapper = mount(GcpRegionsList, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('shows the empty state component', () => { expect(findEmptyState().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/service_accounts/form_spec.js b/spec/frontend/google_cloud/service_accounts/form_spec.js index 8be481774fa..c86c8876b15 100644 --- a/spec/frontend/google_cloud/service_accounts/form_spec.js +++ b/spec/frontend/google_cloud/service_accounts/form_spec.js @@ -17,10 +17,6 @@ describe('google_cloud/service_accounts/form', () => { wrapper = shallowMount(ServiceAccountsForm, { propsData, stubs: { GlFormCheckbox } }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains header', () => { expect(findHeader().exists()).toBe(true); }); diff --git a/spec/frontend/google_cloud/service_accounts/list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js index c2bd2005b5d..ae5776081d7 100644 --- a/spec/frontend/google_cloud/service_accounts/list_spec.js +++ b/spec/frontend/google_cloud/service_accounts/list_spec.js @@ -18,10 +18,6 @@ describe('google_cloud/service_accounts/list', () => { wrapper = mount(ServiceAccountsList, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('shows the empty state component', () => { expect(findEmptyState().exists()).toBe(true); }); diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js index 021a3aa41ed..9cb27670c98 100644 --- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js +++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js @@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue'; import { createStore } from '~/grafana_integration/store'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; jest.mock('~/lib/utils/url_utility'); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('grafana integration component', () => { let wrapper; @@ -103,7 +103,7 @@ describe('grafana integration component', () => { expect(refreshCurrentPage).toHaveBeenCalled(); }); - it('creates flash banner on error', async () => { + it('creates alert banner on error', async () => { const message = 'mockErrorMessage'; axios.patch.mockRejectedValue({ response: { data: { message } } }); 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 85475c749b0..e92493315f7 100644 --- a/spec/frontend/group_settings/components/shared_runners_form_spec.js +++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js @@ -45,9 +45,6 @@ describe('group_settings/components/shared_runners_form', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; - updateGroup.mockReset(); }); diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index 4e6ddd89a55..98868de8475 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -3,7 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import appComponent from '~/groups/components/app.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; @@ -34,7 +34,7 @@ import { const $toast = { show: jest.fn(), }; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('AppComponent', () => { let wrapper; @@ -65,11 +65,6 @@ describe('AppComponent', () => { vm = wrapper.vm; }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - beforeEach(async () => { mock = new AxiosMockAdapter(axios); mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockGroups); @@ -117,7 +112,7 @@ describe('AppComponent', () => { }); }); - it('should show flash error when request fails', () => { + it('should show alert error when request fails', () => { mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST); jest.spyOn(window, 'scrollTo').mockImplementation(() => {}); @@ -325,7 +320,7 @@ describe('AppComponent', () => { }); }); - it('should show error flash message if request failed to leave group', () => { + it('should show error alert message if request failed to leave group', () => { const message = 'An error occurred. Please try again.'; jest .spyOn(vm.service, 'leaveGroup') @@ -342,7 +337,7 @@ describe('AppComponent', () => { }); }); - it('should show appropriate error flash message if request forbids to leave group', () => { + it('should show appropriate error alert message if request forbids to leave group', () => { const message = 'Failed to leave the group. Please make sure you are not the only owner.'; jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: HTTP_STATUS_FORBIDDEN }); jest.spyOn(vm.store, 'removeGroup'); diff --git a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js index 75edc602fbf..dc4271b98ee 100644 --- a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js +++ b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js @@ -24,10 +24,6 @@ const createComponent = ({ provide = {} } = {}) => { }); }; -afterEach(() => { - wrapper.destroy(); -}); - const findNewSubgroupLink = () => wrapper.findByRole('link', { name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title), diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js index f223333360d..da31fb02f69 100644 --- a/spec/frontend/groups/components/group_folder_spec.js +++ b/spec/frontend/groups/components/group_folder_spec.js @@ -20,10 +20,6 @@ describe('GroupFolder component', () => { }, }); - afterEach(() => { - wrapper.destroy(); - }); - it('does not render more children stats link when children count of group is under limit', () => { wrapper = createComponent(); diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 4570aa33a6c..663dd341a58 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -37,10 +37,6 @@ describe('GroupItemComponent', () => { return waitForPromises(); }); - afterEach(() => { - wrapper.destroy(); - }); - const withMicrodata = (group) => ({ ...group, microdata: getGroupItemMicrodata(group), 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 9965b608f27..0a18e657c94 100644 --- a/spec/frontend/groups/components/group_name_and_path_spec.js +++ b/spec/frontend/groups/components/group_name_and_path_spec.js @@ -7,11 +7,11 @@ 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 { createAlert } from '~/alert'; 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('~/alert'); jest.mock('~/rest_api', () => ({ getGroupPathAvailability: jest.fn(), })); diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index cae29a8f15a..9ee785d688a 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -37,10 +37,6 @@ describe('GroupsComponent', () => { Vue.component('GroupItem', GroupItemComponent); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('methods', () => { describe('change', () => { it('should emit `fetchPage` event when page is changed via pagination', () => { diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js index 4a385cb00ee..c4bc35dcd57 100644 --- a/spec/frontend/groups/components/invite_members_banner_spec.js +++ b/spec/frontend/groups/components/invite_members_banner_spec.js @@ -42,8 +42,6 @@ describe('InviteMembersBanner', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mockAxios.restore(); unmockTracking(); }); @@ -59,7 +57,6 @@ describe('InviteMembersBanner', () => { }); const trackCategory = undefined; - const buttonClickEvent = 'invite_members_banner_button_clicked'; it('sends the displayEvent when the banner is displayed', () => { const displayEvent = 'invite_members_banner_displayed'; @@ -80,12 +77,6 @@ describe('InviteMembersBanner', () => { source: 'invite_members_banner', }); }); - - it('sends the buttonClickEvent with correct trackCategory and trackLabel', () => { - expect(trackingSpy).toHaveBeenCalledWith(trackCategory, buttonClickEvent, { - label: provide.trackLabel, - }); - }); }); it('sends the dismissEvent when the banner is dismissed', () => { diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js index 3ceb038dd3c..fac6fb77709 100644 --- a/spec/frontend/groups/components/item_actions_spec.js +++ b/spec/frontend/groups/components/item_actions_spec.js @@ -18,11 +18,6 @@ describe('ItemActions', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findEditGroupBtn = () => wrapper.findByTestId(`edit-group-${mockParentGroupItem.id}-btn`); const findLeaveGroupBtn = () => wrapper.findByTestId(`leave-group-${mockParentGroupItem.id}-btn`); const findRemoveGroupBtn = () => diff --git a/spec/frontend/groups/components/new_top_level_group_alert_spec.js b/spec/frontend/groups/components/new_top_level_group_alert_spec.js index db9a5c7b16b..060663747e4 100644 --- a/spec/frontend/groups/components/new_top_level_group_alert_spec.js +++ b/spec/frontend/groups/components/new_top_level_group_alert_spec.js @@ -30,10 +30,6 @@ describe('NewTopLevelGroupAlert', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when the component is created', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js index d1ae2c4be17..906609c97f9 100644 --- a/spec/frontend/groups/components/overview_tabs_spec.js +++ b/spec/frontend/groups/components/overview_tabs_spec.js @@ -76,7 +76,6 @@ describe('OverviewTabs', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js index 0065820f78f..fd0c3907e04 100644 --- a/spec/frontend/groups/components/transfer_group_form_spec.js +++ b/spec/frontend/groups/components/transfer_group_form_spec.js @@ -48,10 +48,6 @@ describe('Transfer group form', () => { const findTransferLocations = () => wrapper.findComponent(TransferLocations); const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js index 77c0966ba1e..86913bb4c09 100644 --- a/spec/frontend/groups_projects/components/transfer_locations_spec.js +++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js @@ -109,10 +109,6 @@ describe('TransferLocations', () => { const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - afterEach(() => { - wrapper.destroy(); - }); - describe('when `GlDropdown` is opened', () => { it('shows loading icon', async () => { getTransferLocations.mockReturnValueOnce(new Promise(() => {})); diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index d6263c663d2..8e84c672d90 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -80,10 +80,6 @@ describe('HeaderSearchApp', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form'); const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findScopeToken = () => wrapper.findComponent(GlToken); @@ -187,10 +183,10 @@ describe('HeaderSearchApp', () => { describe.each` username | showDropdown | expectedDesc - ${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown} - ${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown} - ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown} - ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown} + ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN} + ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN} + ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN} + ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN} `('Search Input Description', ({ username, showDropdown, expectedDesc }) => { describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => { beforeEach(() => { @@ -212,7 +208,7 @@ describe('HeaderSearchApp', () => { ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`} - ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING} `( 'Search Results Description', ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { @@ -354,8 +350,8 @@ describe('HeaderSearchApp', () => { describe('events', () => { beforeEach(() => { - createComponent(); window.gon.current_username = MOCK_USERNAME; + createComponent(); }); describe('Header Search Input', () => { @@ -463,8 +459,8 @@ describe('HeaderSearchApp', () => { ${2} | ${'test1'} `('currentFocusedOption', ({ MOCK_INDEX, search }) => { beforeEach(() => { - createComponent({ search }); window.gon.current_username = MOCK_USERNAME; + createComponent({ search }); findHeaderSearchInput().vm.$emit('click'); }); @@ -504,8 +500,8 @@ describe('HeaderSearchApp', () => { const MOCK_INDEX = 1; beforeEach(() => { - createComponent(); window.gon.current_username = MOCK_USERNAME; + createComponent(); findHeaderSearchInput().vm.$emit('click'); }); diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js index 7952661e2d2..e77a9231b7a 100644 --- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js +++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js @@ -3,15 +3,14 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue'; +import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '~/header_search/constants'; import { - GROUPS_CATEGORY, - LARGE_AVATAR_PX, PROJECTS_CATEGORY, - SMALL_AVATAR_PX, + GROUPS_CATEGORY, ISSUES_CATEGORY, MERGE_REQUEST_CATEGORY, RECENT_EPICS_CATEGORY, -} from '~/header_search/constants'; +} from '~/vue_shared/global_search/constants'; import { MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, MOCK_SORTED_AUTOCOMPLETE_OPTIONS, @@ -46,10 +45,6 @@ describe('HeaderSearchAutocompleteItems', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider); const findFirstDropdownItem = () => findDropdownItems().at(0); diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js index abcacc487df..3768862d83e 100644 --- a/spec/frontend/header_search/components/header_search_default_items_spec.js +++ b/spec/frontend/header_search/components/header_search_default_items_spec.js @@ -29,10 +29,6 @@ describe('HeaderSearchDefaultItems', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstDropdownItem = () => findDropdownItems().at(0); diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js index 2db9f71d702..51d67198f04 100644 --- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js +++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js @@ -5,7 +5,8 @@ import Vuex from 'vuex'; import { trimText } from 'helpers/text_helper'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; import { truncate } from '~/lib/utils/text_utility'; -import { MSG_IN_ALL_GITLAB, SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants'; +import { SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants'; +import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants'; import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS, @@ -38,10 +39,6 @@ describe('HeaderSearchScopedItems', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js index 3a8624ad9dd..2218c81efc3 100644 --- a/spec/frontend/header_search/mock_data.js +++ b/spec/frontend/header_search/mock_data.js @@ -1,16 +1,14 @@ +import { ICON_PROJECT, ICON_GROUP, ICON_SUBGROUP } from '~/header_search/constants'; import { + PROJECTS_CATEGORY, + GROUPS_CATEGORY, MSG_ISSUES_ASSIGNED_TO_ME, MSG_ISSUES_IVE_CREATED, MSG_MR_ASSIGNED_TO_ME, MSG_MR_IM_REVIEWER, MSG_MR_IVE_CREATED, MSG_IN_ALL_GITLAB, - PROJECTS_CATEGORY, - ICON_PROJECT, - GROUPS_CATEGORY, - ICON_GROUP, - ICON_SUBGROUP, -} from '~/header_search/constants'; +} from '~/vue_shared/global_search/constants'; export const MOCK_USERNAME = 'anyone'; diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js index bd93b0edadf..95a619ebeca 100644 --- a/spec/frontend/header_search/store/actions_spec.js +++ b/spec/frontend/header_search/store/actions_spec.js @@ -16,7 +16,7 @@ import { MOCK_ISSUE_PATH, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Header Search Store Actions', () => { let state; diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js index a1d9481b5cc..7a7a00178f1 100644 --- a/spec/frontend/header_search/store/getters_spec.js +++ b/spec/frontend/header_search/store/getters_spec.js @@ -241,6 +241,13 @@ describe('Header Search Store Getters', () => { MOCK_DEFAULT_SEARCH_OPTIONS, ); }); + + it('returns the correct array if issues path is false', () => { + mockGetters.scopedIssuesPath = undefined; + expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length), + ); + }); }); describe('scopedSearchOptions', () => { diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js index 05161437c22..28c742386cc 100644 --- a/spec/frontend/helpers/startup_css_helper_spec.js +++ b/spec/frontend/helpers/startup_css_helper_spec.js @@ -21,17 +21,10 @@ describe('waitForCSSLoaded', () => { }); describe('when gon features is not provided', () => { - let originalGon; - beforeEach(() => { - originalGon = window.gon; window.gon = null; }); - afterEach(() => { - window.gon = originalGon; - }); - it('should invoke the action right away', async () => { const events = waitForCSSLoaded(mockedCallback); await events; diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js index a97e883a8bf..ff04f9a84f1 100644 --- a/spec/frontend/ide/components/activity_bar_spec.js +++ b/spec/frontend/ide/components/activity_bar_spec.js @@ -22,10 +22,6 @@ describe('IDE ActivityBar component', () => { wrapper = shallowMount(ActivityBar, { store }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('updateActivityBarView', () => { beforeEach(() => { mountComponent(); diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js index 3dbd1210916..4cae146cbd2 100644 --- a/spec/frontend/ide/components/branches/item_spec.js +++ b/spec/frontend/ide/components/branches/item_spec.js @@ -34,10 +34,6 @@ describe('IDE branch item', () => { router = createRouter(store); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('if not active', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js index bbde45d700f..eeab26f7559 100644 --- a/spec/frontend/ide/components/branches/search_list_spec.js +++ b/spec/frontend/ide/components/branches/search_list_spec.js @@ -35,11 +35,6 @@ describe('IDE branches search list', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('calls fetch on mounted', () => { createComponent(); expect(fetchBranchesMock).toHaveBeenCalled(); diff --git a/spec/frontend/ide/components/cannot_push_code_alert_spec.js b/spec/frontend/ide/components/cannot_push_code_alert_spec.js index ff659ecdf3f..d4db2246008 100644 --- a/spec/frontend/ide/components/cannot_push_code_alert_spec.js +++ b/spec/frontend/ide/components/cannot_push_code_alert_spec.js @@ -10,10 +10,6 @@ const TEST_BUTTON_TEXT = 'Fork text'; describe('ide/components/cannot_push_code_alert', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - const createComponent = (props = {}) => { wrapper = shallowMount(CannotPushCodeAlert, { propsData: { diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js index dc103fec5d0..019469cbf87 100644 --- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js @@ -46,10 +46,6 @@ describe('IDE commit sidebar actions', () => { jest.spyOn(store, 'dispatch').mockImplementation(() => {}); }); - afterEach(() => { - wrapper.destroy(); - }); - const findText = () => wrapper.text(); const findRadios = () => wrapper.findAll('input[type="radio"]'); 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 f6d5833edee..ce43e648b43 100644 --- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js @@ -1,7 +1,9 @@ -import { mount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue'; +import { stubComponent } from 'helpers/stub_component'; import { createStore } from '~/ide/stores'; import { file } from '../../helpers'; @@ -12,9 +14,10 @@ const TEST_FILE_PATH = 'test/file/path'; describe('IDE commit editor header', () => { let wrapper; let store; + const showMock = jest.fn(); const createComponent = (fileProps = {}) => { - wrapper = mount(EditorHeader, { + wrapper = shallowMount(EditorHeader, { store, propsData: { activeFile: { @@ -23,22 +26,17 @@ describe('IDE commit editor header', () => { ...fileProps, }, }, + stubs: { + GlModal: stubComponent(GlModal, { + methods: { show: showMock }, + }), + }, }); }; const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' }); const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' }); - beforeEach(() => { - store = createStore(); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` fileProps | shouldExist ${{ staged: false, changed: false }} | ${false} @@ -52,20 +50,19 @@ describe('IDE commit editor header', () => { }); describe('discard button', () => { - beforeEach(() => { + it('opens a dialog confirming discard', () => { createComponent(); + findDiscardButton().vm.$emit('click'); - const modal = findDiscardModal(); - jest.spyOn(modal.vm, 'show'); - - findDiscardButton().trigger('click'); - }); - - it('opens a dialog confirming discard', () => { - expect(findDiscardModal().vm.show).toHaveBeenCalled(); + expect(showMock).toHaveBeenCalled(); }); it('calls discardFileChanges if dialog result is confirmed', () => { + store = createStore(); + jest.spyOn(store, 'dispatch').mockImplementation(); + + createComponent(); + expect(store.dispatch).not.toHaveBeenCalled(); findDiscardModal().vm.$emit('primary'); diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js index 7c48c0e6f95..4a6aafe42ae 100644 --- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js @@ -11,10 +11,6 @@ describe('IDE commit panel EmptyState component', () => { wrapper = shallowMount(EmptyState, { store }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders no changes text when last commit message is empty', () => { expect(wrapper.find('h4').text()).toBe('No changes'); }); diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index a8ee81afa0b..0c0998c037a 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -26,7 +26,7 @@ describe('IDE commit form', () => { wrapper = shallowMount(CommitForm, { store, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { GlModal: stubComponent(GlModal), @@ -73,10 +73,6 @@ describe('IDE commit form', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - // Notes: // - When there are no changes, there is no commit button so there's nothing to test :) describe.each` diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js index c9571d39acb..c2a33c0d71e 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js @@ -36,10 +36,6 @@ describe('Multi-file editor commit sidebar list item', () => { findPathEl = wrapper.find('.multi-file-commit-list-path'); }); - afterEach(() => { - wrapper.destroy(); - }); - const findPathText = () => trimText(findPathEl.text()); it('renders file path', () => { diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index 4406d14d990..6b9ba939a87 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -19,10 +19,6 @@ describe('Multi-file editor commit sidebar list', () => { }, }); - afterEach(() => { - wrapper.destroy(); - }); - describe('with a list of files', () => { beforeEach(async () => { const f = file('file name'); diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js index c2ef29c1059..3403a7b8ad9 100644 --- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js @@ -15,10 +15,6 @@ describe('IDE commit message field', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - const findMessage = () => wrapper.find('textarea'); const findHighlights = () => wrapper.findAll('.highlights span'); const findMarks = () => wrapper.findAll('mark'); diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js index 2a455c9d7c1..adc9a0f1421 100644 --- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js @@ -33,15 +33,11 @@ describe('NewMergeRequestOption component', () => { }, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when the `shouldHideNewMrOption` getter returns false', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js index a3fa03a4aa5..cdf14056523 100644 --- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js @@ -19,15 +19,11 @@ describe('IDE commit sidebar radio group', () => { propsData: config.props, slots: config.slots, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('without input', () => { const props = { value: '1', diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js index 63d51953915..d1a81dd1639 100644 --- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js @@ -12,10 +12,6 @@ describe('IDE commit panel successful commit state', () => { wrapper = shallowMount(SuccessMessage, { store }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders last commit message when it exists', () => { expect(wrapper.text()).toContain('testing commit message'); }); diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js index 204d39de741..5f6579654bc 100644 --- a/spec/frontend/ide/components/error_message_spec.js +++ b/spec/frontend/ide/components/error_message_spec.js @@ -32,11 +32,6 @@ describe('IDE error message component', () => { setErrorMessageMock.mockReset(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findDismissButton = () => wrapper.find('button[aria-label=Dismiss]'); const findActionButton = () => wrapper.find('button.gl-alert-action'); diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js index 281c549a1b4..f5a6e7222f9 100644 --- a/spec/frontend/ide/components/file_row_extra_spec.js +++ b/spec/frontend/ide/components/file_row_extra_spec.js @@ -37,8 +37,6 @@ describe('IDE extra file row component', () => { }; afterEach(() => { - wrapper.destroy(); - stagedFilesCount = 0; unstagedFilesCount = 0; changesCount = 0; diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js index 60f37260393..b8c850fdd13 100644 --- a/spec/frontend/ide/components/file_templates/bar_spec.js +++ b/spec/frontend/ide/components/file_templates/bar_spec.js @@ -21,10 +21,6 @@ describe('IDE file templates bar component', () => { wrapper = mount(Bar, { store }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template type dropdown', () => { it('renders dropdown component', () => { expect(wrapper.find('.dropdown').text()).toContain('Choose a type'); diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js index ee90d87357c..72fdd05eb2c 100644 --- a/spec/frontend/ide/components/file_templates/dropdown_spec.js +++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js @@ -49,11 +49,6 @@ describe('IDE file templates dropdown component', () => { ({ element } = wrapper); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('calls clickItem on click', async () => { const itemData = { name: 'test.yml ' }; createComponent({ props: { data: [itemData] } }); diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js index aa66224fa19..331877ff112 100644 --- a/spec/frontend/ide/components/ide_file_row_spec.js +++ b/spec/frontend/ide/components/ide_file_row_spec.js @@ -34,11 +34,6 @@ describe('Ide File Row component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findFileRowExtra = () => wrapper.findComponent(FileRowExtra); const findFileRow = () => wrapper.findComponent(FileRow); const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen'); diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js index d0636352a3f..7613f407e45 100644 --- a/spec/frontend/ide/components/ide_project_header_spec.js +++ b/spec/frontend/ide/components/ide_project_header_spec.js @@ -20,10 +20,6 @@ describe('IDE project header', () => { wrapper = shallowMount(IDEProjectHeader, { propsData: { project: mockProject } }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js index 0759f957374..e6fd018969f 100644 --- a/spec/frontend/ide/components/ide_review_spec.js +++ b/spec/frontend/ide/components/ide_review_spec.js @@ -30,10 +30,6 @@ describe('IDE review mode', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders list of files', () => { expect(wrapper.text()).toContain('fileName'); }); diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js index 4784d6c516f..c258c5312d8 100644 --- a/spec/frontend/ide/components/ide_side_bar_spec.js +++ b/spec/frontend/ide/components/ide_side_bar_spec.js @@ -29,11 +29,6 @@ describe('IdeSidebar', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders a sidebar', () => { wrapper = createComponent(); diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js index 80e8aba4072..4ee24f63f76 100644 --- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js +++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js @@ -25,10 +25,6 @@ describe('ide/components/ide_sidebar_nav', () => { let wrapper; const createComponent = (props = {}) => { - if (wrapper) { - throw new Error('wrapper already exists'); - } - wrapper = shallowMount(IdeSidebarNav, { propsData: { tabs: TEST_TABS, @@ -37,16 +33,11 @@ describe('ide/components/ide_sidebar_nav', () => { ...props, }, directives: { - tooltip: createMockDirective(), + tooltip: createMockDirective('tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findButtons = () => wrapper.findAll('li button'); const findButtonsData = () => findButtons().wrappers.map((button) => { diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index a575f428a69..1c8d570cdce 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -52,8 +52,6 @@ describe('WebIDE', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; window.onbeforeunload = null; }); diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js index e6e0ebaf1e8..0ee16f98e7e 100644 --- a/spec/frontend/ide/components/ide_status_bar_spec.js +++ b/spec/frontend/ide/components/ide_status_bar_spec.js @@ -34,10 +34,6 @@ describe('IdeStatusBar component', () => { wrapper = mount(IdeStatusBar, { store }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { it('triggers a setInterval', () => { mountComponent(); diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js index 0b54e8b6afb..344a1fbc4f6 100644 --- a/spec/frontend/ide/components/ide_status_list_spec.js +++ b/spec/frontend/ide/components/ide_status_list_spec.js @@ -53,10 +53,7 @@ describe('ide/components/ide_status_list', () => { }); afterEach(() => { - wrapper.destroy(); - store = null; - wrapper = null; }); describe('with regular file', () => { diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js index 0b9111c0e2a..3501ecce061 100644 --- a/spec/frontend/ide/components/ide_status_mr_spec.js +++ b/spec/frontend/ide/components/ide_status_mr_spec.js @@ -17,10 +17,6 @@ describe('ide/components/ide_status_mr', () => { const findIcon = () => wrapper.findComponent(GlIcon); const findLink = () => wrapper.findComponent(GlLink); - afterEach(() => { - wrapper.destroy(); - }); - describe('when mounted', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index 0f61aa80e53..427daa57324 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -25,10 +25,6 @@ describe('IdeTreeList component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('normal branch', () => { const tree = [file('fileName')]; diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js index f00017a2736..9f452910496 100644 --- a/spec/frontend/ide/components/ide_tree_spec.js +++ b/spec/frontend/ide/components/ide_tree_spec.js @@ -29,10 +29,6 @@ describe('IdeTree', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders list of files', () => { expect(wrapper.text()).toContain('fileName'); }); diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js index 629c4424314..2bb0f3fccf4 100644 --- a/spec/frontend/ide/components/jobs/detail/description_spec.js +++ b/spec/frontend/ide/components/jobs/detail/description_spec.js @@ -14,10 +14,6 @@ describe('IDE job description', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders job details', () => { expect(wrapper.text()).toContain('#1'); expect(wrapper.text()).toContain('test'); 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 5eb66f75978..eec1bd6b123 100644 --- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js +++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js @@ -15,10 +15,6 @@ describe('IDE job log scroll button', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe.each` direction | icon | title ${'up'} | ${'scroll_up'} | ${'Scroll to top'} diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js index bf2be3aa595..60e03a7b882 100644 --- a/spec/frontend/ide/components/jobs/detail_spec.js +++ b/spec/frontend/ide/components/jobs/detail_spec.js @@ -34,10 +34,6 @@ describe('IDE jobs detail view', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('mounted', () => { const findJobOutput = () => wrapper.find('.bash'); const findBuildLoaderAnimation = () => wrapper.find('.build-loader-animation'); diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js index 32e27333e42..ab442a27817 100644 --- a/spec/frontend/ide/components/jobs/item_spec.js +++ b/spec/frontend/ide/components/jobs/item_spec.js @@ -12,10 +12,6 @@ describe('IDE jobs item', () => { wrapper = mount(JobItem, { propsData: { job } }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders job details', () => { expect(wrapper.text()).toContain(job.name); expect(wrapper.text()).toContain(`#${job.id}`); diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js index 52fbff2f497..23ef92f9682 100644 --- a/spec/frontend/ide/components/jobs/stage_spec.js +++ b/spec/frontend/ide/components/jobs/stage_spec.js @@ -31,11 +31,6 @@ describe('IDE pipeline stage', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('emits fetch event when mounted', () => { createComponent(); expect(wrapper.emitted().fetch).toBeDefined(); diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js index d6cf8127b53..2fbb6919b8b 100644 --- a/spec/frontend/ide/components/merge_requests/item_spec.js +++ b/spec/frontend/ide/components/merge_requests/item_spec.js @@ -39,11 +39,6 @@ describe('IDE merge request item', () => { router = createRouter(store); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js index ea6e2741a85..3b0e8c632fb 100644 --- a/spec/frontend/ide/components/merge_requests/list_spec.js +++ b/spec/frontend/ide/components/merge_requests/list_spec.js @@ -48,11 +48,6 @@ describe('IDE merge requests list', () => { fetchMergeRequestsMock = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('calls fetch on mounted', () => { createComponent(); expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), { diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js index 8eebcdd9e08..3aae2c83e80 100644 --- a/spec/frontend/ide/components/nav_dropdown_button_spec.js +++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js @@ -9,10 +9,6 @@ describe('NavDropdownButton component', () => { const TEST_MR_ID = '12345'; let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - const createComponent = ({ props = {}, state = {} } = {}) => { const store = createStore(); store.replaceState(state); diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js index 33e638843f5..794aaba6d01 100644 --- a/spec/frontend/ide/components/nav_dropdown_spec.js +++ b/spec/frontend/ide/components/nav_dropdown_spec.js @@ -30,10 +30,6 @@ describe('IDE NavDropdown', () => { jest.spyOn(store, 'dispatch').mockImplementation(() => {}); }); - afterEach(() => { - wrapper.destroy(); - }); - const createComponent = () => { wrapper = mount(NavDropdown, { store, diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js index a9cfdfd20c1..bfd5cdf7263 100644 --- a/spec/frontend/ide/components/new_dropdown/button_spec.js +++ b/spec/frontend/ide/components/new_dropdown/button_spec.js @@ -14,10 +14,6 @@ describe('IDE new entry dropdown button component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders button with label', () => { createComponent(); diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js index 747c099db33..01dcb174c41 100644 --- a/spec/frontend/ide/components/new_dropdown/index_spec.js +++ b/spec/frontend/ide/components/new_dropdown/index_spec.js @@ -30,10 +30,6 @@ describe('new dropdown component', () => { jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {}); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders new file, upload and new directory links', () => { expect(findAllButtons().at(0).text()).toBe('New file'); expect(findAllButtons().at(1).text()).toBe('Upload file'); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index c6f9fd0c4ea..36c3d323e63 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -1,13 +1,13 @@ import { GlButton, GlModal } from '@gitlab/ui'; import { nextTick } from 'vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import Modal from '~/ide/components/new_dropdown/modal.vue'; import { createStore } from '~/ide/stores'; import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createEntriesFromPaths } from '../../helpers'; -jest.mock('~/flash'); +jest.mock('~/alert'); const NEW_NAME = 'babar'; @@ -79,7 +79,6 @@ describe('new file modal component', () => { afterEach(() => { store = null; - wrapper.destroy(); document.body.innerHTML = ''; }); @@ -94,11 +93,11 @@ describe('new file modal component', () => { it('renders modal', () => { expect(findGlModal().props()).toMatchObject({ actionCancel: { - attributes: [{ variant: 'default' }], + attributes: { variant: 'default' }, text: 'Cancel', }, actionPrimary: { - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, text: 'Create file', }, actionSecondary: null, @@ -170,7 +169,7 @@ describe('new file modal component', () => { expect(findGlModal().props()).toMatchObject({ title: modalTitle, actionPrimary: { - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, text: btnTitle, }, }); @@ -298,7 +297,7 @@ describe('new file modal component', () => { expect(findGlModal().props()).toMatchObject({ title, actionPrimary: { - attributes: [{ variant: 'confirm' }], + attributes: { variant: 'confirm' }, text: title, }, }); @@ -340,7 +339,7 @@ describe('new file modal component', () => { }); }); - it('does not trigger flash', () => { + it('does not trigger alert', () => { expect(createAlert).not.toHaveBeenCalled(); }); }); @@ -359,7 +358,7 @@ describe('new file modal component', () => { }); }); - it('does not trigger flash', () => { + it('does not trigger alert', () => { expect(createAlert).not.toHaveBeenCalled(); }); }); @@ -379,7 +378,7 @@ describe('new file modal component', () => { triggerSubmitModal(); }); - it('creates flash', () => { + it('creates alert', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'The name "src" is already taken in this directory.', fadeTransition: false, @@ -404,7 +403,7 @@ describe('new file modal component', () => { triggerSubmitModal(); }); - it('does not create flash', () => { + it('does not create alert', () => { expect(createAlert).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js index fc643589d51..40780c7f0bd 100644 --- a/spec/frontend/ide/components/new_dropdown/upload_spec.js +++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js @@ -12,10 +12,6 @@ describe('new dropdown upload', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('openFile', () => { it('calls for each file', () => { const files = ['test', 'test2', 'test3']; diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js index e92f843ae6e..42eb5b3fc7a 100644 --- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js +++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js @@ -35,11 +35,6 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => { jest.spyOn(store, 'dispatch').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with a tab', () => { let fakeView; let extensionTabs; diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js index 1d81c3ea89d..832983edf21 100644 --- a/spec/frontend/ide/components/panes/right_spec.js +++ b/spec/frontend/ide/components/panes/right_spec.js @@ -28,11 +28,6 @@ describe('ide/components/panes/right.vue', () => { store = createStore(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js index 31081e8f9d5..71de9aecb52 100644 --- a/spec/frontend/ide/components/pipelines/empty_state_spec.js +++ b/spec/frontend/ide/components/pipelines/empty_state_spec.js @@ -22,10 +22,6 @@ describe('~/ide/components/pipelines/empty_state.vue', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index d82b97561f0..e913fa84d56 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -65,11 +65,6 @@ describe('IDE pipelines list', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('fetches latest pipeline', () => { createComponent(); diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js index d3312358402..92bb645b1c0 100644 --- a/spec/frontend/ide/components/repo_commit_section_spec.js +++ b/spec/frontend/ide/components/repo_commit_section_spec.js @@ -63,11 +63,6 @@ describe('RepoCommitSection', () => { jest.spyOn(router, 'push').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('empty state', () => { beforeEach(() => { store.state.noChangesStateSvgPath = TEST_NO_CHANGES_SVG; diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index c9f033bffbb..9253bfc7e71 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -162,8 +162,6 @@ describe('RepoEditor', () => { // create a new model each time, otherwise tests conflict with each other // because of same model being used in multiple tests monacoEditor.getModels().forEach((model) => model.dispose()); - wrapper.destroy(); - wrapper = null; }); describe('default', () => { diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js index b26edc5a85b..b329baea783 100644 --- a/spec/frontend/ide/components/repo_tab_spec.js +++ b/spec/frontend/ide/components/repo_tab_spec.js @@ -37,11 +37,6 @@ describe('RepoTab', () => { jest.spyOn(router, 'push').mockImplementation(() => {}); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders a close link and a name link', () => { createComponent({ tab: file(), diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js index 1cfc1f12745..06ad162d398 100644 --- a/spec/frontend/ide/components/repo_tabs_spec.js +++ b/spec/frontend/ide/components/repo_tabs_spec.js @@ -25,10 +25,6 @@ describe('RepoTabs', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a list of tabs', async () => { store.state.openFiles[0].active = true; diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js index fe2a128c9c8..240e675a38e 100644 --- a/spec/frontend/ide/components/resizable_panel_spec.js +++ b/spec/frontend/ide/components/resizable_panel_spec.js @@ -19,11 +19,6 @@ describe('~/ide/components/resizable_panel', () => { jest.spyOn(store, 'dispatch').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const createComponent = (props = {}) => { wrapper = shallowMount(ResizablePanel, { propsData: { 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 94da06f4cb2..186b1997497 100644 --- a/spec/frontend/ide/components/shared/commit_message_field_spec.js +++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js @@ -23,10 +23,6 @@ describe('CommitMessageField', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - const findTextArea = () => wrapper.find('textarea'); const findHighlights = () => wrapper.findByTestId('highlights'); const findHighlightsText = () => wrapper.findByTestId('highlights-text'); diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js index b70c9659e46..4bd5a6527e2 100644 --- a/spec/frontend/ide/components/shared/tokened_input_spec.js +++ b/spec/frontend/ide/components/shared/tokened_input_spec.js @@ -28,10 +28,6 @@ describe('IDE shared/TokenedInput', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders tokens', () => { createComponent(); const renderedTokens = getTokenElements(wrapper).wrappers.map((w) => w.text()); diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js index 15fb0fe9013..3a691c151d5 100644 --- a/spec/frontend/ide/components/terminal/empty_state_spec.js +++ b/spec/frontend/ide/components/terminal/empty_state_spec.js @@ -16,10 +16,6 @@ describe('IDE TerminalEmptyState', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('does not show illustration, if no path specified', () => { factory(); diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js index 0d22f7f73fe..0500c116d23 100644 --- a/spec/frontend/ide/components/terminal/terminal_spec.js +++ b/spec/frontend/ide/components/terminal/terminal_spec.js @@ -59,10 +59,6 @@ describe('IDE Terminal', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('loading text', () => { [STARTING, PENDING].forEach((status) => { it(`shows when starting (${status})`, () => { diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js index 57c8da9f5b7..b8ffaa89047 100644 --- a/spec/frontend/ide/components/terminal/view_spec.js +++ b/spec/frontend/ide/components/terminal/view_spec.js @@ -59,10 +59,6 @@ describe('IDE TerminalView', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders empty state', async () => { await factory(); 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 5b1502cc190..e420e28c7b6 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 @@ -22,10 +22,6 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => { beforeEach(createComponent); - afterEach(() => { - wrapper.destroy(); - }); - describe('with terminal sync module in store', () => { beforeEach(() => { store.registerModule('terminalSync', { 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 147235abc8e..4541c3b5ec8 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 @@ -48,10 +48,6 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when doing nothing', () => { it('shows nothing', () => { createComponent(); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 623dee387e5..cd099e60070 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -252,12 +252,10 @@ describe('IDE services', () => { describe('pingUsage', () => { let mock; - let relativeUrlRoot; const TEST_RELATIVE_URL_ROOT = 'blah-blah'; beforeEach(() => { jest.spyOn(axios, 'post'); - relativeUrlRoot = gon.relative_url_root; gon.relative_url_root = TEST_RELATIVE_URL_ROOT; mock = new MockAdapter(axios); @@ -265,7 +263,6 @@ describe('IDE services', () => { afterEach(() => { mock.restore(); - gon.relative_url_root = relativeUrlRoot; }); it('posts to usage endpoint', () => { diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js index 5f752197e13..5b6b60a250c 100644 --- a/spec/frontend/ide/services/terminals_spec.js +++ b/spec/frontend/ide/services/terminals_spec.js @@ -9,7 +9,6 @@ const TEST_BRANCH = 'ref'; describe('~/ide/services/terminals', () => { let axiosSpy; let mock; - const prevRelativeUrlRoot = gon.relative_url_root; beforeEach(() => { axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]); @@ -19,7 +18,6 @@ describe('~/ide/services/terminals', () => { }); afterEach(() => { - gon.relative_url_root = prevRelativeUrlRoot; mock.restore(); }); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js index 90ca8526698..7f4e1cf761d 100644 --- a/spec/frontend/ide/stores/actions/file_spec.js +++ b/spec/frontend/ide/stores/actions/file_spec.js @@ -16,7 +16,6 @@ const RELATIVE_URL_ROOT = '/gitlab'; describe('IDE store file actions', () => { let mock; - let originalGon; let store; let router; @@ -24,9 +23,7 @@ describe('IDE store file actions', () => { stubPerformanceWebAPI(); mock = new MockAdapter(axios); - originalGon = window.gon; window.gon = { - ...window.gon, relative_url_root: RELATIVE_URL_ROOT, }; @@ -44,7 +41,6 @@ describe('IDE store file actions', () => { afterEach(() => { mock.restore(); - window.gon = originalGon; }); describe('closeFile', () => { diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js index fbae84631ee..a41ffdb0a31 100644 --- a/spec/frontend/ide/stores/actions/merge_request_spec.js +++ b/spec/frontend/ide/stores/actions/merge_request_spec.js @@ -3,7 +3,7 @@ import { range } from 'lodash'; import { stubPerformanceWebAPI } from 'helpers/performance'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants'; import service from '~/ide/services'; import { createStore } from '~/ide/stores'; @@ -30,7 +30,7 @@ const createMergeRequestChangesCount = (n) => const testGetUrlForPath = (path) => `${TEST_HOST}/test/${path}`; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('IDE store merge request actions', () => { let store; @@ -135,7 +135,7 @@ describe('IDE store merge request actions', () => { mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError(); }); - it('flashes message, if error', () => { + it('shows an alert, if error', () => { return store .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, @@ -519,7 +519,7 @@ describe('IDE store merge request actions', () => { ); }); - it('flashes message, if error', () => { + it('shows an alert, if error', () => { store.dispatch.mockRejectedValue(); return openMergeRequest(store, mr).catch(() => { diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js index 5a5ead4c544..b13228c20f5 100644 --- a/spec/frontend/ide/stores/actions/project_spec.js +++ b/spec/frontend/ide/stores/actions/project_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import testAction from 'helpers/vuex_action_helper'; import api from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import service from '~/ide/services'; import { createStore } from '~/ide/stores'; import { @@ -19,7 +19,7 @@ import { import { logError } from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/logger'); const TEST_PROJECT_ID = 'abc/def'; @@ -104,7 +104,7 @@ describe('IDE store project actions', () => { desc | projectPath | responseSuccess | expectedMutations ${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]} ${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]} - ${'flashes an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]} + ${'alerts an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]} `('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => { store.state.currentProjectId = projectPath; if (responseSuccess) { diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js index 1c90c0f943a..63b63af667c 100644 --- a/spec/frontend/ide/stores/actions_spec.js +++ b/spec/frontend/ide/stores/actions_spec.js @@ -4,7 +4,7 @@ import testAction from 'helpers/vuex_action_helper'; import eventHub from '~/ide/eventhub'; import { createRouter } from '~/ide/ide_router'; import { createStore } from '~/ide/stores'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { init, stageAllChanges, @@ -31,7 +31,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, })); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Multi-file store actions', () => { let store; @@ -210,7 +210,7 @@ describe('Multi-file store actions', () => { expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test'); }); - it('creates flash message if file already exists', async () => { + it('creates alert message if file already exists', async () => { const f = file('test', '1', 'blob'); store.state.trees['abcproject/mybranch'].tree = [f]; store.state.entries[f.path] = f; @@ -927,7 +927,7 @@ describe('Multi-file store actions', () => { expect(document.querySelector('.flash-alert')).toBeNull(); }); - it('does not pass the error further and flashes an alert if error is not 404', async () => { + it('does not pass the error further and creates an alert if error is not 404', async () => { mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_IM_A_TEAPOT); await expect(getBranchData(...callParams)).rejects.toEqual( diff --git a/spec/frontend/ide/stores/extend_spec.js b/spec/frontend/ide/stores/extend_spec.js index ffb00f9ef5b..88909999c82 100644 --- a/spec/frontend/ide/stores/extend_spec.js +++ b/spec/frontend/ide/stores/extend_spec.js @@ -6,12 +6,10 @@ jest.mock('~/ide/stores/plugins/terminal', () => jest.fn()); jest.mock('~/ide/stores/plugins/terminal_sync', () => jest.fn()); describe('ide/stores/extend', () => { - let prevGon; let store; let el; beforeEach(() => { - prevGon = global.gon; store = {}; el = {}; @@ -23,13 +21,12 @@ describe('ide/stores/extend', () => { }); afterEach(() => { - global.gon = prevGon; terminalPlugin.mockClear(); terminalSyncPlugin.mockClear(); }); const withGonFeatures = (features) => { - global.gon = { ...global.gon, features }; + global.gon.features = features; }; describe('terminalPlugin', () => { diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js index d4166a3bd6d..0fe6a16c676 100644 --- a/spec/frontend/ide/stores/getters_spec.js +++ b/spec/frontend/ide/stores/getters_spec.js @@ -24,11 +24,8 @@ const TEST_FORK_PATH = '/test/fork/path'; describe('IDE store getters', () => { let localState; let localStore; - let origGon; beforeEach(() => { - origGon = window.gon; - // Feature flag is defaulted to on in prod window.gon = { features: { rejectUnsignedCommitsByGitlab: true } }; @@ -36,10 +33,6 @@ describe('IDE store getters', () => { localState = localStore.state; }); - afterEach(() => { - window.gon = origGon; - }); - describe('activeFile', () => { it('returns the current active file', () => { localState.openFiles.push(file()); diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js index 4068a9d0919..872aa9b6e6b 100644 --- a/spec/frontend/ide/stores/modules/commit/actions_spec.js +++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js @@ -1,6 +1,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { file } from 'jest/ide/helpers'; import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants'; import eventHub from '~/ide/eventhub'; @@ -39,12 +40,14 @@ describe('IDE commit module actions', () => { let mock; let store; let router; + let trackingSpy; beforeEach(() => { store = createStore(); router = createRouter(store); gon.api_version = 'v1'; mock = new MockAdapter(axios); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); jest.spyOn(router, 'push').mockImplementation(); mock @@ -53,7 +56,7 @@ describe('IDE commit module actions', () => { }); afterEach(() => { - delete gon.api_version; + unmockTracking(); mock.restore(); }); @@ -81,19 +84,12 @@ describe('IDE commit module actions', () => { }); describe('updateBranchName', () => { - let originalGon; - beforeEach(() => { - originalGon = window.gon; - window.gon = { current_username: 'johndoe' }; + window.gon.current_username = 'johndoe'; store.state.currentBranchId = 'main'; }); - afterEach(() => { - window.gon = originalGon; - }); - it('updates store with new branch name', async () => { await store.dispatch('commit/updateBranchName', 'branch-name'); @@ -430,6 +426,28 @@ describe('IDE commit module actions', () => { }); }); }); + + describe('learnGitlabSource', () => { + describe('learnGitlabSource is true', () => { + it('tracks commit', async () => { + store.state.learnGitlabSource = true; + + await store.dispatch('commit/commitChanges'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'commit', { + label: 'web_ide_learn_gitlab_source', + }); + }); + }); + + describe('learnGitlabSource is false', () => { + it('does not track commit', async () => { + await store.dispatch('commit/commitChanges'); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + }); }); describe('success response with failed message', () => { @@ -447,6 +465,26 @@ describe('IDE commit module actions', () => { expect(alert.textContent.trim()).toBe('failed message'); }); + + describe('learnGitlabSource', () => { + describe('learnGitlabSource is true', () => { + it('does not track commit', async () => { + store.state.learnGitlabSource = true; + + await store.dispatch('commit/commitChanges'); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + + describe('learnGitlabSource is false', () => { + it('does not track commit', async () => { + await store.dispatch('commit/commitChanges'); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + }); }); describe('failed response', () => { @@ -466,6 +504,26 @@ describe('IDE commit module actions', () => { ['commit/SET_ERROR', createUnexpectedCommitError(), undefined], ]); }); + + describe('learnGitlabSource', () => { + describe('learnGitlabSource is true', () => { + it('does not track commit', async () => { + store.state.learnGitlabSource = true; + + await store.dispatch('commit/commitChanges').catch(() => {}); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + + describe('learnGitlabSource is false', () => { + it('does not track commit', async () => { + await store.dispatch('commit/commitChanges').catch(() => {}); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + }); }); describe('first commit of a branch', () => { diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js index 0287e5269ee..3f7ded5e718 100644 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import * as actions from '~/ide/stores/modules/terminal/actions/session_controls'; import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants'; import * as messages from '~/ide/stores/modules/terminal/messages'; @@ -13,7 +13,7 @@ import { HTTP_STATUS_UNPROCESSABLE_ENTITY, } from '~/lib/utils/http_status'; -jest.mock('~/flash'); +jest.mock('~/alert'); const TEST_PROJECT_PATH = 'lorem/root'; const TEST_BRANCH_ID = 'main'; @@ -91,7 +91,7 @@ describe('IDE store terminal session controls actions', () => { }); describe('receiveStartSessionError', () => { - it('flashes message', () => { + it('shows an alert', () => { actions.receiveStartSessionError({ dispatch }); expect(createAlert).toHaveBeenCalledWith({ @@ -165,7 +165,7 @@ describe('IDE store terminal session controls actions', () => { }); describe('receiveStopSessionError', () => { - it('flashes message', () => { + it('shows an alert', () => { actions.receiveStopSessionError({ dispatch }); expect(createAlert).toHaveBeenCalledWith({ diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js index 9616733f052..30ae7d203a9 100644 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js +++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import * as actions from '~/ide/stores/modules/terminal/actions/session_status'; import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants'; import * as messages from '~/ide/stores/modules/terminal/messages'; @@ -8,7 +8,7 @@ import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -jest.mock('~/flash'); +jest.mock('~/alert'); const TEST_SESSION = { id: 7, @@ -113,7 +113,7 @@ describe('IDE store terminal session controls actions', () => { }); describe('receiveSessionStatusError', () => { - it('flashes message', () => { + it('shows an alert', () => { actions.receiveSessionStatusError({ dispatch }); expect(createAlert).toHaveBeenCalledWith({ diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js index 31e097cfa7b..b44bc33de6f 100644 --- a/spec/frontend/import_entities/components/group_dropdown_spec.js +++ b/spec/frontend/import_entities/components/group_dropdown_spec.js @@ -64,10 +64,6 @@ describe('Import entities group dropdown component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('passes namespaces from graphql query to default slot', async () => { createComponent(); jest.advanceTimersByTime(DEBOUNCE_DELAY); diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js index 56c4ed827d7..3488d9f60c8 100644 --- a/spec/frontend/import_entities/components/import_status_spec.js +++ b/spec/frontend/import_entities/components/import_status_spec.js @@ -12,10 +12,6 @@ describe('Import entities status component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('success status', () => { const getStatusText = () => wrapper.findComponent(GlBadge).text(); const getStatusIcon = () => wrapper.findComponent(GlBadge).props('icon'); diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js index 163a60bae36..1a52485f779 100644 --- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDropdown, GlIcon, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue'; @@ -8,7 +8,6 @@ describe('import actions cell', () => { const createComponent = (props) => { wrapper = shallowMount(ImportActionsCell, { propsData: { - isProjectsImportEnabled: false, isFinished: false, isAvailableForImport: false, isInvalid: false, @@ -17,19 +16,15 @@ describe('import actions cell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when group is available for import', () => { beforeEach(() => { createComponent({ isAvailableForImport: true }); }); - it('renders import button', () => { - const button = wrapper.findComponent(GlButton); - expect(button.exists()).toBe(true); - expect(button.text()).toBe('Import'); + it('renders import dropdown', () => { + const dropdown = wrapper.findComponent(GlDropdown); + expect(dropdown.exists()).toBe(true); + expect(dropdown.props('text')).toBe('Import with projects'); }); it('does not render icon with a hint', () => { @@ -42,10 +37,10 @@ describe('import actions cell', () => { createComponent({ isAvailableForImport: false, isFinished: true }); }); - it('renders re-import button', () => { - const button = wrapper.findComponent(GlButton); - expect(button.exists()).toBe(true); - expect(button.text()).toBe('Re-import'); + it('renders re-import dropdown', () => { + const dropdown = wrapper.findComponent(GlDropdown); + expect(dropdown.exists()).toBe(true); + expect(dropdown.props('text')).toBe('Re-import with projects'); }); it('renders icon with a hint', () => { @@ -57,25 +52,25 @@ describe('import actions cell', () => { }); }); - it('does not render import button when group is not available for import', () => { + it('does not render import dropdown when group is not available for import', () => { createComponent({ isAvailableForImport: false }); - const button = wrapper.findComponent(GlButton); - expect(button.exists()).toBe(false); + const dropdown = wrapper.findComponent(GlDropdown); + expect(dropdown.exists()).toBe(false); }); - it('renders import button as disabled when group is invalid', () => { + it('renders import dropdown as disabled when group is invalid', () => { createComponent({ isInvalid: true, isAvailableForImport: true }); - const button = wrapper.findComponent(GlButton); - expect(button.props().disabled).toBe(true); + const dropdown = wrapper.findComponent(GlDropdown); + expect(dropdown.props().disabled).toBe(true); }); it('emits import-group event when import button is clicked', () => { createComponent({ isAvailableForImport: true }); - const button = wrapper.findComponent(GlButton); - button.vm.$emit('click'); + const dropdown = wrapper.findComponent(GlDropdown); + dropdown.vm.$emit('click'); expect(wrapper.emitted('import-group')).toHaveLength(1); }); @@ -85,10 +80,10 @@ describe('import actions cell', () => { ${false} | ${'Import'} ${true} | ${'Re-import'} `( - 'when import projects is enabled, group is available for import and finish status is $status', + 'group is available for import and finish status is $isFinished', ({ isFinished, expectedAction }) => { beforeEach(() => { - createComponent({ isProjectsImportEnabled: true, isAvailableForImport: true, isFinished }); + createComponent({ isAvailableForImport: true, isFinished }); }); it('render import dropdown', () => { diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js index f2735d86493..9ead483d02f 100644 --- a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js @@ -22,10 +22,6 @@ describe('import source cell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when group status is NONE', () => { beforeEach(() => { group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE }); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index c7bda5a60ec..205218fdabd 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -6,7 +6,7 @@ import MockAdapter from 'axios-mock-adapter'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { HTTP_STATUS_OK, HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status'; import axios from '~/lib/utils/axios_utils'; import { STATUSES } from '~/import_entities/constants'; @@ -23,7 +23,7 @@ import { generateFakeEntry, } from '../graphql/fixtures'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/import_entities/import_groups/services/status_poller'); Vue.use(VueApollo); @@ -49,12 +49,12 @@ describe('import table', () => { }, }; - const findImportSelectedButton = () => - wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected'); const findImportSelectedDropdown = () => - wrapper.findAll('.gl-dropdown').wrappers.find((w) => w.text().includes('Import with projects')); - const findImportButtons = () => - wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import'); + wrapper.find('[data-testid="import-selected-groups-dropdown"]'); + const findRowImportDropdownAtIndex = (idx) => + wrapper.findAll('tbody td button').wrappers.filter((w) => w.text() === 'Import with projects')[ + idx + ]; const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]'); const findTargetNamespaceDropdown = (rowWrapper) => rowWrapper.find('[data-testid="target-namespace-selector"]'); @@ -70,12 +70,7 @@ describe('import table', () => { const findRowCheckbox = (idx) => wrapper.findAll('tbody td input[type=checkbox]').at(idx); const selectRow = (idx) => findRowCheckbox(idx).setChecked(true); - const createComponent = ({ - bulkImportSourceGroups, - importGroups, - defaultTargetNamespace, - glFeatures = {}, - }) => { + const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => { apolloProvider = createMockApollo( [ [ @@ -102,10 +97,7 @@ describe('import table', () => { defaultTargetNamespace, }, directives: { - GlTooltip: createMockDirective(), - }, - provide: { - glFeatures, + GlTooltip: createMockDirective('gl-tooltip'), }, apolloProvider, }); @@ -120,10 +112,6 @@ describe('import table', () => { axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK, { exists: false }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('loading state', () => { it('renders loading icon while performing request', async () => { createComponent({ @@ -134,7 +122,7 @@ describe('import table', () => { expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); - it('does not renders loading icon when request is completed', async () => { + it('does not render loading icon when request is completed', async () => { createComponent({ bulkImportSourceGroups: () => [], }); @@ -245,12 +233,13 @@ describe('import table', () => { await waitForPromises(); - await findImportButtons()[0].trigger('click'); + await findRowImportDropdownAtIndex(0).trigger('click'); expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ mutation: importGroupsMutation, variables: { importRequests: [ { + migrateProjects: true, newName: FAKE_GROUP.lastImportTarget.newName, sourceGroupId: FAKE_GROUP.id, targetNamespace: AVAILABLE_NAMESPACES[0].fullPath, @@ -273,7 +262,7 @@ describe('import table', () => { }); await waitForPromises(); - await findImportButtons()[0].trigger('click'); + await findRowImportDropdownAtIndex(0).trigger('click'); await waitForPromises(); expect(createAlert).toHaveBeenCalledWith( @@ -298,7 +287,7 @@ describe('import table', () => { }); await waitForPromises(); - await findImportButtons()[0].trigger('click'); + await findRowImportDropdownAtIndex(0).trigger('click'); await waitForPromises(); expect(createAlert).not.toHaveBeenCalled(); @@ -476,7 +465,7 @@ describe('import table', () => { }); await waitForPromises(); - expect(findImportSelectedButton().props().disabled).toBe(true); + expect(findImportSelectedDropdown().props().disabled).toBe(true); }); it('import selected button is enabled when groups were selected for import', async () => { @@ -491,7 +480,7 @@ describe('import table', () => { await selectRow(0); - expect(findImportSelectedButton().props().disabled).toBe(false); + expect(findImportSelectedDropdown().props().disabled).toBe(false); }); it('does not allow selecting already started groups', async () => { @@ -509,7 +498,7 @@ describe('import table', () => { await selectRow(0); await nextTick(); - expect(findImportSelectedButton().props().disabled).toBe(true); + expect(findImportSelectedDropdown().props().disabled).toBe(true); }); it('does not allow selecting groups with validation errors', async () => { @@ -534,10 +523,10 @@ describe('import table', () => { await selectRow(0); await nextTick(); - expect(findImportSelectedButton().props().disabled).toBe(true); + expect(findImportSelectedDropdown().props().disabled).toBe(true); }); - it('invokes importGroups mutation when import selected button is clicked', async () => { + it('invokes importGroups mutation when import selected dropdown is clicked', async () => { const NEW_GROUPS = [ generateFakeEntry({ id: 1, status: STATUSES.NONE }), generateFakeEntry({ id: 2, status: STATUSES.NONE }), @@ -558,7 +547,7 @@ describe('import table', () => { await selectRow(1); await nextTick(); - await findImportSelectedButton().trigger('click'); + await findImportSelectedDropdown().find('button').trigger('click'); expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ mutation: importGroupsMutation, @@ -679,7 +668,7 @@ describe('import table', () => { }); }); - describe('when import projects is enabled', () => { + describe('importing projects', () => { const NEW_GROUPS = [ generateFakeEntry({ id: 1, status: STATUSES.NONE }), generateFakeEntry({ id: 2, status: STATUSES.NONE }), @@ -693,9 +682,6 @@ describe('import table', () => { pageInfo: FAKE_PAGE_INFO, versionValidation: FAKE_VERSION_VALIDATION, }), - glFeatures: { - bulkImportProjects: true, - }, }); jest.spyOn(apolloProvider.defaultClient, 'mutate'); return waitForPromises(); diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js index d5286e71c44..a524d9ebdb0 100644 --- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js @@ -57,11 +57,6 @@ describe('import target cell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('events', () => { beforeEach(async () => { group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE }); diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js index ce111a0c10c..83566469176 100644 --- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -16,7 +16,7 @@ import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { statusEndpointFixture } from './fixtures'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({ LocalStorageCache: jest.fn().mockImplementation(function mock() { this.get = jest.fn(); diff --git a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js index 4a1b85d24e3..5ee2b2e698f 100644 --- a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js +++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import Visibility from 'visibilityjs'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { STATUSES } from '~/import_entities/constants'; import { StatusPoller } from '~/import_entities/import_groups/services/status_poller'; import axios from '~/lib/utils/axios_utils'; @@ -8,7 +8,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import Poll from '~/lib/utils/poll'; jest.mock('visibilityjs'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/poll'); const FAKE_POLL_PATH = '/fake/poll/path'; @@ -81,7 +81,7 @@ describe('Bulk import status poller', () => { expect(pollInstance.makeRequest).toHaveBeenCalled(); }); - it('when error occurs shows flash with error', () => { + it('when error occurs shows alert with error', () => { const [[pollConfig]] = Poll.mock.calls; pollConfig.errorCallback(); expect(createAlert).toHaveBeenCalled(); diff --git a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js index 68716600592..2294d236e8b 100644 --- a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js +++ b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js @@ -25,10 +25,6 @@ describe('Import Advanced Settings', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders GLFormCheckbox for each optional stage', () => { expect(wrapper.findAllComponents(GlFormCheckbox)).toHaveLength(OPTIONAL_STAGES.length); }); diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js index e613b9756af..8e73f76382a 100644 --- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js @@ -60,11 +60,6 @@ describe('ProviderRepoTableRow', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when rendering importable project', () => { const repo = { importSource: { diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js index 990587d4af7..f78016eefcf 100644 --- a/spec/frontend/import_entities/import_projects/store/actions_spec.js +++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { STATUSES, PROVIDERS } from '~/import_entities/constants'; import actionsFactory from '~/import_entities/import_projects/store/actions'; import { getImportTarget } from '~/import_entities/import_projects/store/getters'; @@ -27,7 +27,7 @@ import { HTTP_STATUS_TOO_MANY_REQUESTS, } from '~/lib/utils/http_status'; -jest.mock('~/flash'); +jest.mock('~/alert'); const MOCK_ENDPOINT = `${TEST_HOST}/endpoint.json`; const endpoints = { diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js index 1d1b285c1b6..c5c29b4bb19 100644 --- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js +++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js @@ -1,12 +1,12 @@ import AxiosMockAdapter from 'axios-mock-adapter'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { ERROR_MSG } from '~/incidents_settings/constants'; import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); describe('IncidentsSettingsService', () => { @@ -33,7 +33,7 @@ describe('IncidentsSettingsService', () => { }); }); - it('should display a flash message on update error', () => { + it('should display an alert message on update error', () => { mock.onPatch().reply(HTTP_STATUS_BAD_REQUEST); return service.updateSettings({}).then(() => { diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js index 521a861829b..77258db437d 100644 --- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js +++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js @@ -26,10 +26,6 @@ describe('Alert integration settings form', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should match the default snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js index 1f7a5f0dbc9..8afff842a85 100644 --- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js +++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js @@ -17,10 +17,6 @@ describe('ActiveCheckbox', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findInputInCheckbox = () => findGlFormCheckbox().find('input'); diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js index cbe3402727a..dfb6b7d9a9c 100644 --- a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js +++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js @@ -14,10 +14,6 @@ describe('ConfirmationModal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlModal = () => wrapper.findComponent(GlModal); describe('template', () => { diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js index 7589b04b0fd..e1d9aef752f 100644 --- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js +++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js @@ -21,10 +21,6 @@ describe('DynamicField', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findGlFormInput = () => wrapper.findComponent(GlFormInput); diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index 383dfb36aa5..58fb456eb53 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -84,7 +84,6 @@ describe('IntegrationForm', () => { }); afterEach(() => { - wrapper.destroy(); mockAxios.restore(); }); @@ -507,30 +506,21 @@ describe('IntegrationForm', () => { const dummyHelp = 'Foo Help'; it.each` - integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} - ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true} - ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false} - ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} - ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false} - ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false} - ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true} - ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} - ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false} + integration | helpHtml | sections | shouldShowSections | shouldShowHelp + ${INTEGRATION_FORM_TYPE_SLACK} | ${''} | ${[]} | ${false} | ${false} + ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[]} | ${false} | ${true} + ${INTEGRATION_FORM_TYPE_SLACK} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} + ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true} + ${'foo'} | ${''} | ${[]} | ${false} | ${false} + ${'foo'} | ${dummyHelp} | ${[]} | ${false} | ${true} + ${'foo'} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false} + ${'foo'} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false} `( - '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration', - ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => { + '$sections sections, and "$helpHtml" helpHtml for "$integration" integration', + ({ integration, helpHtml, sections, shouldShowSections, shouldShowHelp }) => { createComponent({ provide: { helpHtml, - glFeatures: { integrationSlackAppNotifications: flagIsOn }, }, customStateProps: { sections, @@ -553,20 +543,15 @@ describe('IntegrationForm', () => { ${false} | ${true} | ${'When having only the fields without a section'} `('$description', ({ hasSections, hasFieldsWithoutSections }) => { it.each` - prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert - ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true} - ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${true} | ${false} - ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${false} | ${false} - ${'does not'} | ${'foo'} | ${true} | ${true} | ${false} - ${'does not'} | ${'foo'} | ${false} | ${true} | ${false} - ${'does not'} | ${'foo'} | ${true} | ${false} | ${false} + prefix | integration | shouldUpgradeSlack | shouldShowAlert + ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} + ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${false} + ${'does not'} | ${'foo'} | ${true} | ${false} + ${'does not'} | ${'foo'} | ${false} | ${false} `( - '$prefix render the upgrade warning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections', - ({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => { + '$prefix render the upgrade warning when we are in "$integration" integration with Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections', + ({ integration, shouldUpgradeSlack, shouldShowAlert }) => { createComponent({ - provide: { - glFeatures: { integrationSlackAppNotifications: flagIsOn }, - }, customStateProps: { shouldUpgradeSlack, type: integration, diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js index fa91f8de45a..90ee69ef2dc 100644 --- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js @@ -31,10 +31,6 @@ describe('JiraIssuesFields', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findEnableCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findEnableCheckboxDisabled = () => findEnableCheckbox().find('[type=checkbox]').attributes('disabled'); diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js index 6011b3e6edc..f876a497f98 100644 --- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js @@ -22,10 +22,6 @@ describe('JiraTriggerFields', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findCommentSettings = () => wrapper.findByTestId('comment-settings'); const findCommentDetail = () => wrapper.findByTestId('comment-detail'); const findCommentSettingsCheckbox = () => findCommentSettings().findComponent(GlFormCheckbox); diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js index 90facaff1f9..2d1a6b3ace1 100644 --- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js +++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js @@ -26,10 +26,6 @@ describe('OverrideDropdown', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlLink = () => wrapper.findComponent(GlLink); const findGlDropdown = () => wrapper.findComponent(GlDropdown); diff --git a/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js new file mode 100644 index 00000000000..62f0439a13f --- /dev/null +++ b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; + +import IntegrationSectionAppleAppStore from '~/integrations/edit/components/sections/apple_app_store.vue'; +import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue'; +import { createStore } from '~/integrations/edit/store'; + +describe('IntegrationSectionAppleAppStore', () => { + let wrapper; + + const createComponent = (componentFields) => { + const store = createStore({ + customState: { ...componentFields }, + }); + wrapper = shallowMount(IntegrationSectionAppleAppStore, { + store, + }); + }; + + const componentFields = (fileName = '') => { + return { + fields: [ + { + name: 'app_store_private_key_file_name', + value: fileName, + }, + ], + }; + }; + + const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField); + + describe('computed properties', () => { + it('renders UploadDropzoneField with default values', () => { + createComponent(componentFields()); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'The Apple App Store Connect Private Key (.p8)', + helpText: '', + }); + }); + + it('renders UploadDropzoneField with custom values for an attached file', () => { + createComponent(componentFields('fileName.txt')); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'Upload a new Apple App Store Connect Private Key (replace fileName.txt)', + helpText: 'Leave empty to use your current Private Key.', + }); + }); + }); +}); diff --git a/spec/frontend/integrations/edit/components/sections/configuration_spec.js b/spec/frontend/integrations/edit/components/sections/configuration_spec.js index e697212ea0b..c8a7d17c041 100644 --- a/spec/frontend/integrations/edit/components/sections/configuration_spec.js +++ b/spec/frontend/integrations/edit/components/sections/configuration_spec.js @@ -19,10 +19,6 @@ describe('IntegrationSectionCoonfiguration', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField); describe('template', () => { diff --git a/spec/frontend/integrations/edit/components/sections/connection_spec.js b/spec/frontend/integrations/edit/components/sections/connection_spec.js index 1eb92e80723..a24253d542d 100644 --- a/spec/frontend/integrations/edit/components/sections/connection_spec.js +++ b/spec/frontend/integrations/edit/components/sections/connection_spec.js @@ -20,10 +20,6 @@ describe('IntegrationSectionConnection', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox); const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField); diff --git a/spec/frontend/integrations/edit/components/sections/google_play_spec.js b/spec/frontend/integrations/edit/components/sections/google_play_spec.js new file mode 100644 index 00000000000..c0d6d17f639 --- /dev/null +++ b/spec/frontend/integrations/edit/components/sections/google_play_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; + +import IntegrationSectionGooglePlay from '~/integrations/edit/components/sections/google_play.vue'; +import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue'; +import { createStore } from '~/integrations/edit/store'; + +describe('IntegrationSectionGooglePlay', () => { + let wrapper; + + const createComponent = (fileName = '') => { + const store = createStore({ + customState: { + fields: [ + { + name: 'service_account_key_file_name', + value: fileName, + }, + ], + }, + }); + + wrapper = shallowMount(IntegrationSectionGooglePlay, { + store, + }); + }; + + const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField); + + describe('computed properties', () => { + it('renders UploadDropzoneField with default values', () => { + createComponent(); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'Service account key (.json)', + helpText: '', + }); + }); + + it('renders UploadDropzoneField with custom values for an attached file', () => { + createComponent('fileName.txt'); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'Upload a new service account key (replace fileName.txt)', + helpText: 'Leave empty to use your current service account key.', + }); + }); + }); +}); diff --git a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js index a7c1cc2a03f..8b39fa8f583 100644 --- a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js +++ b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js @@ -18,10 +18,6 @@ describe('IntegrationSectionJiraIssue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields); describe('template', () => { diff --git a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js index d4ab9864fab..b3b7f508e25 100644 --- a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js +++ b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js @@ -18,10 +18,6 @@ describe('IntegrationSectionJiraTrigger', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields); describe('template', () => { diff --git a/spec/frontend/integrations/edit/components/sections/trigger_spec.js b/spec/frontend/integrations/edit/components/sections/trigger_spec.js index 883f5c7bf79..b9c1efbb0a2 100644 --- a/spec/frontend/integrations/edit/components/sections/trigger_spec.js +++ b/spec/frontend/integrations/edit/components/sections/trigger_spec.js @@ -18,10 +18,6 @@ describe('IntegrationSectionTrigger', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findAllTriggerFields = () => wrapper.findAllComponents(TriggerField); describe('template', () => { diff --git a/spec/frontend/integrations/edit/components/trigger_field_spec.js b/spec/frontend/integrations/edit/components/trigger_field_spec.js index ed0b3324708..3b736b33a2f 100644 --- a/spec/frontend/integrations/edit/components/trigger_field_spec.js +++ b/spec/frontend/integrations/edit/components/trigger_field_spec.js @@ -23,10 +23,6 @@ describe('TriggerField', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findGlFormInput = () => wrapper.findComponent(GlFormInput); const findHiddenInput = () => wrapper.find('input[type="hidden"]'); diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js index 082eeea30f1..defa02aefd2 100644 --- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js +++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js @@ -20,10 +20,6 @@ describe('TriggerFields', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label'); const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAllComponents(GlFormGroup); const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); diff --git a/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js new file mode 100644 index 00000000000..36e20db0022 --- /dev/null +++ b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js @@ -0,0 +1,88 @@ +import { mount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; +import { nextTick } from 'vue'; + +import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue'; +import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; +import { mockField } from '../mock_data'; + +describe('UploadDropzoneField', () => { + let wrapper; + + const contentsInputName = 'service[app_store_private_key]'; + const fileNameInputName = 'service[app_store_private_key_file_name]'; + + const createComponent = (props) => { + wrapper = mount(UploadDropzoneField, { + propsData: { + ...mockField, + ...props, + name: contentsInputName, + label: 'Input Label', + fileInputName: fileNameInputName, + }, + }); + }; + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findUploadDropzone = () => wrapper.findComponent(UploadDropzone); + const findFileContentsHiddenInput = () => wrapper.find(`input[name="${contentsInputName}"]`); + const findFileNameHiddenInput = () => wrapper.find(`input[name="${fileNameInputName}"]`); + + describe('template', () => { + it('adds the expected file inputFieldName', () => { + createComponent(); + + expect(findUploadDropzone().props('inputFieldName')).toBe('service[dropzone_file_name]'); + }); + + it('adds a disabled, hidden text input for the file contents', () => { + createComponent(); + + expect(findFileContentsHiddenInput().attributes('name')).toBe(contentsInputName); + expect(findFileContentsHiddenInput().attributes('disabled')).toBeDefined(); + }); + + it('adds a disabled, hidden text input for the file name', () => { + createComponent(); + + expect(findFileNameHiddenInput().attributes('name')).toBe(fileNameInputName); + expect(findFileNameHiddenInput().attributes('disabled')).toBeDefined(); + }); + }); + + describe('clearError', () => { + it('clears uploadError when called', async () => { + createComponent(); + + expect(findGlAlert().exists()).toBe(false); + + findUploadDropzone().vm.$emit('error'); + await nextTick(); + + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe( + 'Error: You are trying to upload something other than an allowed file.', + ); + + findGlAlert().vm.$emit('dismiss'); + await nextTick(); + + expect(findGlAlert().exists()).toBe(false); + }); + }); + + describe('onError', () => { + it('assigns uploadError to the supplied custom message', async () => { + const message = 'test error message'; + createComponent({ errorMessage: message }); + + findUploadDropzone().vm.$emit('error'); + + await nextTick(); + + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe(message); + }); + }); +}); diff --git a/spec/frontend/integrations/index/components/integrations_list_spec.js b/spec/frontend/integrations/index/components/integrations_list_spec.js index ee54a5fd359..155a3d1c6be 100644 --- a/spec/frontend/integrations/index/components/integrations_list_spec.js +++ b/spec/frontend/integrations/index/components/integrations_list_spec.js @@ -13,10 +13,6 @@ describe('IntegrationsList', () => { wrapper = shallowMountExtended(IntegrationsList, { propsData }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('provides correct `integrations` prop to the IntegrationsTable instance', () => { createComponent({ integrations: [...mockInactiveIntegrations, ...mockActiveIntegrations] }); diff --git a/spec/frontend/integrations/index/components/integrations_table_spec.js b/spec/frontend/integrations/index/components/integrations_table_spec.js index 976c7b74890..5456a23a98d 100644 --- a/spec/frontend/integrations/index/components/integrations_table_spec.js +++ b/spec/frontend/integrations/index/components/integrations_table_spec.js @@ -1,6 +1,5 @@ -import { GlTable, GlIcon, GlLink } from '@gitlab/ui'; +import { GlTable, GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import { INTEGRATION_TYPE_SLACK } from '~/integrations/constants'; import IntegrationsTable from '~/integrations/index/components/integrations_table.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -11,24 +10,15 @@ describe('IntegrationsTable', () => { const findTable = () => wrapper.findComponent(GlTable); - const createComponent = (propsData = {}, flagIsOn = false) => { + const createComponent = (propsData = {}) => { wrapper = mount(IntegrationsTable, { propsData: { integrations: mockActiveIntegrations, ...propsData, }, - provide: { - glFeatures: { - integrationSlackAppNotifications: flagIsOn, - }, - }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe.each([true, false])('when `showUpdatedAt` is %p', (showUpdatedAt) => { beforeEach(() => { createComponent({ showUpdatedAt }); @@ -56,51 +46,4 @@ describe('IntegrationsTable', () => { expect(findTable().findComponent(GlIcon).exists()).toBe(shouldRenderActiveIcon); }); }); - - describe('integrations filtering', () => { - const slackActive = { - ...mockActiveIntegrations[0], - name: INTEGRATION_TYPE_SLACK, - title: 'Slack', - }; - const slackInactive = { - ...mockInactiveIntegrations[0], - name: INTEGRATION_TYPE_SLACK, - title: 'Slack', - }; - - describe.each` - desc | flagIsOn | integrations | expectedIntegrations - ${'only active'} | ${false} | ${mockActiveIntegrations} | ${mockActiveIntegrations} - ${'only active'} | ${true} | ${mockActiveIntegrations} | ${mockActiveIntegrations} - ${'only inactive'} | ${true} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations} - ${'only inactive'} | ${false} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations} - ${'active and inactive'} | ${true} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} - ${'active and inactive'} | ${false} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} - ${'Slack active with active'} | ${false} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]} - ${'Slack active with active'} | ${true} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]} - ${'Slack active with inactive'} | ${false} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]} - ${'Slack active with inactive'} | ${true} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]} - ${'Slack inactive with active'} | ${false} | ${[slackInactive, ...mockActiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations]} - ${'Slack inactive with active'} | ${true} | ${[slackInactive, ...mockActiveIntegrations]} | ${mockActiveIntegrations} - ${'Slack inactive with inactive'} | ${false} | ${[slackInactive, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockInactiveIntegrations]} - ${'Slack inactive with inactive'} | ${true} | ${[slackInactive, ...mockInactiveIntegrations]} | ${mockInactiveIntegrations} - ${'Slack active with active and inactive'} | ${true} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} - ${'Slack active with active and inactive'} | ${false} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} - ${'Slack inactive with active and inactive'} | ${true} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} - ${'Slack inactive with active and inactive'} | ${false} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} - `('when $desc and flag "$flagIsOn"', ({ flagIsOn, integrations, expectedIntegrations }) => { - beforeEach(() => { - createComponent({ integrations }, flagIsOn); - }); - - it('renders correctly', () => { - const links = wrapper.findAllComponents(GlLink); - expect(links).toHaveLength(expectedIntegrations.length); - expectedIntegrations.forEach((integration, index) => { - expect(links.at(index).text()).toBe(integration.title); - }); - }); - }); - }); }); diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js index fdb728281b5..9e863eaecfd 100644 --- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js +++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js @@ -47,7 +47,6 @@ describe('IntegrationOverrides', () => { afterEach(() => { mockAxios.restore(); - wrapper.destroy(); }); const findGlTable = () => wrapper.findComponent(GlTable); diff --git a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js index a728b4d391f..b35a40d69c1 100644 --- a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js +++ b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js @@ -21,10 +21,6 @@ describe('IntegrationTabs', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlBadge = () => wrapper.findComponent(GlBadge); const findGlTab = () => wrapper.findComponent(GlTab); const findSettingsLink = () => wrapper.find('a'); diff --git a/spec/frontend/invite_members/components/confetti_spec.js b/spec/frontend/invite_members/components/confetti_spec.js index 2f361f1dc1e..382569abfd9 100644 --- a/spec/frontend/invite_members/components/confetti_spec.js +++ b/spec/frontend/invite_members/components/confetti_spec.js @@ -6,16 +6,10 @@ jest.mock('canvas-confetti', () => ({ create: jest.fn(), })); -let wrapper; - const createComponent = () => { - wrapper = shallowMount(Confetti); + shallowMount(Confetti); }; -afterEach(() => { - wrapper.destroy(); -}); - describe('Confetti', () => { it('initiates confetti', () => { const basicCannon = jest.spyOn(Confetti.methods, 'basicCannon').mockImplementation(() => {}); diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js index e1563a7bb3a..a1ca9a69926 100644 --- a/spec/frontend/invite_members/components/group_select_spec.js +++ b/spec/frontend/invite_members/components/group_select_spec.js @@ -26,14 +26,9 @@ describe('GroupSelect', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]'); + const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]'); const findAvatarByLabel = (text) => wrapper .findAllComponents(GlAvatarLabeled) @@ -66,6 +61,7 @@ describe('GroupSelect', () => { expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, { exclude_internal: true, active: true, + order_by: 'similarity', }); }); diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js index d839cde163c..74cb59a9b52 100644 --- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js +++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js @@ -54,7 +54,6 @@ beforeEach(() => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/invite_members/components/import_project_members_trigger_spec.js b/spec/frontend/invite_members/components/import_project_members_trigger_spec.js index b6375fcfa22..0e8243491a8 100644 --- a/spec/frontend/invite_members/components/import_project_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/import_project_members_trigger_spec.js @@ -17,10 +17,6 @@ const createComponent = (props = {}) => { describe('ImportProjectMembersTrigger', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - const findButton = () => wrapper.findComponent(GlButton); describe('displayText', () => { diff --git a/spec/frontend/invite_members/components/invite_group_trigger_spec.js b/spec/frontend/invite_members/components/invite_group_trigger_spec.js index 84ddb779a9e..e088dc41a2b 100644 --- a/spec/frontend/invite_members/components/invite_group_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_group_trigger_spec.js @@ -17,11 +17,6 @@ const createComponent = (props = {}) => { describe('InviteGroupTrigger', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findButton = () => wrapper.findComponent(GlButton); describe('displayText', () => { diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js index c2a55517405..82b4717fbf1 100644 --- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js @@ -44,11 +44,6 @@ describe('InviteGroupsModal', () => { createComponent({ isProject: false }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findModal = () => wrapper.findComponent(GlModal); const findGroupSelect = () => wrapper.findComponent(GroupSelect); const findInviteGroupAlert = () => wrapper.findComponent(InviteGroupNotification); @@ -58,11 +53,13 @@ describe('InviteGroupsModal', () => { findMembersFormGroup().attributes('invalid-feedback'); const findBase = () => wrapper.findComponent(InviteModalBase); const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val); - const emitEventFromModal = (eventName) => () => - findModal().vm.$emit(eventName, { preventDefault: jest.fn() }); - const hideModal = emitEventFromModal('hidden'); - const clickInviteButton = emitEventFromModal('primary'); - const clickCancelButton = emitEventFromModal('cancel'); + const hideModal = () => findModal().vm.$emit('hidden', { preventDefault: jest.fn() }); + + const emitClickFromModal = (testId) => () => + wrapper.findByTestId(testId).vm.$emit('click', { preventDefault: jest.fn() }); + + const clickInviteButton = emitClickFromModal('invite-modal-submit'); + const clickCancelButton = emitClickFromModal('invite-modal-cancel'); describe('displaying the correct introText and form group description', () => { describe('when inviting to a project', () => { 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 9687d528321..39d5ddee723 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -73,6 +73,7 @@ describe('InviteMembersModal', () => { wrapper = shallowMountExtended(InviteMembersModal, { provide: { newProjectPath, + name: propsData.name, }, propsData: { usersLimitDataset: {}, @@ -116,8 +117,6 @@ describe('InviteMembersModal', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mock.restore(); }); @@ -134,10 +133,15 @@ describe('InviteMembersModal', () => { `${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${ Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element] }`; - const emitEventFromModal = (eventName) => () => - findModal().vm.$emit(eventName, { preventDefault: jest.fn() }); - const clickInviteButton = emitEventFromModal('primary'); - const clickCancelButton = emitEventFromModal('cancel'); + const findActionButton = () => wrapper.findByTestId('invite-modal-submit'); + const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel'); + + const emitClickFromModal = (findButton) => () => + findButton().vm.$emit('click', { preventDefault: jest.fn() }); + + const clickInviteButton = emitClickFromModal(findActionButton); + const clickCancelButton = emitClickFromModal(findCancelButton); + const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const membersFormGroupInvalidFeedback = () => findMembersFormGroup().attributes('invalid-feedback'); @@ -368,13 +372,11 @@ describe('InviteMembersModal', () => { it('tracks actions', async () => { trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - const mockEvent = { preventDefault: jest.fn() }; - await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL }); expectTracking('render', ON_CELEBRATION_TRACK_LABEL); - findModal().vm.$emit('cancel', mockEvent); + clickCancelButton(); expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL); findModal().vm.$emit('close'); @@ -411,13 +413,11 @@ describe('InviteMembersModal', () => { trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - const mockEvent = { preventDefault: jest.fn() }; - await triggerOpenModal(source); expectTracking('render', label); - findModal().vm.$emit('cancel', mockEvent); + clickCancelButton(); expectTracking('click_cancel', label); findModal().vm.$emit('close'); @@ -734,7 +734,7 @@ describe('InviteMembersModal', () => { expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); - expect(findModal().props('actionPrimary').attributes.loading).toBe(false); + expect(findActionButton().props('loading')).toBe(false); }); it('clears the error when the modal is hidden', async () => { @@ -746,7 +746,7 @@ describe('InviteMembersModal', () => { expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError); expect(findMembersSelect().props('exceptionState')).toBe(false); - expect(findModal().props('actionPrimary').attributes.loading).toBe(false); + expect(findActionButton().props('loading')).toBe(false); findModal().vm.$emit('hidden'); @@ -768,7 +768,7 @@ describe('InviteMembersModal', () => { expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError); expect(membersFormGroupInvalidFeedback()).toBe(''); expect(findMembersSelect().props('exceptionState')).not.toBe(false); - expect(findModal().props('actionPrimary').attributes.loading).toBe(false); + expect(findActionButton().props('loading')).toBe(false); }); it('displays all errors when there are multiple emails that return a restricted error message', async () => { diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js index c522abe63c5..cdb6182e2ae 100644 --- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js @@ -1,12 +1,14 @@ -import { GlButton, GlLink, GlIcon } from '@gitlab/ui'; +import { GlButton, GlLink, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import eventHub from '~/invite_members/event_hub'; import { TRIGGER_ELEMENT_BUTTON, - TRIGGER_ELEMENT_SIDE_NAV, TRIGGER_DEFAULT_QA_SELECTOR, + TRIGGER_ELEMENT_WITH_EMOJI, + TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, } from '~/invite_members/constants'; +import { GlEmoji } from '../mock_data/member_modal'; jest.mock('~/experimentation/experiment_tracking'); @@ -19,7 +21,8 @@ let findButton; const triggerComponent = { button: GlButton, anchor: GlLink, - 'side-nav': GlLink, + 'text-emoji': GlLink, + 'dropdown-text-emoji': GlDropdownItem, }; const createComponent = (props = {}) => { @@ -29,6 +32,9 @@ const createComponent = (props = {}) => { ...triggerProps, ...props, }, + stubs: { + GlEmoji, + }, }); }; @@ -40,8 +46,8 @@ const triggerItems = [ triggerElement: 'anchor', }, { - triggerElement: TRIGGER_ELEMENT_SIDE_NAV, - icon: 'plus', + triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, + icon: 'shaking_hands', }, ]; @@ -50,10 +56,6 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => { findButton = () => wrapper.findComponent(triggerComponent[triggerItem.triggerElement]); - afterEach(() => { - wrapper.destroy(); - }); - describe('configurable attributes', () => { it('includes the correct displayText for the button', () => { createComponent(); @@ -91,31 +93,26 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => { }); }); }); +}); - describe('tracking', () => { - it('does not add tracking attributes', () => { - createComponent(); - - expect(findButton().attributes('data-track-action')).toBeUndefined(); - expect(findButton().attributes('data-track-label')).toBeUndefined(); - }); +describe('link with emoji', () => { + it('includes the specified icon with correct size when triggerElement is link', () => { + const findEmoji = () => wrapper.findComponent(GlEmoji); - it('adds tracking attributes', () => { - createComponent({ label: '_label_', event: '_event_' }); + createComponent({ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, icon: 'shaking_hands' }); - expect(findButton().attributes('data-track-action')).toBe('_event_'); - expect(findButton().attributes('data-track-label')).toBe('_label_'); - }); + expect(findEmoji().exists()).toBe(true); + expect(findEmoji().attributes('data-name')).toBe('shaking_hands'); }); }); -describe('side-nav with icon', () => { +describe('dropdown item with emoji', () => { it('includes the specified icon with correct size when triggerElement is link', () => { - const findIcon = () => wrapper.findComponent(GlIcon); + const findEmoji = () => wrapper.findComponent(GlEmoji); - createComponent({ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus' }); + createComponent({ triggerElement: TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, icon: 'shaking_hands' }); - expect(findIcon().exists()).toBe(true); - expect(findIcon().props('name')).toBe('plus'); + expect(findEmoji().exists()).toBe(true); + expect(findEmoji().attributes('data-name')).toBe('shaking_hands'); }); }); diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js index f34f9902514..e70c83a424e 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -54,10 +54,6 @@ describe('InviteModalBase', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findFormSelect = () => wrapper.findComponent(GlFormSelect); const findFormSelectOptions = () => findFormSelect().findAllComponents('option'); const findDatepicker = () => wrapper.findComponent(GlDatepicker); @@ -66,8 +62,8 @@ describe('InviteModalBase', () => { const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text(); const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const findDisabledInput = () => wrapper.findByTestId('disabled-input'); - const findCancelButton = () => wrapper.find('.js-modal-action-cancel'); - const findActionButton = () => wrapper.find('.js-modal-action-primary'); + const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel'); + const findActionButton = () => wrapper.findByTestId('invite-modal-submit'); describe('rendering the modal', () => { let trackingSpy; @@ -88,20 +84,19 @@ describe('InviteModalBase', () => { }); it('renders the Cancel button text correctly', () => { - expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({ - text: CANCEL_BUTTON_TEXT, - }); + expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT); }); it('renders the Invite button correctly', () => { - expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({ - text: INVITE_BUTTON_TEXT, - attributes: { - variant: 'confirm', - disabled: false, - loading: false, - 'data-qa-selector': 'invite_button', - }, + const actionButton = findActionButton(); + + expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT); + expect(actionButton.attributes('data-qa-selector')).toBe('invite_button'); + + expect(actionButton.props()).toMatchObject({ + variant: 'confirm', + disabled: false, + loading: false, }); }); @@ -235,7 +230,7 @@ describe('InviteModalBase', () => { }, }); - expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true); + expect(findActionButton().props('loading')).toBe(true); }); it('with invalidFeedbackMessage, set members form group exception state', () => { 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 0455460918c..c7e9905dee3 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -30,11 +30,6 @@ const createComponent = (props) => { describe('MembersTokenSelect', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); describe('rendering the token-selector component', () => { diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js index 6fbf95362fa..20db4f20408 100644 --- a/spec/frontend/invite_members/components/project_select_spec.js +++ b/spec/frontend/invite_members/components/project_select_spec.js @@ -23,10 +23,6 @@ describe('ProjectSelect', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); const findAvatarLabeled = (index) => wrapper.findAllComponents(GlAvatarLabeled).at(index); diff --git a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js index 38b16dd0c2c..fd011658f95 100644 --- a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js +++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js @@ -6,10 +6,10 @@ import { TOAST_MESSAGE_LOCALSTORAGE_KEY, TOAST_MESSAGE_SUCCESSFUL, } from '~/invite_members/constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; -jest.mock('~/flash'); +jest.mock('~/alert'); useLocalStorageSpy(); describe('Display Successful Invitation Alert', () => { diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js index f798f87b6b2..ccd53e64c4d 100644 --- a/spec/frontend/issuable/components/csv_export_modal_spec.js +++ b/spec/frontend/issuable/components/csv_export_modal_spec.js @@ -17,7 +17,7 @@ describe('CsvExportModal', () => { ...props, }, provide: { - issuableType: 'issues', + issuableType: 'issue', ...injectedProperties, }, stubs: { @@ -29,19 +29,15 @@ describe('CsvExportModal', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findModal = () => wrapper.findComponent(GlModal); const findIcon = () => wrapper.findComponent(GlIcon); describe('template', () => { describe.each` - issuableType | modalTitle - ${'issues'} | ${'Export issues'} - ${'merge-requests'} | ${'Export merge requests'} - `('with the issuableType "$issuableType"', ({ issuableType, modalTitle }) => { + issuableType | modalTitle | dataTrackLabel + ${'issue'} | ${'Export issues'} | ${'export_issues_csv'} + ${'merge_request'} | ${'Export merge requests'} | ${'export_merge-requests_csv'} + `('with the issuableType "$issuableType"', ({ issuableType, modalTitle, dataTrackLabel }) => { beforeEach(() => { wrapper = createComponent({ injectedProperties: { issuableType } }); }); @@ -57,9 +53,9 @@ describe('CsvExportModal', () => { href: 'export/csv/path', variant: 'confirm', 'data-method': 'post', - 'data-qa-selector': `export_${issuableType}_button`, + 'data-qa-selector': `export_issues_button`, 'data-track-action': 'click_button', - 'data-track-label': `export_${issuableType}_csv`, + 'data-track-label': dataTrackLabel, }, }); }); diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js index 118c12d968b..a861148abb6 100644 --- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js +++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js @@ -16,7 +16,7 @@ describe('CsvImportExportButtons', () => { glModalDirective = jest.fn(); return mountExtended(CsvImportExportButtons, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), glModal: { bind(_, { value }) { glModalDirective(value); @@ -33,10 +33,6 @@ describe('CsvImportExportButtons', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findExportCsvButton = () => wrapper.findComponent(GlButton); const findImportDropdown = () => wrapper.findComponent(GlDropdown); const findImportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Import CSV' }); diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js index 6e954c91f46..9069d2b3ab3 100644 --- a/spec/frontend/issuable/components/csv_import_modal_spec.js +++ b/spec/frontend/issuable/components/csv_import_modal_spec.js @@ -32,10 +32,6 @@ describe('CsvImportModal', () => { formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findModal = () => wrapper.findComponent(GlModal); const findForm = () => wrapper.find('form'); const findFileInput = () => wrapper.findByLabelText('Upload CSV file'); diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js index b04a6c0b8fd..4cc5775b54e 100644 --- a/spec/frontend/issuable/components/issuable_by_email_spec.js +++ b/spec/frontend/issuable/components/issuable_by_email_spec.js @@ -53,8 +53,6 @@ describe('IssuableByEmail', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mockAxios.restore(); }); diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js index 99aa6778e1e..ff772040d22 100644 --- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js +++ b/spec/frontend/issuable/components/issuable_header_warnings_spec.js @@ -25,16 +25,11 @@ describe('IssuableHeaderWarnings', () => { store, provide, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` issuableType ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR} diff --git a/spec/frontend/issuable/components/issue_assignees_spec.js b/spec/frontend/issuable/components/issue_assignees_spec.js index 9a33bfae240..8ed51120508 100644 --- a/spec/frontend/issuable/components/issue_assignees_spec.js +++ b/spec/frontend/issuable/components/issue_assignees_spec.js @@ -21,11 +21,6 @@ describe('IssueAssigneesComponent', () => { vm = wrapper.vm; }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text(); const findAvatars = () => wrapper.findAllComponents(UserAvatarLink); const findOverflowCounter = () => wrapper.find('.avatar-counter'); diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js index eac53c5f761..232d6177862 100644 --- a/spec/frontend/issuable/components/issue_milestone_spec.js +++ b/spec/frontend/issuable/components/issue_milestone_spec.js @@ -1,160 +1,61 @@ -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltip } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; - import { mockMilestone } from 'jest/boards/mock_data'; import IssueMilestone from '~/issuable/components/issue_milestone.vue'; -const createComponent = (milestone = mockMilestone) => { - const Component = Vue.extend(IssueMilestone); - - return shallowMount(Component, { - propsData: { - milestone, - }, - }); -}; - -describe('IssueMilestoneComponent', () => { +describe('IssueMilestone component', () => { let wrapper; - let vm; - beforeEach(async () => { - wrapper = createComponent(); + const findTooltip = () => wrapper.findComponent(GlTooltip); - ({ vm } = wrapper); + const createComponent = (milestone = mockMilestone) => + shallowMount(IssueMilestone, { propsData: { milestone } }); - await nextTick(); + beforeEach(() => { + wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); + it('renders milestone icon', () => { + expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock'); }); - describe('computed', () => { - describe('isMilestoneStarted', () => { - it('should return `false` when milestoneStart prop is not defined', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.isMilestoneStarted).toBe(false); - }); - - it('should return `true` when milestone start date is past current date', async () => { - await wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '1990-07-22' }, - }); - await nextTick(); + it('renders milestone title', () => { + expect(wrapper.find('.milestone-title').text()).toBe(mockMilestone.title); + }); - expect(wrapper.vm.isMilestoneStarted).toBe(true); - }); + describe('tooltip', () => { + it('renders `Milestone`', () => { + expect(findTooltip().text()).toContain('Milestone'); }); - describe('isMilestonePastDue', () => { - it('should return `false` when milestoneDue prop is not defined', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.isMilestonePastDue).toBe(false); - }); - - it('should return `true` when milestone due is past current date', () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '1990-07-22' }, - }); - - expect(wrapper.vm.isMilestonePastDue).toBe(true); - }); + it('renders milestone title', () => { + expect(findTooltip().text()).toContain(mockMilestone.title); }); - describe('milestoneDatesAbsolute', () => { - it('returns string containing absolute milestone due date', () => { - expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)'); - }); + describe('humanized dates', () => { + it('renders `Expired` when there is a due date in the past', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' }); - it('returns string containing absolute milestone start date when due date is not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)'); + expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)'); }); - it('returns empty string when both milestone start and due dates are not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '', due_date: '' }, - }); - await nextTick(); + it('renders `remaining` when there is a due date in the future', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' }); - expect(wrapper.vm.milestoneDatesAbsolute).toBe(''); + expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)'); }); - }); - describe('milestoneDatesHuman', () => { - it('returns string containing milestone due date when date is yet to be due', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` }, - }); - await nextTick(); + it('renders `Started` when there is a start date in the past', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' }); - expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining'); + expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)'); }); - it('returns string containing milestone start date when date has already started and due date is not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' }, - }); - await nextTick(); + it('renders `Starts` when there is a start date in the future', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' }); - expect(wrapper.vm.milestoneDatesHuman).toContain('Started'); + expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)'); }); - - it('returns string containing milestone start date when date is yet to start and due date is not present', async () => { - wrapper.setProps({ - milestone: { - ...mockMilestone, - start_date: `${new Date().getFullYear() + 10}-01-01`, - due_date: '', - }, - }); - await nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toContain('Starts'); - }); - - it('returns empty string when milestone start and due dates are not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '', due_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toBe(''); - }); - }); - }); - - describe('template', () => { - it('renders component root element with class `issue-milestone-details`', () => { - expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true); - }); - - it('renders milestone icon', () => { - expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock'); - }); - - it('renders milestone title', () => { - expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title); - }); - - it('renders milestone tooltip', () => { - expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain( - mockMilestone.title, - ); }); }); }); diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js index 3f9f048605a..3e23558ceb4 100644 --- a/spec/frontend/issuable/components/related_issuable_item_spec.js +++ b/spec/frontend/issuable/components/related_issuable_item_spec.js @@ -53,10 +53,6 @@ describe('RelatedIssuableItem', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('contains issuable-info-container class when canReorder is false', () => { mountComponent({ props: { canReorder: false } }); diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js index 728b8958b9b..d26f287d90c 100644 --- a/spec/frontend/issuable/components/status_box_spec.js +++ b/spec/frontend/issuable/components/status_box_spec.js @@ -11,11 +11,6 @@ function factory(propsData) { describe('Merge request status box component', () => { const findBadge = () => wrapper.findComponent(GlBadge); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon ${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'} diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js index 444165f61c7..a7605016039 100644 --- a/spec/frontend/issuable/popover/components/issue_popover_spec.js +++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js @@ -33,10 +33,6 @@ describe('Issue Popover', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('shows skeleton-loader while apollo is loading', () => { mountComponent(); diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js index 5fdd1e6e8fc..d9e113eeaae 100644 --- a/spec/frontend/issuable/popover/components/mr_popover_spec.js +++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js @@ -71,10 +71,6 @@ describe('MR Popover', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('shows skeleton-loader while apollo is loading', () => { mountComponent(); 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 72fcab63ba7..f8e47bc0a4b 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 @@ -1,9 +1,9 @@ -import { GlFormGroup } from '@gitlab/ui'; +import { GlButton, GlFormGroup, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue'; import IssueToken from '~/related_issues/components/issue_token.vue'; +import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue'; import { linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants'; const issuable1 = { @@ -26,71 +26,60 @@ const issuable2 = { const pathIdSeparator = PathIdSeparator.Issue; -const findFormInput = (wrapper) => wrapper.find('input').element; - -const findRadioInput = (inputs, value) => - inputs.filter((input) => input.element.value === value)[0]; - -const findRadioInputs = (wrapper) => wrapper.findAll('[name="linked-issue-type-radio"]'); - -const constructWrapper = (props) => { - return shallowMount(AddIssuableForm, { - propsData: { - inputValue: '', - pendingReferences: [], - pathIdSeparator, - ...props, - }, - }); -}; - describe('AddIssuableForm', () => { let wrapper; - afterEach(() => { - // Jest doesn't blur an item even if it is destroyed, - // so blur the input manually after each test - const input = findFormInput(wrapper); - if (input) input.blur(); + const createComponent = (props = {}, mountFn = shallowMount) => { + wrapper = mountFn(AddIssuableForm, { + propsData: { + inputValue: '', + pendingReferences: [], + pathIdSeparator, + ...props, + }, + stubs: { + RelatedIssuableInput, + }, + }); + }; - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); + const findAddIssuableForm = () => wrapper.find('form'); + const findFormInput = () => wrapper.find('input').element; + const findRadioInput = (inputs, value) => + inputs.filter((input) => input.element.value === value)[0]; + const findAllIssueTokens = () => wrapper.findAllComponents(IssueToken); + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + const findRadioInputs = () => wrapper.findAllComponents(GlFormRadio); + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findFormButtons = () => wrapper.findAllComponents(GlButton); + const findSubmitButton = () => findFormButtons().at(0); + const findRelatedIssuableInput = () => wrapper.findComponent(RelatedIssuableInput); describe('with data', () => { describe('without references', () => { describe('without any input text', () => { beforeEach(() => { - wrapper = shallowMount(AddIssuableForm, { - propsData: { - inputValue: '', - pendingReferences: [], - pathIdSeparator, - }, - }); + createComponent(); }); it('should have disabled submit button', () => { - expect(wrapper.vm.$refs.addButton.disabled).toBe(true); - expect(wrapper.vm.$refs.loadingIcon).toBeUndefined(); + expect(findSubmitButton().props('disabled')).toBe(true); + expect(findSubmitButton().props('loading')).toBe(false); }); }); describe('with input text', () => { beforeEach(() => { - wrapper = shallowMount(AddIssuableForm, { - propsData: { - inputValue: 'foo', - pendingReferences: [], - pathIdSeparator, - }, + createComponent({ + inputValue: 'foo', + pendingReferences: [], + pathIdSeparator, }); }); it('should not have disabled submit button', () => { - expect(wrapper.vm.$refs.addButton.disabled).toBe(false); + expect(findSubmitButton().props('disabled')).toBe(false); }); }); }); @@ -99,59 +88,56 @@ describe('AddIssuableForm', () => { const inputValue = 'foo #123'; beforeEach(() => { - wrapper = mount(AddIssuableForm, { - propsData: { - inputValue, - pendingReferences: [issuable1.reference, issuable2.reference], - pathIdSeparator, - }, + createComponent({ + inputValue, + pendingReferences: [issuable1.reference, issuable2.reference], + pathIdSeparator, }); - }); + }, mount); it('should put input value in place', () => { expect(findFormInput(wrapper).value).toBe(inputValue); }); it('should render pending issuables items', () => { - expect(wrapper.findAllComponents(IssueToken)).toHaveLength(2); + expect(findAllIssueTokens()).toHaveLength(2); }); it('should not have disabled submit button', () => { - expect(wrapper.vm.$refs.addButton.disabled).toBe(false); + expect(findSubmitButton().props('disabled')).toBe(false); }); }); describe('when issuable type is "issue"', () => { beforeEach(() => { - wrapper = mount(AddIssuableForm, { - propsData: { + createComponent( + { inputValue: '', issuableType: TYPE_ISSUE, pathIdSeparator, pendingReferences: [], }, - }); + mount, + ); }); it('does not show radio inputs', () => { - expect(findRadioInputs(wrapper).length).toBe(0); + expect(findRadioInputs()).toHaveLength(0); }); }); describe('when issuable type is "epic"', () => { beforeEach(() => { - wrapper = shallowMount(AddIssuableForm, { - propsData: { - inputValue: '', - issuableType: TYPE_EPIC, - pathIdSeparator, - pendingReferences: [], - }, + createComponent({ + inputValue: '', + issuableType: TYPE_EPIC, + pathIdSeparator, + pendingReferences: [], }); }); it('does not show radio inputs', () => { - expect(findRadioInputs(wrapper).length).toBe(0); + expect(findRadioInputs()).toHaveLength(0); }); }); @@ -163,17 +149,15 @@ describe('AddIssuableForm', () => { `( 'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType', ({ issuableType, contextHeader, contextFooter }) => { - wrapper = shallowMount(AddIssuableForm, { - propsData: { - issuableType, - inputValue: '', - showCategorizedIssues: true, - pathIdSeparator, - pendingReferences: [], - }, + createComponent({ + issuableType, + inputValue: '', + showCategorizedIssues: true, + pathIdSeparator, + pendingReferences: [], }); - expect(wrapper.findComponent(GlFormGroup).attributes('label')).toBe(contextHeader); + expect(findFormGroup().attributes('label')).toBe(contextHeader); expect(wrapper.find('p.bold').text()).toContain(contextFooter); }, ); @@ -181,26 +165,24 @@ describe('AddIssuableForm', () => { describe('when it is a Linked Issues form', () => { beforeEach(() => { - wrapper = mount(AddIssuableForm, { - propsData: { - inputValue: '', - showCategorizedIssues: true, - issuableType: TYPE_ISSUE, - pathIdSeparator, - pendingReferences: [], - }, + createComponent({ + inputValue: '', + showCategorizedIssues: true, + issuableType: TYPE_ISSUE, + pathIdSeparator, + pendingReferences: [], }); }); it('shows radio inputs to allow categorisation of blocking issues', () => { - expect(findRadioInputs(wrapper).length).toBeGreaterThan(0); + expect(findRadioGroup().props('options').length).toBeGreaterThan(0); }); describe('form radio buttons', () => { let radioInputs; beforeEach(() => { - radioInputs = findRadioInputs(wrapper); + radioInputs = findRadioInputs(); }); it('shows "relates to" option', () => { @@ -216,58 +198,59 @@ describe('AddIssuableForm', () => { }); it('shows 3 options in total', () => { - expect(radioInputs.length).toBe(3); + expect(findRadioGroup().props('options')).toHaveLength(3); }); }); describe('when the form is submitted', () => { - it('emits an event with a "relates_to" link type when the "relates to" radio input selected', async () => { - jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); - - wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO; - wrapper.vm.onFormSubmit(); - - await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', { - pendingReferences: '', - linkedIssueType: linkedIssueTypesMap.RELATES_TO, - }); + it('emits an event with a "relates_to" link type when the "relates to" radio input selected', () => { + findAddIssuableForm().trigger('submit'); + + expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([ + [ + { + pendingReferences: '', + linkedIssueType: linkedIssueTypesMap.RELATES_TO, + }, + ], + ]); }); - it('emits an event with a "blocks" link type when the "blocks" radio input selected', async () => { - jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); - - wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS; - wrapper.vm.onFormSubmit(); - - await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', { - pendingReferences: '', - linkedIssueType: linkedIssueTypesMap.BLOCKS, - }); + it('emits an event with a "blocks" link type when the "blocks" radio input selected', () => { + findRadioGroup().vm.$emit('input', linkedIssueTypesMap.BLOCKS); + findAddIssuableForm().trigger('submit'); + + expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([ + [ + { + pendingReferences: '', + linkedIssueType: linkedIssueTypesMap.BLOCKS, + }, + ], + ]); }); it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', async () => { - jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); - - wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY; - wrapper.vm.onFormSubmit(); - - await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', { - pendingReferences: '', - linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY, - }); + findRadioGroup().vm.$emit('input', linkedIssueTypesMap.IS_BLOCKED_BY); + findAddIssuableForm().trigger('submit'); + + expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([ + [ + { + pendingReferences: '', + linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY, + }, + ], + ]); }); - it('shows error message when error is present', async () => { + it('shows error message when error is present', () => { const itemAddFailureMessage = 'Something went wrong while submitting.'; - wrapper.setProps({ + createComponent({ hasError: true, itemAddFailureMessage, }); - await nextTick(); expect(wrapper.find('.gl-field-error').exists()).toBe(true); expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage); }); @@ -283,27 +266,31 @@ describe('AddIssuableForm', () => { }; it('returns autocomplete object', () => { - wrapper = constructWrapper({ + createComponent({ autoCompleteSources, }); - expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources); + expect(findRelatedIssuableInput().props('autoCompleteSources')).toEqual( + autoCompleteSources, + ); - wrapper = constructWrapper({ + createComponent({ autoCompleteSources, confidential: false, }); - expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources); + expect(findRelatedIssuableInput().props('autoCompleteSources')).toEqual( + autoCompleteSources, + ); }); it('returns autocomplete sources with query `confidential_only`, when it is confidential', () => { - wrapper = constructWrapper({ + createComponent({ autoCompleteSources, confidential: true, }); - const actualSources = wrapper.vm.transformedAutocompleteSources; + const actualSources = findRelatedIssuableInput().props('autoCompleteSources'); expect(actualSources.epics).toContain('?confidential_only=true'); expect(actualSources.issues).toContain('?confidential_only=true'); 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 ff8d5073005..b9580b90c12 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,5 @@ import { nextTick } from 'vue'; -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlCard } from '@gitlab/ui'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { issuable1, @@ -78,6 +78,9 @@ describe('RelatedIssuesBlock', () => { pathIdSeparator: PathIdSeparator.Issue, issuableType: 'issue', }, + stubs: { + GlCard, + }, slots: { 'header-text': headerText }, }); @@ -94,6 +97,9 @@ describe('RelatedIssuesBlock', () => { pathIdSeparator: PathIdSeparator.Issue, issuableType: 'issue', }, + stubs: { + GlCard, + }, slots: { 'header-actions': headerActions }, }); @@ -222,6 +228,9 @@ describe('RelatedIssuesBlock', () => { pathIdSeparator: PathIdSeparator.Issue, issuableType, }, + stubs: { + GlCard, + }, }); const iconComponent = wrapper.findComponent(GlIcon); @@ -239,6 +248,9 @@ describe('RelatedIssuesBlock', () => { relatedIssues: [issuable1, issuable2, issuable3], issuableType: TYPE_ISSUE, }, + stubs: { + GlCard, + }, }); }); 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 96c0b87e2cb..1383013aedb 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 @@ -7,7 +7,7 @@ import { issuable1, issuable2, } from 'jest/issuable/components/related_issuable_mock_data'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_CONFLICT, @@ -19,7 +19,7 @@ import RelatedIssuesBlock from '~/related_issues/components/related_issues_block import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue'; import relatedIssuesService from '~/related_issues/services/related_issues_service'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('RelatedIssuesRoot', () => { let wrapper; @@ -34,7 +34,6 @@ describe('RelatedIssuesRoot', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); const createComponent = ({ props = {}, data = {} } = {}) => { diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js index cc2ee84348a..21ae844e2dd 100644 --- a/spec/frontend/issues/create_merge_request_dropdown_spec.js +++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js @@ -65,6 +65,14 @@ describe('CreateMergeRequestDropdown', () => { expect(dropdown.createMrPath).toBe( `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`, ); + + expect(dropdown.wrapperEl.dataset.createBranchPath).toBe( + `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`, + ); + + expect(dropdown.wrapperEl.dataset.createMrPath).toBe( + `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`, + ); }); }); diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js index 77d5a0579a4..ebf4771e97f 100644 --- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js +++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js @@ -18,6 +18,7 @@ import { setSortPreferenceMutationResponse, setSortPreferenceMutationResponseWithErrors, } from 'jest/issues/list/mock_data'; +import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue'; import getIssuesCountsQuery from '~/issues/dashboard/queries/get_issues_counts.query.graphql'; import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants'; @@ -36,7 +37,6 @@ import { TOKEN_TYPE_TYPE, } from '~/vue_shared/components/filtered_search_bar/constants'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; -import { IssuableStates } from '~/vue_shared/issuable/list/constants'; import { emptyIssuesQueryResponse, issuesCountsQueryResponse, @@ -124,7 +124,7 @@ describe('IssuesDashboardApp component', () => { // eslint-disable-next-line jest/no-disabled-tests it.skip('renders IssuableList component', () => { expect(findIssuableList().props()).toMatchObject({ - currentTab: IssuableStates.Opened, + currentTab: STATUS_OPEN, hasNextPage: true, hasPreviousPage: false, hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature, @@ -148,7 +148,7 @@ describe('IssuesDashboardApp component', () => { tabs: IssuesDashboardApp.IssuableListTabs, urlParams: { sort: urlSortParams[CREATED_DESC], - state: IssuableStates.Opened, + state: STATUS_OPEN, }, useKeysetPagination: true, }); @@ -283,7 +283,7 @@ describe('IssuesDashboardApp component', () => { describe('state', () => { it('is set from the url params', () => { - const initialState = IssuableStates.All; + const initialState = STATUS_ALL; setWindowLocation(`?state=${initialState}`); mountComponent(); @@ -337,11 +337,9 @@ describe('IssuesDashboardApp component', () => { username: 'root', avatar_url: 'avatar/url', }; - const originalGon = window.gon; beforeEach(() => { window.gon = { - ...originalGon, current_user_id: mockCurrentUser.id, current_user_fullname: mockCurrentUser.name, current_username: mockCurrentUser.username, @@ -350,10 +348,6 @@ describe('IssuesDashboardApp component', () => { mountComponent(); }); - afterEach(() => { - window.gon = originalGon; - }); - it('renders all tokens alphabetically', () => { const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }]; @@ -375,16 +369,16 @@ describe('IssuesDashboardApp component', () => { beforeEach(() => { mountComponent(); - findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); + findIssuableList().vm.$emit('click-tab', STATUS_CLOSED); }); it('updates ui to the new tab', () => { - expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); + expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED); }); it('updates url to the new tab', () => { expect(findIssuableList().props('urlParams')).toMatchObject({ - state: IssuableStates.Closed, + state: STATUS_CLOSED, }); }); }); diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js index ab4d023ee39..e80ffea0591 100644 --- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js +++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js @@ -45,10 +45,6 @@ describe('CE IssueCardTimeInfo component', () => { }, }); - afterEach(() => { - wrapper.destroy(); - }); - describe('milestone', () => { it('renders', () => { wrapper = mountComponent(); 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 8281ce0ed1a..b28a08e2fce 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -15,19 +15,21 @@ import waitForPromises from 'helpers/wait_for_promises'; import { getIssuesCountsQueryResponse, getIssuesQueryResponse, + getIssuesQueryEmptyResponse, filteredTokens, locationSearch, setSortPreferenceMutationResponse, setSortPreferenceMutationResponseWithErrors, urlParams, } from 'jest/issues/list/mock_data'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_INFO } from '~/alert'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; -import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; +import { IssuableListTabs } from '~/vue_shared/issuable/list/constants'; import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue'; import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; @@ -70,7 +72,7 @@ import('~/issuable'); import('~/users_select'); jest.mock('@sentry/browser'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() })); describe('CE IssuesListApp component', () => { @@ -154,7 +156,24 @@ describe('CE IssuesListApp component', () => { router = new VueRouter({ mode: 'history' }); return mountFn(IssuesListApp, { - apolloProvider: createMockApollo(requestHandlers), + apolloProvider: createMockApollo( + requestHandlers, + {}, + { + typePolicies: { + Query: { + fields: { + project: { + merge: true, + }, + group: { + merge: true, + }, + }, + }, + }, + }, + ), router, provide: { ...defaultProvide, @@ -174,13 +193,11 @@ describe('CE IssuesListApp component', () => { afterEach(() => { axiosMock.reset(); - wrapper.destroy(); }); describe('IssuableList', () => { beforeEach(() => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -197,7 +214,7 @@ describe('CE IssuesListApp component', () => { initialSortBy: CREATED_DESC, issuables: getIssuesQueryResponse.data.project.issues.nodes, tabs: IssuableListTabs, - currentTab: IssuableStates.Opened, + currentTab: STATUS_OPEN, tabCounts: { opened: 1, closed: 1, @@ -247,7 +264,6 @@ describe('CE IssuesListApp component', () => { mountFn: mount, }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -416,7 +432,7 @@ describe('CE IssuesListApp component', () => { describe('state', () => { it('is set from the url params', () => { - const initialState = IssuableStates.All; + const initialState = STATUS_ALL; setWindowLocation(`?state=${initialState}`); wrapper = mountComponent(); @@ -477,7 +493,12 @@ describe('CE IssuesListApp component', () => { describe('empty states', () => { describe('when there are issues', () => { beforeEach(() => { - wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); + wrapper = mountComponent({ + provide: { hasAnyIssues: true }, + mountFn: mount, + issuesQueryResponse: getIssuesQueryEmptyResponse, + }); + return waitForPromises(); }); it('shows EmptyStateWithAnyIssues empty state', () => { @@ -543,11 +564,8 @@ describe('CE IssuesListApp component', () => { }); describe('when all tokens are available', () => { - const originalGon = window.gon; - beforeEach(() => { window.gon = { - ...originalGon, current_user_id: mockCurrentUser.id, current_user_fullname: mockCurrentUser.name, current_username: mockCurrentUser.username, @@ -563,10 +581,6 @@ describe('CE IssuesListApp component', () => { }); }); - afterEach(() => { - window.gon = originalGon; - }); - it('renders all tokens alphabetically', () => { const preloadedUsers = [ { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) }, @@ -599,7 +613,6 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -620,20 +633,21 @@ describe('CE IssuesListApp component', () => { describe('events', () => { describe('when "click-tab" event is emitted by IssuableList', () => { - beforeEach(() => { + beforeEach(async () => { wrapper = mountComponent(); + await waitForPromises(); router.push = jest.fn(); - findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); + findIssuableList().vm.$emit('click-tab', STATUS_CLOSED); }); it('updates ui to the new tab', () => { - expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); + expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED); }); it('updates url to the new tab', () => { expect(router.push).toHaveBeenCalledWith({ - query: expect.objectContaining({ state: IssuableStates.Closed }), + query: expect.objectContaining({ state: STATUS_CLOSED }), }); }); }); @@ -641,19 +655,25 @@ describe('CE IssuesListApp component', () => { describe.each` event | params ${'next-page'} | ${{ - page_after: 'endCursor', + page_after: 'endcursor', page_before: undefined, first_page_size: 20, last_page_size: undefined, + search: undefined, + sort: 'created_date', + state: 'opened', }} ${'previous-page'} | ${{ page_after: undefined, - page_before: 'startCursor', + page_before: 'startcursor', first_page_size: undefined, last_page_size: 20, + search: undefined, + sort: 'created_date', + state: 'opened', }} `('when "$event" event is emitted by IssuableList', ({ event, params }) => { - beforeEach(() => { + beforeEach(async () => { wrapper = mountComponent({ data: { pageInfo: { @@ -662,6 +682,7 @@ describe('CE IssuesListApp component', () => { }, }, }); + await waitForPromises(); router.push = jest.fn(); findIssuableList().vm.$emit(event); @@ -735,7 +756,6 @@ describe('CE IssuesListApp component', () => { provide: { isProject }, issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -761,7 +781,6 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response()), }); - jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -793,8 +812,6 @@ describe('CE IssuesListApp component', () => { router.push = jest.fn(); findIssuableList().vm.$emit('sort', sortKey); - jest.runOnlyPendingTimers(); - await nextTick(); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ sort: urlSortParams[sortKey] }), @@ -914,13 +931,13 @@ describe('CE IssuesListApp component', () => { ${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false} ${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true} ${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false} - `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => { + `('$description', async ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => { const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse); wrapper = mountComponent({ provide: { isPublicVisibilityRestricted, isSignedIn }, issuesQueryResponse: mockQuery, }); - jest.runOnlyPendingTimers(); + await waitForPromises(); expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers })); }); @@ -929,7 +946,6 @@ describe('CE IssuesListApp component', () => { describe('fetching issues', () => { beforeEach(() => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); }); it('fetches issue, incident, test case, and task types', () => { diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js index 406b1fbc1af..7bbb5a954ae 100644 --- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js +++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js @@ -38,11 +38,6 @@ describe('JiraIssuesImportStatus', () => { }, }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when Jira import is neither in progress nor finished', () => { beforeEach(() => { wrapper = mountComponent(); diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index 1e8a81116f3..0332f68ddb6 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -101,6 +101,26 @@ export const getIssuesQueryResponse = { }, }; +export const getIssuesQueryEmptyResponse = { + data: { + project: { + id: '1', + __typename: 'Project', + issues: { + __persist: true, + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'startcursor', + endCursor: 'endcursor', + }, + nodes: [], + }, + }, + }, +}; + export const getIssuesCountsQueryResponse = { data: { project: { diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js index a281ed1c989..e4ecdc6c29e 100644 --- a/spec/frontend/issues/list/utils_spec.js +++ b/spec/frontend/issues/list/utils_spec.js @@ -10,7 +10,7 @@ import { urlParams, urlParamsWithSpecialValues, } from 'jest/issues/list/mock_data'; -import { PAGE_SIZE, urlSortParams } from '~/issues/list/constants'; +import { urlSortParams } from '~/issues/list/constants'; import { convertToApiParams, convertToSearchQuery, @@ -22,10 +22,11 @@ import { isSortKey, } from '~/issues/list/utils'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants'; describe('getInitialPageParams', () => { it('returns page params with a default page size when no arguments are given', () => { - expect(getInitialPageParams()).toEqual({ firstPageSize: PAGE_SIZE }); + expect(getInitialPageParams()).toEqual({ firstPageSize: DEFAULT_PAGE_SIZE }); }); it('returns page params with the given page size', () => { diff --git a/spec/frontend/issues/new/components/title_suggestions_item_spec.js b/spec/frontend/issues/new/components/title_suggestions_item_spec.js index c54a762440f..4454ef81416 100644 --- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js +++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js @@ -25,10 +25,6 @@ describe('Issue title suggestions item component', () => { const findTooltip = () => wrapper.findComponent(GlTooltip); const findUserAvatar = () => wrapper.findComponent(UserAvatarImage); - afterEach(() => { - wrapper.destroy(); - }); - it('renders title', () => { createComponent(); diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js index 1cd6576967a..343bdbba301 100644 --- a/spec/frontend/issues/new/components/title_suggestions_spec.js +++ b/spec/frontend/issues/new/components/title_suggestions_spec.js @@ -1,106 +1,95 @@ import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import TitleSuggestions from '~/issues/new/components/title_suggestions.vue'; import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue'; +import getIssueSuggestionsQuery from '~/issues/new/queries/issues.query.graphql'; +import { mockIssueSuggestionResponse } from '../mock_data'; + +Vue.use(VueApollo); + +const MOCK_PROJECT_PATH = 'project'; +const MOCK_ISSUES_COUNT = mockIssueSuggestionResponse.data.project.issues.edges.length; describe('Issue title suggestions component', () => { let wrapper; + let mockApollo; + + function createComponent({ + search = 'search', + queryResponse = jest.fn().mockResolvedValue(mockIssueSuggestionResponse), + } = {}) { + mockApollo = createMockApollo([[getIssueSuggestionsQuery, queryResponse]]); - function createComponent(search = 'search') { wrapper = shallowMount(TitleSuggestions, { propsData: { search, - projectPath: 'project', + projectPath: MOCK_PROJECT_PATH, }, + apolloProvider: mockApollo, }); } - beforeEach(() => { - createComponent(); - }); + const waitForDebounce = () => { + jest.runOnlyPendingTimers(); + return waitForPromises(); + }; afterEach(() => { - wrapper.destroy(); + mockApollo = null; }); it('does not render with empty search', async () => { - wrapper.setProps({ search: '' }); + createComponent({ search: '' }); + await waitForDebounce(); - await nextTick(); expect(wrapper.isVisible()).toBe(false); }); - describe('with data', () => { - let data; - - beforeEach(() => { - data = { issues: [{ id: 1 }, { id: 2 }] }; - }); - - it('renders component', 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(data); - - await nextTick(); - expect(wrapper.findAll('li').length).toBe(data.issues.length); - }); + it('does not render when loading', () => { + createComponent(); + expect(wrapper.isVisible()).toBe(false); + }); - it('does not render with empty search', async () => { - wrapper.setProps({ search: '' }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData(data); + it('does not render with empty issues data', async () => { + const emptyIssuesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + issues: { + edges: [], + }, + }, + }, + }; - await nextTick(); - expect(wrapper.isVisible()).toBe(false); - }); + createComponent({ queryResponse: jest.fn().mockResolvedValue(emptyIssuesResponse) }); + await waitForDebounce(); - it('does not render when loading', 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({ - ...data, - loading: 1, - }); + expect(wrapper.isVisible()).toBe(false); + }); - await nextTick(); - expect(wrapper.isVisible()).toBe(false); + describe('with data', () => { + beforeEach(async () => { + createComponent(); + await waitForDebounce(); }); - it('does not render with empty issues data', 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({ issues: [] }); - - await nextTick(); - expect(wrapper.isVisible()).toBe(false); + it('renders component', () => { + expect(wrapper.findAll('li').length).toBe(MOCK_ISSUES_COUNT); }); - it('renders list of issues', 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(data); - - await nextTick(); - expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(2); + it('renders list of issues', () => { + expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(MOCK_ISSUES_COUNT); }); - it('adds margin class to first item', 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(data); - - await nextTick(); + it('adds margin class to first item', () => { expect(wrapper.findAll('li').at(0).classes()).toContain('gl-mb-3'); }); - it('does not add margin class to last item', 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(data); - - await nextTick(); + it('does not add margin class to last item', () => { expect(wrapper.findAll('li').at(1).classes()).not.toContain('gl-mb-3'); }); }); diff --git a/spec/frontend/issues/new/components/type_popover_spec.js b/spec/frontend/issues/new/components/type_popover_spec.js index fe3d5207516..1ae150797c3 100644 --- a/spec/frontend/issues/new/components/type_popover_spec.js +++ b/spec/frontend/issues/new/components/type_popover_spec.js @@ -8,10 +8,6 @@ describe('Issue type info popover', () => { wrapper = shallowMount(TypePopover); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { createComponent(); diff --git a/spec/frontend/issues/new/mock_data.js b/spec/frontend/issues/new/mock_data.js index 74b569d9833..0d2a388cd86 100644 --- a/spec/frontend/issues/new/mock_data.js +++ b/spec/frontend/issues/new/mock_data.js @@ -26,3 +26,67 @@ export default () => ({ webUrl: `${TEST_HOST}/author`, }, }); + +export const mockIssueSuggestionResponse = { + data: { + project: { + id: 'gid://gitlab/Project/278964', + issues: { + edges: [ + { + node: { + id: 'gid://gitlab/Issue/123725957', + iid: '696', + title: 'Remove unused MR widget extension expand success, failed, warning events', + confidential: false, + userNotesCount: 16, + upvotes: 0, + webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/696', + state: 'opened', + closedAt: null, + createdAt: '2023-02-15T12:29:59Z', + updatedAt: '2023-03-01T19:38:22Z', + author: { + id: 'gid://gitlab/User/325', + name: 'User Name', + username: 'user-name', + avatarUrl: '/uploads/-/system/user/avatar/325/avatar.png', + webUrl: 'https://gitlab.com/user-name', + __typename: 'UserCore', + }, + __typename: 'Issue', + }, + __typename: 'IssueEdge', + }, + { + node: { + id: 'gid://gitlab/Issue/123', + iid: '391', + title: 'Remove unused MR widget extension expand success, failed, warning events', + confidential: false, + userNotesCount: 16, + upvotes: 0, + webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391', + state: 'opened', + closedAt: null, + createdAt: '2023-02-15T12:29:59Z', + updatedAt: '2023-03-01T19:38:22Z', + author: { + id: 'gid://gitlab/User/2080', + name: 'User Name', + username: 'user-name', + avatarUrl: '/uploads/-/system/user/avatar/2080/avatar.png', + webUrl: 'https://gitlab.com/user-name', + __typename: 'UserCore', + }, + __typename: 'Issue', + }, + __typename: 'IssueEdge', + }, + ], + __typename: 'IssueConnection', + }, + __typename: 'Project', + }, + }, +}; diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js index 010c719bd84..c5507c88fd7 100644 --- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js +++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js @@ -34,7 +34,6 @@ describe('RelatedMergeRequests', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js index 7339372a8d1..31c96265f8d 100644 --- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js +++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js @@ -1,12 +1,12 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import * as actions from '~/issues/related_merge_requests/store/actions'; import * as types from '~/issues/related_merge_requests/store/mutation_types'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('RelatedMergeRequest store actions', () => { let state; diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 9fa0ce6f93d..1006f54eeaf 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -1,11 +1,10 @@ import { GlIcon, GlIntersectionObserver } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { setHTMLFixture } from 'helpers/fixtures'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { IssuableStatusText, STATUS_CLOSED, @@ -21,29 +20,27 @@ import FormComponent from '~/issues/show/components/form.vue'; import TitleComponent from '~/issues/show/components/title.vue'; import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue'; import PinnedLinks from '~/issues/show/components/pinned_links.vue'; -import { POLLING_DELAY } from '~/issues/show/constants'; import eventHub from '~/issues/show/event_hub'; import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import { appProps, initialRequest, publishedIncidentUrl, + putRequest, secondRequest, zoomMeetingUrl, } from '../mock_data/mock_data'; -jest.mock('~/flash'); -jest.mock('~/issues/show/event_hub'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); jest.mock('~/behaviors/markdown/render_gfm'); const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; describe('Issuable output', () => { - let mock; - let realtimeRequestCount = 0; + let axiosMock; let wrapper; const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header'); @@ -57,15 +54,14 @@ describe('Issuable output', () => { const findForm = () => wrapper.findComponent(FormComponent); const findPinnedLinks = () => wrapper.findComponent(PinnedLinks); - const mountComponent = (props = {}, options = {}, data = {}) => { + const createComponent = ({ props = {}, options = {}, data = {} } = {}) => { wrapper = shallowMountExtended(IssuableApp, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { ...appProps, ...props }, provide: { fullPath: 'gitlab-org/incidents', - iid: '19', uploadMetricsFeatureAvailable: false, }, stubs: { @@ -79,6 +75,28 @@ describe('Issuable output', () => { }, ...options, }); + + jest.advanceTimersToNextTimer(2); + return waitForPromises(); + }; + + const emitHubEvent = (event) => { + eventHub.$emit(event); + return waitForPromises(); + }; + + const openForm = () => { + return emitHubEvent('open.form'); + }; + + const updateIssuable = () => { + return emitHubEvent('update.issuable'); + }; + + const advanceToNextPoll = () => { + // We get new data through the HTTP request. + jest.advanceTimersToNextTimer(); + return waitForPromises(); }; beforeEach(() => { @@ -98,79 +116,100 @@ describe('Issuable output', () => { </div> `); - mock = new MockAdapter(axios); - mock - .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes') - .reply(() => { - const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]); - realtimeRequestCount += 1; - return res; - }); + jest.spyOn(eventHub, '$emit'); - mountComponent(); + axiosMock = new MockAdapter(axios); + const endpoint = '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes'; - jest.advanceTimersByTime(2); + axiosMock.onGet(endpoint).replyOnce(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[0], { + 'POLL-INTERVAL': '1', + }); + axiosMock.onGet(endpoint).reply(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[1], { + 'POLL-INTERVAL': '-1', + }); + axiosMock.onPut().reply(HTTP_STATUS_OK, putRequest); }); - afterEach(() => { - mock.restore(); - realtimeRequestCount = 0; - wrapper.vm.poll.stop(); - wrapper.destroy(); - resetHTMLFixture(); - }); + describe('update', () => { + beforeEach(async () => { + await createComponent(); + }); - it('should render a title/description/edited and update title/description/edited on update', () => { - return axios - .waitForAll() - .then(() => { - expect(findTitle().props('titleText')).toContain('this is a title'); - expect(findDescription().props('descriptionText')).toContain('this is a description'); - - expect(findEdited().exists()).toBe(true); - expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); - expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at); - expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version); - }) - .then(() => { - wrapper.vm.poll.makeRequest(); - return axios.waitForAll(); - }) - .then(() => { - expect(findTitle().props('titleText')).toContain('2'); - expect(findDescription().props('descriptionText')).toContain('42'); - - expect(findEdited().exists()).toBe(true); - expect(findEdited().props('updatedByName')).toBe('Other User'); - expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); - expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at); - }); - }); + it('should render a title/description/edited and update title/description/edited on update', async () => { + expect(findTitle().props('titleText')).toContain(initialRequest.title_text); + expect(findDescription().props('descriptionText')).toContain('this is a description'); - it('shows actions if permissions are correct', async () => { - wrapper.vm.showForm = true; + expect(findEdited().exists()).toBe(true); + expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); + expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at); + expect(findDescription().props().lockVersion).toBe(initialRequest.lock_version); - await nextTick(); - expect(findForm().exists()).toBe(true); - }); + await advanceToNextPoll(); - it('does not show actions if permissions are incorrect', async () => { - wrapper.vm.showForm = true; - wrapper.setProps({ canUpdate: false }); + expect(findTitle().props('titleText')).toContain('2'); + expect(findDescription().props('descriptionText')).toContain('42'); - await nextTick(); - expect(findForm().exists()).toBe(false); + expect(findEdited().exists()).toBe(true); + expect(findEdited().props('updatedByName')).toBe('Other User'); + expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); + expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at); + }); }); - it('does not update formState if form is already open', async () => { - wrapper.vm.updateAndShowForm(); + describe('with permissions', () => { + beforeEach(async () => { + await createComponent(); + }); - wrapper.vm.state.titleText = 'testing 123'; + it('shows actions on `open.form` event', async () => { + expect(findForm().exists()).toBe(false); - wrapper.vm.updateAndShowForm(); + await openForm(); - await nextTick(); - expect(wrapper.vm.store.formState.title).not.toBe('testing 123'); + expect(findForm().exists()).toBe(true); + }); + + it('update formState if form is not open', async () => { + const titleValue = initialRequest.title_text; + + expect(findTitle().exists()).toBe(true); + expect(findTitle().props('titleText')).toBe(titleValue); + + await advanceToNextPoll(); + + // The title component has the new data, so the state was updated + expect(findTitle().exists()).toBe(true); + expect(findTitle().props('titleText')).toBe(secondRequest.title_text); + }); + + it('does not update formState if form is already open', async () => { + const titleValue = initialRequest.title_text; + + expect(findTitle().exists()).toBe(true); + expect(findTitle().props('titleText')).toBe(titleValue); + + await openForm(); + + // Opening the form, the data has not changed + expect(findForm().props().formState.title).toBe(titleValue); + + await advanceToNextPoll(); + + // We expect the prop value not to have changed after another API call + expect(findForm().props().formState.title).toBe(titleValue); + }); + }); + + describe('without permissions', () => { + beforeEach(async () => { + await createComponent({ props: { canUpdate: false } }); + }); + + it('does not show actions if permissions are incorrect', async () => { + await openForm(); + + expect(findForm().exists()).toBe(false); + }); }); describe('Pinned links propagated', () => { @@ -178,288 +217,130 @@ describe('Issuable output', () => { prop | value ${'zoomMeetingUrl'} | ${zoomMeetingUrl} ${'publishedIncidentUrl'} | ${publishedIncidentUrl} - `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => { + `('sets the $prop correctly on underlying pinned links', async ({ prop, value }) => { + await createComponent(); + expect(findPinnedLinks().props(prop)).toBe(value); }); }); - describe('updateIssuable', () => { + describe('updating an issue', () => { + beforeEach(async () => { + await createComponent(); + }); + it('fetches new data after update', async () => { - const updateStoreSpy = jest.spyOn(wrapper.vm, 'updateStoreState'); - const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData'); - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { web_url: window.location.pathname }, - }); + await advanceToNextPoll(); - await wrapper.vm.updateIssuable(); - expect(updateStoreSpy).toHaveBeenCalled(); - expect(getDataSpy).toHaveBeenCalled(); + await updateIssuable(); + + expect(axiosMock.history.put).toHaveLength(1); + // The call was made with the new data + expect(axiosMock.history.put[0].data.title).toEqual(findTitle().props().title); }); - it('correctly updates issuable data', async () => { - const spy = jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { web_url: window.location.pathname }, - }); + it('closes the form after fetching data', async () => { + await updateIssuable(); - await wrapper.vm.updateIssuable(); - expect(spy).toHaveBeenCalledWith(wrapper.vm.formState); expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); it('does not redirect if issue has not moved', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { - web_url: window.location.pathname, - confidential: wrapper.vm.isConfidential, - }, + axiosMock.onPut().reply(HTTP_STATUS_OK, { + ...putRequest, + confidential: appProps.isConfidential, }); - await wrapper.vm.updateIssuable(); + await updateIssuable(); + expect(visitUrl).not.toHaveBeenCalled(); }); it('does not redirect if issue has not moved and user has switched tabs', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { - web_url: '', - confidential: wrapper.vm.isConfidential, - }, + axiosMock.onPut().reply(HTTP_STATUS_OK, { + ...putRequest, + web_url: '', + confidential: appProps.isConfidential, }); - await wrapper.vm.updateIssuable(); + await updateIssuable(); + expect(visitUrl).not.toHaveBeenCalled(); }); it('redirects if returned web_url has changed', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({ - data: { - web_url: '/testing-issue-move', - confidential: wrapper.vm.isConfidential, - }, - }); - - wrapper.vm.updateIssuable(); - - await wrapper.vm.updateIssuable(); - expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move'); - }); - - describe('shows dialog when issue has unsaved changed', () => { - it('confirms on title change', async () => { - wrapper.vm.showForm = true; - wrapper.vm.state.titleText = 'title has changed'; - const e = { returnValue: null }; - wrapper.vm.handleBeforeUnloadEvent(e); - - await nextTick(); - expect(e.returnValue).not.toBeNull(); - }); - - it('confirms on description change', async () => { - wrapper.vm.showForm = true; - wrapper.vm.state.descriptionText = 'description has changed'; - const e = { returnValue: null }; - wrapper.vm.handleBeforeUnloadEvent(e); + const webUrl = '/testing-issue-move'; - await nextTick(); - expect(e.returnValue).not.toBeNull(); + axiosMock.onPut().reply(HTTP_STATUS_OK, { + ...putRequest, + web_url: webUrl, + confidential: appProps.isConfidential, }); - it('does nothing when nothing has changed', async () => { - const e = { returnValue: null }; - wrapper.vm.handleBeforeUnloadEvent(e); + await updateIssuable(); - await nextTick(); - expect(e.returnValue).toBeNull(); - }); + expect(visitUrl).toHaveBeenCalledWith(webUrl); }); describe('error when updating', () => { - it('closes form on error', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue(); + it('closes form', async () => { + axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED); - await wrapper.vm.updateIssuable(); - expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); - expect(createAlert).toHaveBeenCalledWith({ message: `Error updating issue` }); - }); + await updateIssuable(); - it('returns the correct error message for issuableType', async () => { - jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue(); - wrapper.setProps({ issuableType: 'merge request' }); - - await nextTick(); - await wrapper.vm.updateIssuable(); expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); - expect(createAlert).toHaveBeenCalledWith({ message: `Error updating merge request` }); - }); - - it('shows error message from backend if exists', async () => { - const msg = 'Custom error message from backend'; - jest - .spyOn(wrapper.vm.service, 'updateIssuable') - .mockRejectedValue({ response: { data: { errors: [msg] } } }); - - await wrapper.vm.updateIssuable(); expect(createAlert).toHaveBeenCalledWith({ - message: `${wrapper.vm.defaultErrorMessage}. ${msg}`, + message: `Error updating issue. Request failed with status code 401`, }); }); - }); - }); - - describe('updateAndShowForm', () => { - it('shows locked warning if form is open & data is different', async () => { - await nextTick(); - wrapper.vm.updateAndShowForm(); - - wrapper.vm.poll.makeRequest(); - await new Promise((resolve) => { - wrapper.vm.$watch('formState.lockedWarningVisible', (value) => { - if (value) { - resolve(); - } - }); - }); - - expect(wrapper.vm.formState.lockedWarningVisible).toBe(true); - expect(wrapper.vm.formState.lock_version).toBe(1); - }); - }); - - describe('requestTemplatesAndShowForm', () => { - let formSpy; - - beforeEach(() => { - formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm'); - }); - - it('shows the form if template names as hash request is successful', () => { - const mockData = { - test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }], - }; - mock - .onGet('/issuable-templates-path') - .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData])); - - return wrapper.vm.requestTemplatesAndShowForm().then(() => { - expect(formSpy).toHaveBeenCalledWith(mockData); - }); - }); + it('returns the correct error message for issuableType', async () => { + axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED); - it('shows the form if template names as array request is successful', () => { - const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }]; - mock - .onGet('/issuable-templates-path') - .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData])); + await updateIssuable(); - return wrapper.vm.requestTemplatesAndShowForm().then(() => { - expect(formSpy).toHaveBeenCalledWith(mockData); - }); - }); - - it('shows the form if template names request failed', () => { - mock - .onGet('/issuable-templates-path') - .reply(() => Promise.reject(new Error('something went wrong'))); + wrapper.setProps({ issuableType: 'merge request' }); - return wrapper.vm.requestTemplatesAndShowForm().then(() => { - expect(createAlert).toHaveBeenCalledWith({ message: 'Error updating issue' }); + await updateIssuable(); - expect(formSpy).toHaveBeenCalledWith(); + expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); + expect(createAlert).toHaveBeenCalledWith({ + message: `Error updating merge request. Request failed with status code 401`, + }); }); - }); - }); - - describe('show inline edit button', () => { - it('should render by default', () => { - expect(findTitle().props('showInlineEditButton')).toBe(true); - }); - - it('should render if showInlineEditButton', async () => { - wrapper.setProps({ showInlineEditButton: true }); - - await nextTick(); - expect(findTitle().props('showInlineEditButton')).toBe(true); - }); - it('should not render if showInlineEditButton is false', async () => { - wrapper.setProps({ showInlineEditButton: false }); - - await nextTick(); - expect(findTitle().props('showInlineEditButton')).toBe(false); - }); - }); - - describe('updateStoreState', () => { - it('should make a request and update the state of the store', () => { - const data = { foo: 1 }; - const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData').mockResolvedValue({ data }); - const updateStateSpy = jest - .spyOn(wrapper.vm.store, 'updateState') - .mockImplementation(jest.fn); - - return wrapper.vm.updateStoreState().then(() => { - expect(getDataSpy).toHaveBeenCalled(); - expect(updateStateSpy).toHaveBeenCalledWith(data); - }); - }); + it('shows error message from backend if exists', async () => { + const msg = 'Custom error message from backend'; + axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED, { errors: [msg] }); - it('should show error message if store update fails', () => { - jest.spyOn(wrapper.vm.service, 'getData').mockRejectedValue(); - wrapper.setProps({ issuableType: 'merge request' }); + await updateIssuable(); - return wrapper.vm.updateStoreState().then(() => { expect(createAlert).toHaveBeenCalledWith({ - message: `Error updating ${wrapper.vm.issuableType}`, + message: `Error updating issue. ${msg}`, }); }); }); }); - describe('issueChanged', () => { - beforeEach(() => { - wrapper.vm.store.formState.title = ''; - wrapper.vm.store.formState.description = ''; - wrapper.setProps({ - initialDescriptionText: '', - initialTitleText: '', - }); - }); - - it('returns true when title is changed', () => { - wrapper.vm.store.formState.title = 'RandomText'; - - expect(wrapper.vm.issueChanged).toBe(true); - }); - - it('returns false when title is empty null', () => { - wrapper.vm.store.formState.title = null; - - expect(wrapper.vm.issueChanged).toBe(false); - }); - - it('returns true when description is changed', () => { - wrapper.vm.store.formState.description = 'RandomText'; - - expect(wrapper.vm.issueChanged).toBe(true); + describe('Locked warning', () => { + beforeEach(async () => { + await createComponent(); }); - it('returns false when description is empty null', () => { - wrapper.vm.store.formState.description = null; - - expect(wrapper.vm.issueChanged).toBe(false); - }); - - it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => { - wrapper.vm.store.formState.description = ''; - wrapper.setProps({ initialDescriptionText: null }); + it('shows locked warning if form is open & data is different', async () => { + await openForm(); + await advanceToNextPoll(); - expect(wrapper.vm.issueChanged).toBe(false); + expect(findForm().props().formState.lockedWarningVisible).toBe(true); + expect(findForm().props().formState.lock_version).toBe(1); }); }); describe('sticky header', () => { + beforeEach(async () => { + await createComponent(); + }); + describe('when title is in view', () => { it('is not shown', () => { expect(findStickyHeader().exists()).toBe(false); @@ -467,21 +348,18 @@ describe('Issuable output', () => { }); describe('when title is not in view', () => { - beforeEach(() => { - wrapper.vm.state.titleText = 'Sticky header title'; + beforeEach(async () => { wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); }); it('shows with title', () => { - expect(findStickyHeader().text()).toContain('Sticky header title'); + expect(findStickyHeader().text()).toContain(initialRequest.title_text); }); it('shows with title for an epic', async () => { - wrapper.setProps({ issuableType: 'epic' }); - - await nextTick(); + await wrapper.setProps({ issuableType: 'epic' }); - expect(findStickyHeader().text()).toContain('Sticky header title'); + expect(findStickyHeader().text()).toContain(' this is a title'); }); it.each` @@ -493,9 +371,7 @@ describe('Issuable output', () => { `( 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus', async ({ issuableType, issuableStatus, statusIcon }) => { - wrapper.setProps({ issuableType, issuableStatus }); - - await nextTick(); + await wrapper.setProps({ issuableType, issuableStatus }); expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon); }, @@ -507,9 +383,7 @@ describe('Issuable output', () => { ${'shows with Closed when status is closed'} | ${STATUS_CLOSED} ${'shows with Open when status is reopened'} | ${STATUS_REOPENED} `('$title', async ({ state }) => { - wrapper.setProps({ issuableStatus: state }); - - await nextTick(); + await wrapper.setProps({ issuableStatus: state }); expect(findStickyHeader().text()).toContain(IssuableStatusText[state]); }); @@ -519,9 +393,7 @@ describe('Issuable output', () => { ${'does not show confidential badge when issue is not confidential'} | ${false} ${'shows confidential badge when issue is confidential'} | ${true} `('$title', async ({ isConfidential }) => { - wrapper.setProps({ isConfidential }); - - await nextTick(); + await wrapper.setProps({ isConfidential }); const confidentialEl = findConfidentialBadge(); expect(confidentialEl.exists()).toBe(isConfidential); @@ -538,9 +410,7 @@ describe('Issuable output', () => { ${'does not show locked badge when issue is not locked'} | ${false} ${'shows locked badge when issue is locked'} | ${true} `('$title', async ({ isLocked }) => { - wrapper.setProps({ isLocked }); - - await nextTick(); + await wrapper.setProps({ isLocked }); expect(findLockedBadge().exists()).toBe(isLocked); }); @@ -550,9 +420,7 @@ describe('Issuable output', () => { ${'does not show hidden badge when issue is not hidden'} | ${false} ${'shows hidden badge when issue is hidden'} | ${true} `('$title', async ({ isHidden }) => { - wrapper.setProps({ isHidden }); - - await nextTick(); + await wrapper.setProps({ isHidden }); const hiddenBadge = findHiddenBadge(); @@ -569,6 +437,10 @@ describe('Issuable output', () => { }); describe('Composable description component', () => { + beforeEach(async () => { + await createComponent(); + }); + const findIncidentTabs = () => wrapper.findComponent(IncidentTabs); const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'; @@ -587,13 +459,13 @@ describe('Issuable output', () => { }); describe('when using incident tabs description wrapper', () => { - beforeEach(() => { - mountComponent( - { + beforeEach(async () => { + await createComponent({ + props: { descriptionComponent: IncidentTabs, showTitleBorder: false, }, - { + options: { mocks: { $apollo: { queries: { @@ -604,7 +476,7 @@ describe('Issuable output', () => { }, }, }, - ); + }); }); it('does not the description component', () => { @@ -622,48 +494,77 @@ describe('Issuable output', () => { }); describe('taskListUpdateStarted', () => { - it('stops polling', () => { - jest.spyOn(wrapper.vm.poll, 'stop'); + beforeEach(async () => { + await createComponent(); + }); + + it('stops polling', async () => { + expect(findTitle().props().titleText).toBe(initialRequest.title_text); + + findDescription().vm.$emit('taskListUpdateStarted'); - wrapper.vm.taskListUpdateStarted(); + await advanceToNextPoll(); - expect(wrapper.vm.poll.stop).toHaveBeenCalled(); + expect(findTitle().props().titleText).toBe(initialRequest.title_text); }); }); describe('taskListUpdateSucceeded', () => { - it('enables polling', () => { - jest.spyOn(wrapper.vm.poll, 'enable'); - jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest'); + beforeEach(async () => { + await createComponent(); + findDescription().vm.$emit('taskListUpdateStarted'); + }); + + it('enables polling', async () => { + // Ensure that polling is not working before + expect(findTitle().props().titleText).toBe(initialRequest.title_text); + await advanceToNextPoll(); + + expect(findTitle().props().titleText).toBe(initialRequest.title_text); - wrapper.vm.taskListUpdateSucceeded(); + // Enable Polling an move forward + findDescription().vm.$emit('taskListUpdateSucceeded'); + await advanceToNextPoll(); - expect(wrapper.vm.poll.enable).toHaveBeenCalled(); - expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY); + // Title has changed: polling works! + expect(findTitle().props().titleText).toBe(secondRequest.title_text); }); }); describe('taskListUpdateFailed', () => { - it('enables polling and calls updateStoreState', () => { - jest.spyOn(wrapper.vm.poll, 'enable'); - jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest'); - jest.spyOn(wrapper.vm, 'updateStoreState'); + beforeEach(async () => { + await createComponent(); + findDescription().vm.$emit('taskListUpdateStarted'); + }); + + it('enables polling and calls updateStoreState', async () => { + // Ensure that polling is not working before + expect(findTitle().props().titleText).toBe(initialRequest.title_text); + await advanceToNextPoll(); - wrapper.vm.taskListUpdateFailed(); + expect(findTitle().props().titleText).toBe(initialRequest.title_text); - expect(wrapper.vm.poll.enable).toHaveBeenCalled(); - expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY); - expect(wrapper.vm.updateStoreState).toHaveBeenCalled(); + // Enable Polling an move forward + findDescription().vm.$emit('taskListUpdateFailed'); + await advanceToNextPoll(); + + // Title has changed: polling works! + expect(findTitle().props().titleText).toBe(secondRequest.title_text); }); }); describe('saveDescription event', () => { + beforeEach(async () => { + await createComponent(); + }); + it('makes request to update issue', async () => { const description = 'I have been updated!'; findDescription().vm.$emit('saveDescription', description); + await waitForPromises(); - expect(mock.history.put[0].data).toContain(description); + expect(axiosMock.history.put[0].data).toContain(description); }); }); }); diff --git a/spec/frontend/issues/show/components/delete_issue_modal_spec.js b/spec/frontend/issues/show/components/delete_issue_modal_spec.js index 97a091a1748..b8adeb24005 100644 --- a/spec/frontend/issues/show/components/delete_issue_modal_spec.js +++ b/spec/frontend/issues/show/components/delete_issue_modal_spec.js @@ -20,10 +20,6 @@ describe('DeleteIssueModal component', () => { const mountComponent = (props = {}) => shallowMount(DeleteIssueModal, { propsData: { ...defaultProps, ...props } }); - afterEach(() => { - wrapper.destroy(); - }); - describe('modal', () => { it('renders', () => { wrapper = mountComponent(); diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index da51372dd3d..740b2f782e4 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -1,25 +1,19 @@ import $ from 'jquery'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlModal } from '@gitlab/ui'; import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import setWindowLocation from 'helpers/set_window_location_helper'; -import { stubComponent } from 'helpers/stub_component'; import { TEST_HOST } from 'helpers/test_constants'; -import { mockTracking } from 'helpers/tracking_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import Description from '~/issues/show/components/description.vue'; import eventHub from '~/issues/show/event_hub'; -import { updateHistory } from '~/lib/utils/url_utility'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import TaskList from '~/task_list'; -import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; import { createWorkItemMutationErrorResponse, @@ -27,14 +21,9 @@ import { getIssueDetailsResponse, projectWorkItemTypesQueryResponse, } from 'jest/work_items/mock_data'; -import { - descriptionProps as initialProps, - descriptionHtmlWithList, - descriptionHtmlWithCheckboxes, - descriptionHtmlWithTask, -} from '../mock_data/mock_data'; +import { descriptionProps as initialProps, descriptionHtmlWithList } from '../mock_data/mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), updateHistory: jest.fn(), @@ -43,9 +32,6 @@ jest.mock('~/task_list'); jest.mock('~/behaviors/markdown/render_gfm'); const mockSpriteIcons = '/icons.svg'; -const showModal = jest.fn(); -const hideModal = jest.fn(); -const showDetailsModal = jest.fn(); const $toast = { show: jest.fn(), }; @@ -62,7 +48,6 @@ const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTyp describe('Description component', () => { let wrapper; - let originalGon; Vue.use(VueApollo); @@ -70,21 +55,16 @@ describe('Description component', () => { const findTextarea = () => wrapper.find('[data-testid="textarea"]'); const findListItems = () => findGfmContent().findAll('ul > li'); const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions'); - const findTaskLink = () => wrapper.find('a.gfm-issue'); - const findModal = () => wrapper.findComponent(GlModal); - const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); function createComponent({ props = {}, provide, issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse), createWorkItemMutationHandler, - ...options } = {}) { wrapper = shallowMountExtended(Description, { propsData: { issueId: 1, - issueIid: 1, ...initialProps, ...props, }, @@ -102,25 +82,10 @@ describe('Description component', () => { mocks: { $toast, }, - stubs: { - GlModal: stubComponent(GlModal, { - methods: { - show: showModal, - hide: hideModal, - }, - }), - WorkItemDetailModal: stubComponent(WorkItemDetailModal, { - methods: { - show: showDetailsModal, - }, - }), - }, - ...options, }); } beforeEach(() => { - originalGon = window.gon; window.gon = { sprite_icons: mockSpriteIcons }; setWindowLocation(TEST_HOST); @@ -136,8 +101,6 @@ describe('Description component', () => { }); afterAll(() => { - window.gon = originalGon; - $('.issuable-meta .flash-container').remove(); }); @@ -285,7 +248,6 @@ describe('Description component', () => { props: { descriptionHtml: descriptionHtmlWithList, }, - attachTo: document.body, }); await nextTick(); }); @@ -325,33 +287,6 @@ describe('Description component', () => { }); }); - describe('description with checkboxes', () => { - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithCheckboxes, - }, - }); - return nextTick(); - }); - - it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => { - expect(findTaskActionButtons()).toHaveLength(3); - }); - - it('does not show a modal by default', () => { - expect(findModal().exists()).toBe(false); - }); - - it('shows toast after delete success', async () => { - const newDesc = 'description'; - findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); - - expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); - expect($toast.show).toHaveBeenCalledWith('Task deleted'); - }); - }); - describe('task list item actions', () => { describe('converting the task list item to a task', () => { describe('when successful', () => { @@ -391,11 +326,7 @@ describe('Description component', () => { }); it('calls a mutation to create a task', () => { - const { - confidential, - iteration, - milestone, - } = issueDetailsResponse.data.workspace.issuable; + const { confidential, iteration, milestone } = issueDetailsResponse.data.issue; expect(createWorkItemMutationHandler).toHaveBeenCalledWith({ input: { confidential, @@ -468,109 +399,4 @@ describe('Description component', () => { }); }); }); - - describe('work items detail', () => { - describe('when opening and closing', () => { - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithTask, - }, - }); - return nextTick(); - }); - - it('opens when task button is clicked', async () => { - await findTaskLink().trigger('click'); - - expect(showDetailsModal).toHaveBeenCalled(); - expect(updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?work_item_id=2`, - replace: true, - }); - }); - - it('closes from an open state', async () => { - await findTaskLink().trigger('click'); - - findWorkItemDetailModal().vm.$emit('close'); - await nextTick(); - - expect(updateHistory).toHaveBeenLastCalledWith({ - url: `${TEST_HOST}/`, - replace: true, - }); - }); - - it('tracks when opened', async () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - - await findTaskLink().trigger('click'); - - expect(trackingSpy).toHaveBeenCalledWith( - TRACKING_CATEGORY_SHOW, - 'viewed_work_item_from_modal', - { - category: TRACKING_CATEGORY_SHOW, - label: 'work_item_view', - property: 'type_task', - }, - ); - }); - }); - - describe('when url query `work_item_id` exists', () => { - it.each` - behavior | workItemId | modalOpened - ${'opens'} | ${'2'} | ${1} - ${'does not open'} | ${'123'} | ${0} - ${'does not open'} | ${'123e'} | ${0} - ${'does not open'} | ${'12e3'} | ${0} - ${'does not open'} | ${'1e23'} | ${0} - ${'does not open'} | ${'x'} | ${0} - ${'does not open'} | ${'undefined'} | ${0} - `( - '$behavior when url contains `work_item_id=$workItemId`', - async ({ workItemId, modalOpened }) => { - setWindowLocation(`?work_item_id=${workItemId}`); - - createComponent({ - props: { descriptionHtml: descriptionHtmlWithTask }, - }); - - expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened); - }, - ); - }); - }); - - describe('when hovering task links', () => { - beforeEach(() => { - createComponent({ - props: { - descriptionHtml: descriptionHtmlWithTask, - }, - }); - return nextTick(); - }); - - it('prefetches work item detail after work item link is hovered for 150ms', async () => { - await findTaskLink().trigger('mouseover'); - jest.advanceTimersByTime(150); - await waitForPromises(); - - expect(queryHandler).toHaveBeenCalledWith({ - id: 'gid://gitlab/WorkItem/2', - }); - }); - - it('does not work item detail after work item link is hovered for less than 150ms', async () => { - await findTaskLink().trigger('mouseover'); - await findTaskLink().trigger('mouseout'); - jest.advanceTimersByTime(150); - await waitForPromises(); - - expect(queryHandler).not.toHaveBeenCalled(); - }); - }); }); diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js index 11c43ea4388..ca561149806 100644 --- a/spec/frontend/issues/show/components/edit_actions_spec.js +++ b/spec/frontend/issues/show/components/edit_actions_spec.js @@ -56,10 +56,6 @@ describe('Edit Actions component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all buttons as enabled', () => { const buttons = findEditButtons().wrappers; buttons.forEach((button) => { diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js index aa6e0a9dceb..a509627c347 100644 --- a/spec/frontend/issues/show/components/edited_spec.js +++ b/spec/frontend/issues/show/components/edited_spec.js @@ -15,10 +15,6 @@ describe('Edited component', () => { const mountComponent = (propsData) => mount(Edited, { propsData }); const updatedAt = '2017-05-15T12:31:04.428Z'; - afterEach(() => { - wrapper.destroy(); - }); - it('renders an edited at+by string', () => { wrapper = mountComponent({ updatedAt, diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index 273ddfdd5d4..5c145ed4707 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -33,11 +33,6 @@ describe('Description field component', () => { jest.spyOn(eventHub, '$emit'); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders markdown field with description', () => { wrapper = mountComponent(); @@ -80,17 +75,18 @@ describe('Description field component', () => { }); it('uses the MarkdownEditor component to edit markdown', () => { - expect(findMarkdownEditor().props()).toEqual( - expect.objectContaining({ - value: 'test', - renderMarkdownPath: '/', - markdownDocsPath: '/', - quickActionsDocsPath: expect.any(String), - autofocus: true, - supportsQuickActions: true, - enableAutocomplete: true, - }), - ); + expect(findMarkdownEditor().props()).toMatchObject({ + value: 'test', + renderMarkdownPath: '/', + autofocus: true, + supportsQuickActions: true, + quickActionsDocsPath: expect.any(String), + }); + + expect(findMarkdownEditor().vm.$attrs).toMatchObject({ + 'enable-autocomplete': true, + 'markdown-docs-path': '/', + }); }); it('triggers update with meta+enter', () => { diff --git a/spec/frontend/issues/show/components/fields/description_template_spec.js b/spec/frontend/issues/show/components/fields/description_template_spec.js index 79a3bfa9840..1e8d5e2dd95 100644 --- a/spec/frontend/issues/show/components/fields/description_template_spec.js +++ b/spec/frontend/issues/show/components/fields/description_template_spec.js @@ -22,10 +22,6 @@ describe('Issue description template component with templates as hash', () => { wrapper = shallowMount(descriptionTemplate, options); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders templates as JSON hash in data attribute', () => { createComponent(); expect(findIssuableSelector().attributes('data-data')).toBe( diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js index a5fa96d8d64..b28762f1520 100644 --- a/spec/frontend/issues/show/components/fields/title_spec.js +++ b/spec/frontend/issues/show/components/fields/title_spec.js @@ -17,11 +17,6 @@ describe('Title field component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders form control with formState title', () => { expect(findInput().element.value).toBe('test'); }); diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js index 27ac0e1baf3..e655cf3b37d 100644 --- a/spec/frontend/issues/show/components/fields/type_spec.js +++ b/spec/frontend/issues/show/components/fields/type_spec.js @@ -1,4 +1,4 @@ -import { GlFormGroup, GlListbox, GlIcon } from '@gitlab/ui'; +import { GlFormGroup, GlCollapsibleListbox, GlIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; @@ -32,7 +32,7 @@ describe('Issue type field component', () => { }, }; - const findListBox = () => wrapper.findComponent(GlListbox); + const findListBox = () => wrapper.findComponent(GlCollapsibleListbox); const findFormGroup = () => wrapper.findComponent(GlFormGroup); const findAllIssueItems = () => wrapper.findAll('[data-testid="issue-type-list-item"]'); const findIssueItemAt = (at) => findAllIssueItems().at(at); @@ -60,10 +60,6 @@ describe('Issue type field component', () => { mockIssueStateData = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - it.each` at | text | icon ${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon} diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js index aedb974cbd0..b8ed33801f2 100644 --- a/spec/frontend/issues/show/components/form_spec.js +++ b/spec/frontend/issues/show/components/form_spec.js @@ -30,10 +30,6 @@ describe('Inline edit form component', () => { projectNamespace: '/', }; - afterEach(() => { - wrapper.destroy(); - }); - const createComponent = (props) => { wrapper = shallowMount(formComponent, { propsData: { diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js index 3d9dad3a721..58ec7387851 100644 --- a/spec/frontend/issues/show/components/header_actions_spec.js +++ b/spec/frontend/issues/show/components/header_actions_spec.js @@ -1,20 +1,22 @@ import Vue, { nextTick } from 'vue'; -import { GlButton, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui'; +import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import { mockTracking } from 'helpers/tracking_helper'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; -import { IssueType, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants'; import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue'; import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import HeaderActions from '~/issues/show/components/header_actions.vue'; import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants'; +import issuesEventHub from '~/issues/show/event_hub'; import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql'; import * as urlUtility from '~/lib/utils/url_utility'; import eventHub from '~/notes/event_hub'; import createStore from '~/notes/stores'; -jest.mock('~/flash'); +jest.mock('~/alert'); +jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() })); describe('HeaderActions component', () => { let dispatchEventSpy; @@ -36,7 +38,7 @@ describe('HeaderActions component', () => { iid: '32', isIssueAuthor: true, issuePath: 'gitlab-org/gitlab-test/-/issues/1', - issueType: IssueType.Issue, + issueType: TYPE_ISSUE, newIssuePath: 'gitlab-org/gitlab-test/-/issues/new', projectPath: 'gitlab-org/gitlab-test', reportAbusePath: '-/abuse_reports/add_category', @@ -67,7 +69,8 @@ describe('HeaderActions component', () => { }, }; - const findToggleIssueStateButton = () => wrapper.findComponent(GlButton); + const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`); + const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`); const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`); const findMobileDropdown = () => findDropdownBy('mobile-dropdown'); @@ -103,6 +106,9 @@ describe('HeaderActions component', () => { mutate: mutateMock, }, }, + stubs: { + GlButton, + }, }); }; @@ -113,13 +119,12 @@ describe('HeaderActions component', () => { if (visitUrlSpy) { visitUrlSpy.mockRestore(); } - wrapper.destroy(); }); describe.each` issueType - ${IssueType.Issue} - ${IssueType.Incident} + ${TYPE_ISSUE} + ${TYPE_INCIDENT} `('when issue type is $issueType', ({ issueType }) => { describe('close/reopen button', () => { describe.each` @@ -240,6 +245,30 @@ describe('HeaderActions component', () => { }); }); }); + + describe(`show edit button ${issueType}`, () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + canUpdateIssue: true, + canCreateIssue: false, + isIssueAuthor: true, + issueType, + canReportSpam: false, + canPromoteToEpic: false, + }, + }); + }); + it(`shows the edit button`, () => { + expect(findEditButton().exists()).toBe(true); + }); + + it('should trigger "open.form" event when clicked', async () => { + expect(issuesEventHub.$emit).not.toHaveBeenCalled(); + await findEditButton().trigger('click'); + expect(issuesEventHub.$emit).toHaveBeenCalledWith('open.form'); + }); + }); }); describe('delete issue button', () => { 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 index 6c923cae0cc..6b68e7a0da6 100644 --- 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 @@ -9,7 +9,7 @@ import createTimelineEventMutation from '~/issues/show/components/incidents/grap import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; import { timelineFormI18n } from '~/issues/show/components/incidents/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { useFakeDate } from 'helpers/fake_date'; import { timelineEventsCreateEventResponse, @@ -19,7 +19,7 @@ import { Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); const fakeDate = '2020-07-08T00:00:00.000Z'; @@ -99,7 +99,6 @@ describe('Create Timeline events', () => { afterEach(() => { createAlert.mockReset(); - wrapper.destroy(); }); describe('createIncidentTimelineEvent', () => { diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js index 33a3a6eddfc..0f4fb02a40b 100644 --- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js @@ -1,4 +1,5 @@ import merge from 'lodash/merge'; +import { nextTick } from 'vue'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { trackIncidentDetailsViewsOptions } from '~/incidents/constants'; import DescriptionComponent from '~/issues/show/components/description.vue'; @@ -11,6 +12,11 @@ import Tracking from '~/tracking'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import { descriptionProps } from '../../mock_data/mock_data'; +const push = jest.fn(); +const $router = { + push, +}; + const mockAlert = { __typename: 'AlertManagementAlert', detailsUrl: INVALID_URL, @@ -28,12 +34,20 @@ const defaultMocks = { }, }, }, + $route: { params: {} }, + $router, }; describe('Incident Tabs component', () => { let wrapper; - const mountComponent = ({ data = {}, options = {}, mount = shallowMountExtended } = {}) => { + const mountComponent = ({ + data = {}, + options = {}, + mount = shallowMountExtended, + hasLinkedAlerts = false, + mocks = {}, + } = {}) => { wrapper = mount( IncidentTabs, merge( @@ -54,11 +68,12 @@ describe('Incident Tabs component', () => { slaFeatureAvailable: true, canUpdate: true, canUpdateTimelineEvent: true, + hasLinkedAlerts, }, data() { return { alert: mockAlert, ...data }; }, - mocks: defaultMocks, + mocks: { ...defaultMocks, ...mocks }, }, options, ), @@ -102,11 +117,13 @@ describe('Incident Tabs component', () => { }); it('renders the alert details tab', () => { + mountComponent({ hasLinkedAlerts: true }); expect(findAlertDetailsTab().exists()).toBe(true); expect(findAlertDetailsTab().attributes('title')).toBe('Alert details'); }); it('renders the alert details table with the correct props', () => { + mountComponent({ hasLinkedAlerts: true }); const alert = { iid: mockAlert.iid }; expect(findAlertDetailsComponent().props('alert')).toMatchObject(alert); @@ -156,6 +173,40 @@ describe('Incident Tabs component', () => { expect(findActiveTabs()).toHaveLength(1); expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle); + expect(push).toHaveBeenCalledWith('/timeline'); + }); + }); + + describe('loading page with tab', () => { + it('shows the timeline tab when timeline path is passed', async () => { + mountComponent({ + mount: mountExtended, + mocks: { $route: { params: { tabId: 'timeline' } } }, + }); + await nextTick(); + expect(findActiveTabs()).toHaveLength(1); + expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle); + }); + + it('shows the alerts tab when timeline path is passed', async () => { + mountComponent({ + mount: mountExtended, + mocks: { $route: { params: { tabId: 'alerts' } } }, + hasLinkedAlerts: true, + }); + await nextTick(); + expect(findActiveTabs()).toHaveLength(1); + expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.alertsTitle); + }); + + it('shows the metrics tab when metrics path is passed', async () => { + mountComponent({ + mount: mountExtended, + mocks: { $route: { params: { tabId: 'metrics' } } }, + }); + await nextTick(); + expect(findActiveTabs()).toHaveLength(1); + expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.metricsTitle); }); }); }); 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 e352f9708e4..af01fd34336 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 @@ -10,12 +10,12 @@ import { TIMELINE_EVENT_TAGS, timelineEventTagsI18n, } from '~/issues/show/components/incidents/constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { useFakeDate } from 'helpers/fake_date'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); const fakeDate = '2020-07-08T00:00:00.000Z'; @@ -51,7 +51,6 @@ describe('Timeline events form', () => { afterEach(() => { createAlert.mockReset(); - wrapper.destroy(); }); const findMarkdownField = () => wrapper.findComponent(MarkdownField); 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 26fda877089..8d79dece888 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 @@ -11,7 +11,7 @@ import deleteTimelineEventMutation from '~/issues/show/components/incidents/grap import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import { useFakeDate } from 'helpers/fake_date'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { mockEvents, timelineEventsDeleteEventResponse, @@ -26,7 +26,7 @@ import { Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); const mockConfirmAction = ({ confirmed }) => { @@ -77,10 +77,6 @@ describe('IncidentTimelineEventList', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('groups items correctly', () => { expect(findTimelineEventGroups()).toHaveLength(2); 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 63474070701..48c3f0984a0 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 @@ -8,13 +8,13 @@ import IncidentTimelineEventsList from '~/issues/show/components/incidents/timel 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'; +import { createAlert } from '~/alert'; import { timelineTabI18n } from '~/issues/show/components/incidents/constants'; import { timelineEventsQueryListResponse, timelineEventsQueryEmptyResponse } from './mock_data'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); const graphQLError = new Error('GraphQL error'); const listResponse = jest.fn().mockResolvedValue(timelineEventsQueryListResponse); diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js index 75be17f9889..8ee0d906dd4 100644 --- a/spec/frontend/issues/show/components/incidents/utils_spec.js +++ b/spec/frontend/issues/show/components/incidents/utils_spec.js @@ -5,10 +5,10 @@ import { getUtcShiftedDate, getPreviousEventTags, } from '~/issues/show/components/incidents/utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { mockTimelineEventTags } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('incident utils', () => { describe('display and log error', () => { diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js index dd3c7c58380..f8a8c999632 100644 --- a/spec/frontend/issues/show/components/locked_warning_spec.js +++ b/spec/frontend/issues/show/components/locked_warning_spec.js @@ -13,11 +13,6 @@ describe('LockedWarning component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findAlert = () => wrapper.findComponent(GlAlert); const findLink = () => wrapper.findComponent(GlLink); diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js index d52f9d57453..8caa5236796 100644 --- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js +++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js @@ -17,7 +17,7 @@ describe('TaskListItemActions component', () => { document.body.appendChild(li); wrapper = shallowMount(TaskListItemActions, { - provide: { canUpdate: true, toggleClass: 'task-list-item-actions' }, + provide: { canUpdate: true }, attachTo: document.querySelector('div'), }); }; diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js index 7560b733ae6..16ac675e12c 100644 --- a/spec/frontend/issues/show/components/title_spec.js +++ b/spec/frontend/issues/show/components/title_spec.js @@ -1,96 +1,59 @@ -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import titleComponent from '~/issues/show/components/title.vue'; -import eventHub from '~/issues/show/event_hub'; -import Store from '~/issues/show/stores'; +import Title from '~/issues/show/components/title.vue'; describe('Title component', () => { - let vm; - beforeEach(() => { + let wrapper; + + const getTitleHeader = () => wrapper.findByTestId('issue-title'); + + const createWrapper = (props) => { setHTMLFixture(`<title />`); - const Component = Vue.extend(titleComponent); - const store = new Store({ - titleHtml: '', - descriptionHtml: '', - issuableRef: '', - }); - vm = new Component({ + wrapper = shallowMountExtended(Title, { propsData: { issuableRef: '#1', titleHtml: 'Testing <img />', titleText: 'Testing', - showForm: false, - formState: store.formState, + ...props, }, - }).$mount(); - }); + }); + }; afterEach(() => { resetHTMLFixture(); }); it('renders title HTML', () => { - expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); - }); - - it('updates page title when changing titleHtml', async () => { - const spy = jest.spyOn(vm, 'setPageTitle'); - vm.titleHtml = 'test'; + createWrapper(); - await nextTick(); - expect(spy).toHaveBeenCalled(); + expect(getTitleHeader().element.innerHTML.trim()).toBe('Testing <img>'); }); it('animates title changes', async () => { - vm.titleHtml = 'test'; + createWrapper(); - await nextTick(); + await wrapper.setProps({ + titleHtml: 'test', + }); - expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse'); - jest.runAllTimers(); + expect(getTitleHeader().classes('issue-realtime-pre-pulse')).toBe(true); + jest.runAllTimers(); await nextTick(); - expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse'); + expect(getTitleHeader().classes('issue-realtime-trigger-pulse')).toBe(true); }); it('updates page title after changing title', async () => { - vm.titleHtml = 'changed'; - vm.titleText = 'changed'; - - await nextTick(); - expect(document.querySelector('title').textContent.trim()).toContain('changed'); - }); + createWrapper(); - describe('inline edit button', () => { - it('should not show by default', () => { - expect(vm.$el.querySelector('.btn-edit')).toBeNull(); + await wrapper.setProps({ + titleHtml: 'changed', + titleText: 'changed', }); - it('should not show if canUpdate is false', () => { - vm.showInlineEditButton = true; - vm.canUpdate = false; - - expect(vm.$el.querySelector('.btn-edit')).toBeNull(); - }); - - it('should show if showInlineEditButton and canUpdate', () => { - vm.showInlineEditButton = true; - vm.canUpdate = true; - - expect(vm.$el.querySelector('.btn-edit')).toBeDefined(); - }); - - it('should trigger open.form event when clicked', async () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - vm.showInlineEditButton = true; - vm.canUpdate = true; - - await nextTick(); - vm.$el.querySelector('.btn-edit').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); - }); + expect(document.querySelector('title').textContent.trim()).toContain('changed'); }); }); diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js index 9f0b6fb1148..86d09665947 100644 --- a/spec/frontend/issues/show/mock_data/mock_data.js +++ b/spec/frontend/issues/show/mock_data/mock_data.js @@ -24,6 +24,19 @@ export const secondRequest = { lock_version: 2, }; +export const putRequest = { + web_url: window.location.pathname, + title: '<p>PUT</p>', + title_text: 'PUT', + description: '<p>PUT_DESC</p>', + description_text: 'PUT_DESC', + task_status: '0 of 0 completed', + updated_at: '2016-05-15T12:31:04.428Z', + updated_by_name: 'Other User', + updated_by_path: '/other_user', + lock_version: 2, +}; + export const descriptionProps = { canUpdate: true, descriptionHtml: 'test', @@ -66,47 +79,3 @@ export const descriptionHtmlWithList = ` <li data-sourcepos="3:1-3:8">todo 3</li> </ul> `; - -export const descriptionHtmlWithCheckboxes = ` - <ul dir="auto" class="task-list" data-sourcepos"3:1-5:12"> - <li class="task-list-item" data-sourcepos="3:1-3:11"> - <input class="task-list-item-checkbox" type="checkbox"> todo 1 - </li> - <li class="task-list-item" data-sourcepos="4:1-4:12"> - <input class="task-list-item-checkbox" type="checkbox"> todo 2 - </li> - <li class="task-list-item" data-sourcepos="5:1-5:12"> - <input class="task-list-item-checkbox" type="checkbox"> todo 3 - </li> - </ul> -`; - -export const descriptionHtmlWithTask = ` - <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto"> - <li data-sourcepos="1:1-1:10" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> - <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a> - </li> - <li data-sourcepos="2:1-2:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 2 - </li> - <li data-sourcepos="3:1-3:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 3 - </li> - </ul> -`; - -export const descriptionHtmlWithIssue = ` - <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto"> - <li data-sourcepos="1:1-1:10" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> - <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a> - </li> - <li data-sourcepos="2:1-2:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 2 - </li> - <li data-sourcepos="3:1-3:7" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> 3 - </li> - </ul> -`; diff --git a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js index d41031f9eaa..5e6b67aec40 100644 --- a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js +++ b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js @@ -78,10 +78,6 @@ describe('NewBranchForm', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('when selecting items from dropdowns', () => { describe('when no project selected', () => { beforeEach(() => { 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 944854faab3..0a887efee4b 100644 --- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js @@ -49,10 +49,6 @@ describe('ProjectDropdown', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('when loading projects', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js index 56e425fa4eb..701512953df 100644 --- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js @@ -54,10 +54,6 @@ describe('SourceBranchDropdown', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('when `selectedProject` prop is not specified', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/jira_connect/branches/pages/index_spec.js b/spec/frontend/jira_connect/branches/pages/index_spec.js index 92976dd28da..4b79d5feab5 100644 --- a/spec/frontend/jira_connect/branches/pages/index_spec.js +++ b/spec/frontend/jira_connect/branches/pages/index_spec.js @@ -25,10 +25,6 @@ describe('NewBranchForm', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('page title', () => { it.each` initialBranchName | pageTitle diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js index e2a14a9102f..5a28c6d9789 100644 --- a/spec/frontend/jira_connect/subscriptions/api_spec.js +++ b/spec/frontend/jira_connect/subscriptions/api_spec.js @@ -17,7 +17,6 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({ describe('JiraConnect API', () => { let axiosMock; - let originalGon; let response; const mockAddPath = 'addPath'; @@ -29,13 +28,11 @@ describe('JiraConnect API', () => { beforeEach(() => { axiosMock = new MockAdapter(axiosInstance); - originalGon = window.gon; window.gon = { api_version: 'v4' }; }); afterEach(() => { axiosMock.restore(); - window.gon = originalGon; response = null; }); 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 9f92ad2adc1..934473c15ba 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 @@ -11,7 +11,7 @@ describe('AddNamespaceButton', () => { const createComponent = () => { wrapper = shallowMount(AddNamespaceButton, { directives: { - glModal: createMockDirective(), + glModal: createMockDirective('gl-modal'), }, }); }; @@ -23,10 +23,6 @@ describe('AddNamespaceButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays a button', () => { expect(findButton().exists()).toBe(true); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js index d80381107f2..dbe8a734bb4 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js @@ -17,10 +17,6 @@ describe('AddNamespaceModal', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays modal with correct props', () => { const modal = findModal(); expect(modal.exists()).toBe(true); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js index 5df54abfc05..e437e6e0398 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js @@ -40,10 +40,6 @@ describe('GroupsListItem', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGroupItemName = () => wrapper.findComponent(GroupItemName); const findLinkButton = () => wrapper.findComponent(GlButton); const clickLinkButton = () => findLinkButton().trigger('click'); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js index 97038a2a231..9d5bc8dff2a 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js @@ -48,10 +48,6 @@ describe('GroupsList', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAllItems = () => wrapper.findAllComponents(GroupsListItem); diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 369ddda8dbe..aa4feaa5261 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -41,10 +41,6 @@ describe('JiraConnectApp', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { describe.each` scenario | usersPath | shouldRenderSignInPage | shouldRenderSubscriptionsPage diff --git a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js index aa93a6be3c8..a8aa383d917 100644 --- a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js @@ -12,10 +12,6 @@ describe('BrowserSupportAlert', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findLink = () => wrapper.findComponent(GlLink); - afterEach(() => { - wrapper.destroy(); - }); - it('displays a non-dismissible alert', () => { createComponent(); diff --git a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js index b5fe08486b1..e4da10569f3 100644 --- a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js @@ -14,10 +14,6 @@ describe('GroupItemName', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('matches the snapshot', () => { createComponent(); diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js index 4ebfaed261e..0dadec598e4 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js @@ -22,10 +22,6 @@ describe('SignInLegacyButton', () => { const findButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - wrapper.destroy(); - }); - it('displays a button', () => { createComponent(); 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 e20c4b62e77..9d39b82c05f 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 @@ -56,10 +56,6 @@ describe('SignInOauthButton', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findButton = () => wrapper.findComponent(GlButton); describe('when `gitlabBasePath` is GitLab.com', () => { it('displays a button', () => { diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js index 2d7c58fc278..5337575d5ef 100644 --- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js @@ -29,10 +29,6 @@ describe('SubscriptionsList', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findUnlinkButton = () => wrapper.findComponent(GlButton); const clickUnlinkButton = () => findUnlinkButton().trigger('click'); diff --git a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js index e16121243a0..c0bd908da0f 100644 --- a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js @@ -28,10 +28,6 @@ describe('UserLink', () => { const findSprintf = () => wrapper.findComponent(GlSprintf); const findOauthButton = () => wrapper.findComponent(SignInOauthButton); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` userSignedIn | hasSubscriptions | expectGlSprintf | expectGlLink | expectOauthButton | jiraConnectOauthEnabled ${true} | ${false} | ${true} | ${false} | ${false} | ${false} diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js index b9a8451f3b3..be46c1d1609 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js @@ -42,10 +42,6 @@ describe('SignInGitlabCom', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { describe.each` scenario | hasSubscriptions | signInButtonText diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js index e98c6ff1054..d99d8986296 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js @@ -32,10 +32,6 @@ describe('SignInGitlabMultiversion', () => { wrapper = shallowMountExtended(SignInGitlabMultiversion); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when version is not selected', () => { describe('VersionSelectForm', () => { it('renders version select form', () => { diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js index 29e7fe7a5b2..428aa1d734b 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js @@ -17,10 +17,6 @@ describe('VersionSelectForm', () => { wrapper = shallowMountExtended(VersionSelectForm); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('default state', () => { beforeEach(() => { createComponent(); 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 b27eba6b040..7639c3a9c3f 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 @@ -34,10 +34,6 @@ describe('SignInPage', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` jiraConnectOauthEnabled | publicKeyStorageEnabled | shouldRenderDotCom | shouldRenderMultiversion ${false} | ${true} | ${true} | ${false} diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js index 4956af76ead..d262f4b2735 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js @@ -26,10 +26,6 @@ describe('SubscriptionsPage', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { describe.each` scenario | subscriptionsLoading | hasSubscriptions | expectSubscriptionsList | expectEmptyState diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap index 40e627262db..6766456d09c 100644 --- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap +++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap @@ -2,7 +2,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = ` <table - aria-busy="false" + aria-busy="" aria-colcount="3" class="table b-table gl-table b-table-fixed" role="table" @@ -92,7 +92,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" type="button" > @@ -217,7 +217,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" type="button" > diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js index 022a0f81aaa..dc1b75f5d9e 100644 --- a/spec/frontend/jira_import/components/jira_import_app_spec.js +++ b/spec/frontend/jira_import/components/jira_import_app_spec.js @@ -67,11 +67,6 @@ describe('JiraImportApp', () => { }, }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when Jira integration is not configured', () => { beforeEach(() => { wrapper = mountComponent({ isJiraConfigured: false }); diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js index d43a9f8a145..c7db9f429de 100644 --- a/spec/frontend/jira_import/components/jira_import_form_spec.js +++ b/spec/frontend/jira_import/components/jira_import_form_spec.js @@ -106,7 +106,6 @@ describe('JiraImportForm', () => { axiosMock.restore(); mutateSpy.mockRestore(); querySpy.mockRestore(); - wrapper.destroy(); }); describe('select dropdown project selection', () => { diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js index 42356763492..c0d415a2130 100644 --- a/spec/frontend/jira_import/components/jira_import_progress_spec.js +++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js @@ -25,11 +25,6 @@ describe('JiraImportProgress', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('empty state', () => { beforeEach(() => { wrapper = mountComponent(); diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js index 0085a2b5572..5331467d669 100644 --- a/spec/frontend/jira_import/components/jira_import_setup_spec.js +++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js @@ -17,11 +17,6 @@ describe('JiraImportSetup', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('contains illustration', () => { expect(getGlEmptyStateProp('svgPath')).toBe(illustration); }); diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js index 14613775791..5ecddc7efd6 100644 --- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js +++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js @@ -27,10 +27,6 @@ describe('Jobs filtered search', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('displays filtered search', () => { createComponent(); diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js index fbe5f6a2e11..6755b854f01 100644 --- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js +++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js @@ -45,10 +45,6 @@ describe('Job Status Token', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('passes config correctly', () => { expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); }); diff --git a/spec/frontend/jobs/components/job/artifacts_block_spec.js b/spec/frontend/jobs/components/job/artifacts_block_spec.js index c75deb64d84..ea5d727bd08 100644 --- a/spec/frontend/jobs/components/job/artifacts_block_spec.js +++ b/spec/frontend/jobs/components/job/artifacts_block_spec.js @@ -55,11 +55,6 @@ describe('Artifacts block', () => { locked: true, }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with expired artifacts that are not locked', () => { beforeEach(() => { wrapper = createWrapper({ diff --git a/spec/frontend/jobs/components/job/commit_block_spec.js b/spec/frontend/jobs/components/job/commit_block_spec.js index 4fcc754c82c..1c28b5079d7 100644 --- a/spec/frontend/jobs/components/job/commit_block_spec.js +++ b/spec/frontend/jobs/components/job/commit_block_spec.js @@ -32,10 +32,6 @@ describe('Commit block', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('without merge request', () => { beforeEach(() => { mountComponent(); diff --git a/spec/frontend/jobs/components/job/environments_block_spec.js b/spec/frontend/jobs/components/job/environments_block_spec.js index 134533e2af8..ab36f79ea5e 100644 --- a/spec/frontend/jobs/components/job/environments_block_spec.js +++ b/spec/frontend/jobs/components/job/environments_block_spec.js @@ -51,11 +51,6 @@ describe('Environments block', () => { const findEnvironmentLink = () => wrapper.find('[data-testid="job-environment-link"]'); const findClusterLink = () => wrapper.find('[data-testid="job-cluster-link"]'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with last deployment', () => { it('renders info for most recent deployment', () => { createComponent({ diff --git a/spec/frontend/jobs/components/job/erased_block_spec.js b/spec/frontend/jobs/components/job/erased_block_spec.js index c6aba01fa53..aeab676fc7e 100644 --- a/spec/frontend/jobs/components/job/erased_block_spec.js +++ b/spec/frontend/jobs/components/job/erased_block_spec.js @@ -18,10 +18,6 @@ describe('Erased block', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with job erased by user', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js index cefedcd82fb..394fc8ad43c 100644 --- a/spec/frontend/jobs/components/job/job_app_spec.js +++ b/spec/frontend/jobs/components/job/job_app_spec.js @@ -83,8 +83,9 @@ describe('Job App', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy + wrapper.destroy(); }); describe('while loading', () => { diff --git a/spec/frontend/jobs/components/job/job_container_item_spec.js b/spec/frontend/jobs/components/job/job_container_item_spec.js index 05c38dd74b7..8121aa1172f 100644 --- a/spec/frontend/jobs/components/job/job_container_item_spec.js +++ b/spec/frontend/jobs/components/job/job_container_item_spec.js @@ -24,11 +24,6 @@ describe('JobContainerItem', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when a job is not active and not retried', () => { beforeEach(() => { createComponent(job); diff --git a/spec/frontend/jobs/components/job/job_log_controllers_spec.js b/spec/frontend/jobs/components/job/job_log_controllers_spec.js index 5e9a73b4387..9917c63b2d0 100644 --- a/spec/frontend/jobs/components/job/job_log_controllers_spec.js +++ b/spec/frontend/jobs/components/job/job_log_controllers_spec.js @@ -285,6 +285,18 @@ describe('Job log controllers', () => { expect(findScrollFailure().props('disabled')).toBe(false); }); }); + + describe('on error', () => { + beforeEach(() => { + jest.spyOn(commonUtils, 'backOff').mockRejectedValueOnce(); + + createWrapper({}, { jobLogJumpToFailures: true }); + }); + + it('stays disabled', () => { + expect(findScrollFailure().props('disabled')).toBe(true); + }); + }); }); }); diff --git a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js index d60043f33f7..712269a1e83 100644 --- a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js +++ b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js @@ -64,13 +64,11 @@ describe('Job Retry Forward Deployment Modal', () => { beforeEach(createWrapper); it('should correctly configure the primary action', () => { - expect(findModal().props('actionPrimary').attributes).toMatchObject([ - { - 'data-method': 'post', - href: job.retry_path, - variant: 'danger', - }, - ]); + expect(findModal().props('actionPrimary').attributes).toMatchObject({ + 'data-method': 'post', + href: job.retry_path, + variant: 'danger', + }); }); }); }); diff --git a/spec/frontend/jobs/components/job/jobs_container_spec.js b/spec/frontend/jobs/components/job/jobs_container_spec.js index 2fde4d3020b..05660880751 100644 --- a/spec/frontend/jobs/components/job/jobs_container_spec.js +++ b/spec/frontend/jobs/components/job/jobs_container_spec.js @@ -68,10 +68,6 @@ describe('Jobs List block', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders a list of jobs', () => { createComponent({ jobs: [job, retried, active], diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js index a5b3b0e3b47..98b9ca78a45 100644 --- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js +++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js @@ -2,24 +2,28 @@ import { GlSprintf, GlLink } from '@gitlab/ui'; import { createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import { nextTick } from 'vue'; +import { createAlert } from '~/alert'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants'; +import { JOB_GRAPHQL_ERRORS } from '~/jobs/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import waitForPromises from 'helpers/wait_for_promises'; import { redirectTo } from '~/lib/utils/url_utility'; import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue'; import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql'; -import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql'; +import playJobMutation from '~/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql'; import { mockFullPath, mockId, mockJobResponse, mockJobWithVariablesResponse, - mockJobMutationData, + mockJobPlayMutationData, + mockJobRetryMutationData, } from './mock_data'; const localVue = createLocalVue(); +jest.mock('~/alert'); localVue.use(VueApollo); jest.mock('~/lib/utils/url_utility', () => ({ @@ -39,9 +43,9 @@ describe('Manual Variables Form', () => { const createComponent = ({ options = {}, props = {} } = {}) => { wrapper = mountExtended(ManualVariablesForm, { propsData: { - ...props, jobId: mockId, - isRetryable: true, + isRetryable: false, + ...props, }, provide: { ...defaultProvide, @@ -71,7 +75,7 @@ describe('Manual Variables Form', () => { const findHelpText = () => wrapper.findComponent(GlSprintf); const findHelpLink = () => wrapper.findComponent(GlLink); const findCancelBtn = () => wrapper.findByTestId('cancel-btn'); - const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn'); + const findRunBtn = () => wrapper.findByTestId('run-manual-job-btn'); const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn'); const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn'); const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder'); @@ -97,7 +101,7 @@ describe('Manual Variables Form', () => { }); afterEach(() => { - wrapper.destroy(); + createAlert.mockClear(); }); describe('when page renders', () => { @@ -112,10 +116,30 @@ describe('Manual Variables Form', () => { '/help/ci/variables/index#add-a-cicd-variable-to-a-project', ); }); + }); - it('renders buttons', () => { - expect(findCancelBtn().exists()).toBe(true); - expect(findRerunBtn().exists()).toBe(true); + describe('when query is unsuccessful', () => { + beforeEach(async () => { + getJobQueryResponse.mockRejectedValue({}); + await createComponentWithApollo(); + }); + + it('shows an alert with error', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: JOB_GRAPHQL_ERRORS.jobQueryErrorText, + }); + }); + }); + + describe('when job has not been retried', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse); + await createComponentWithApollo(); + }); + + it('does not render the cancel button', () => { + expect(findCancelBtn().exists()).toBe(false); + expect(findRunBtn().exists()).toBe(true); }); }); @@ -135,10 +159,10 @@ describe('Manual Variables Form', () => { }); }); - describe('when mutation fires', () => { + describe('when play mutation fires', () => { beforeEach(async () => { await createComponentWithApollo(); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobPlayMutationData); }); it('passes variables in correct format', async () => { @@ -146,11 +170,11 @@ describe('Manual Variables Form', () => { await findCiVariableValue().setValue('new value'); - await findRerunBtn().vm.$emit('click'); + await findRunBtn().vm.$emit('click'); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: retryJobMutation, + mutation: playJobMutation, variables: { id: convertToGraphQLId(TYPENAME_CI_BUILD, mockId), variables: [ @@ -163,13 +187,63 @@ describe('Manual Variables Form', () => { }); }); - // redirect to job after initial trigger assertion will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/377268 + it('redirects to job properly after job is run', async () => { + findRunBtn().vm.$emit('click'); + await waitForPromises(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); + expect(redirectTo).toHaveBeenCalledWith(mockJobPlayMutationData.data.jobPlay.job.webPath); + }); + }); + + describe('when play mutation is unsuccessful', () => { + beforeEach(async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({}); + await createComponentWithApollo(); + }); + + it('shows an alert with error', async () => { + findRunBtn().vm.$emit('click'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: JOB_GRAPHQL_ERRORS.jobMutationErrorText, + }); + }); + }); + + describe('when job is retryable', () => { + beforeEach(async () => { + await createComponentWithApollo({ props: { isRetryable: true } }); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobRetryMutationData); + }); + + it('renders cancel button', () => { + expect(findCancelBtn().exists()).toBe(true); + }); + it('redirects to job properly after rerun', async () => { - findRerunBtn().vm.$emit('click'); + findRunBtn().vm.$emit('click'); await waitForPromises(); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); - expect(redirectTo).toHaveBeenCalledWith(mockJobMutationData.data.jobRetry.job.webPath); + expect(redirectTo).toHaveBeenCalledWith(mockJobRetryMutationData.data.jobRetry.job.webPath); + }); + }); + + describe('when retry mutation is unsuccessful', () => { + beforeEach(async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({}); + await createComponentWithApollo({ props: { isRetryable: true } }); + }); + + it('shows an alert with error', async () => { + findRunBtn().vm.$emit('click'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: JOB_GRAPHQL_ERRORS.jobMutationErrorText, + }); }); }); diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js index 8a838acca7a..fb3a361c9c9 100644 --- a/spec/frontend/jobs/components/job/mock_data.js +++ b/spec/frontend/jobs/components/job/mock_data.js @@ -50,7 +50,32 @@ export const mockJobWithVariablesResponse = { }, }; -export const mockJobMutationData = { +export const mockJobPlayMutationData = { + data: { + jobPlay: { + job: { + id: 'gid://gitlab/Ci::Build/401', + manualVariables: { + nodes: [ + { + id: 'gid://gitlab/Ci::JobVariable/151', + key: 'new key', + value: 'new value', + __typename: 'CiManualVariable', + }, + ], + __typename: 'CiManualVariableConnection', + }, + webPath: '/Commit451/lab-coat/-/jobs/401', + __typename: 'CiJob', + }, + errors: [], + __typename: 'JobPlayPayload', + }, + }, +}; + +export const mockJobRetryMutationData = { data: { jobRetry: { job: { diff --git a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js index 5c9c011b4ab..dd5a9e3491d 100644 --- a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js +++ b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js @@ -19,11 +19,6 @@ describe('Sidebar detail row', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with title/value and without helpUrl', () => { beforeEach(() => { createComponent({ title, value }); diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js index aa9ca932023..cefa4582c15 100644 --- a/spec/frontend/jobs/components/job/sidebar_spec.js +++ b/spec/frontend/jobs/components/job/sidebar_spec.js @@ -48,10 +48,6 @@ describe('Sidebar details block', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('without terminal path', () => { it('does not render terminal link', async () => { createWrapper(); diff --git a/spec/frontend/jobs/components/job/stages_dropdown_spec.js b/spec/frontend/jobs/components/job/stages_dropdown_spec.js index 61dec585e82..f782d5600e6 100644 --- a/spec/frontend/jobs/components/job/stages_dropdown_spec.js +++ b/spec/frontend/jobs/components/job/stages_dropdown_spec.js @@ -37,10 +37,6 @@ describe('Stages Dropdown', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('without a merge request pipeline', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/jobs/components/job/trigger_block_spec.js b/spec/frontend/jobs/components/job/trigger_block_spec.js index a1de8fd143f..8bb2c1f3ad8 100644 --- a/spec/frontend/jobs/components/job/trigger_block_spec.js +++ b/spec/frontend/jobs/components/job/trigger_block_spec.js @@ -20,10 +20,6 @@ describe('Trigger block', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with short token and no variables', () => { it('renders short token', () => { createComponent({ diff --git a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js index fb7d389c4d6..1072cdd6781 100644 --- a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js +++ b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js @@ -18,10 +18,6 @@ describe('Unmet Prerequisites Block Job component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders an alert with the correct message', () => { const container = wrapper.findComponent(GlAlert); const alertMessage = diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js index 646935568b1..5adedea28a5 100644 --- a/spec/frontend/jobs/components/log/collapsible_section_spec.js +++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js @@ -19,10 +19,6 @@ describe('Job Log Collapsible Section', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with closed section', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/jobs/components/log/duration_badge_spec.js b/spec/frontend/jobs/components/log/duration_badge_spec.js index 84dae386bdb..644d05366a0 100644 --- a/spec/frontend/jobs/components/log/duration_badge_spec.js +++ b/spec/frontend/jobs/components/log/duration_badge_spec.js @@ -20,10 +20,6 @@ describe('Job Log Duration Badge', () => { createComponent(data); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders provided duration', () => { expect(wrapper.text()).toBe(data.duration); }); diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js index ec8e79bba13..16fe753e08a 100644 --- a/spec/frontend/jobs/components/log/line_header_spec.js +++ b/spec/frontend/jobs/components/log/line_header_spec.js @@ -29,10 +29,6 @@ describe('Job Log Header Line', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('line', () => { beforeEach(() => { createComponent(data); diff --git a/spec/frontend/jobs/components/log/line_number_spec.js b/spec/frontend/jobs/components/log/line_number_spec.js index 96aa31baab9..4130c124a30 100644 --- a/spec/frontend/jobs/components/log/line_number_spec.js +++ b/spec/frontend/jobs/components/log/line_number_spec.js @@ -21,10 +21,6 @@ describe('Job Log Line Number', () => { createComponent(data); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders incremented lineNunber by 1', () => { expect(wrapper.text()).toBe('1'); }); diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js index c933ed5c3e1..265f72ff344 100644 --- a/spec/frontend/jobs/components/log/log_spec.js +++ b/spec/frontend/jobs/components/log/log_spec.js @@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import Log from '~/jobs/components/log/log.vue'; +import LogLineHeader from '~/jobs/components/log/line_header.vue'; import { logLinesParser } from '~/jobs/store/utils'; import { jobLog } from './mock_data'; @@ -10,6 +11,7 @@ describe('Job Log', () => { let actions; let state; let store; + let toggleCollapsibleLineMock; Vue.use(Vuex); @@ -20,8 +22,9 @@ describe('Job Log', () => { }; beforeEach(() => { + toggleCollapsibleLineMock = jest.fn(); actions = { - toggleCollapsibleLine: () => {}, + toggleCollapsibleLine: toggleCollapsibleLineMock, }; state = { @@ -37,11 +40,7 @@ describe('Job Log', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - - const findCollapsibleLine = () => wrapper.find('.collapsible-line'); + const findCollapsibleLine = () => wrapper.findComponent(LogLineHeader); describe('line numbers', () => { it('renders a line number for each open line', () => { @@ -68,11 +67,9 @@ describe('Job Log', () => { describe('on click header section', () => { it('calls toggleCollapsibleLine', () => { - jest.spyOn(wrapper.vm, 'toggleCollapsibleLine'); - findCollapsibleLine().trigger('click'); - expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled(); + expect(toggleCollapsibleLineMock).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js index 763a4b0eaa2..d015edb0e91 100644 --- a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js @@ -22,10 +22,6 @@ describe('Duration Cell', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - it('does not display duration or finished time when no properties are present', () => { createComponent(); diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js index ddc196129a7..73e37eed5f1 100644 --- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js @@ -39,10 +39,6 @@ describe('Job Cell', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Job Id', () => { it('displays the job id and links to the job', () => { createComponent(); diff --git a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js index 1f5e0a7aa21..3d424b20964 100644 --- a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js @@ -42,10 +42,6 @@ describe('Pipeline Cell', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Pipeline Id', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js index 88c97285b85..e3b1ca1cce3 100644 --- a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js +++ b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js @@ -84,4 +84,23 @@ describe('jobs/components/table/graphql/cache_config', () => { expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length); }); }); + + describe('when incoming data has no nodes', () => { + it('should return existing cache', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + { __typename: 'CiJobConnection', count: 500 }, + { + args: { statuses: 'SUCCESS' }, + }, + ); + + const expectedResponse = { + ...CIJobConnectionExistingCache, + statuses: 'SUCCESS', + }; + + expect(res).toEqual(expectedResponse); + }); + }); }); diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js index 109cef6f817..6247cfcc640 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -12,8 +12,9 @@ import { s__ } from '~/locale'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql'; +import getJobsCountQuery from '~/jobs/components/table/graphql/queries/get_jobs_count.query.graphql'; import JobsTable from '~/jobs/components/table/jobs_table.vue'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; @@ -23,12 +24,13 @@ import { mockJobsResponsePaginated, mockJobsResponseEmpty, mockFailedSearchToken, + mockJobsCountResponse, } from '../../mock_data'; const projectPath = 'gitlab-org/gitlab'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Job table app', () => { let wrapper; @@ -37,6 +39,8 @@ describe('Job table app', () => { const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty); + const countSuccessHandler = jest.fn().mockResolvedValue(mockJobsCountResponse); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); const findTable = () => wrapper.findComponent(JobsTable); @@ -48,14 +52,18 @@ describe('Job table app', () => { const triggerInfiniteScroll = () => wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); - const createMockApolloProvider = (handler) => { - const requestHandlers = [[getJobsQuery, handler]]; + const createMockApolloProvider = (handler, countHandler) => { + const requestHandlers = [ + [getJobsQuery, handler], + [getJobsCountQuery, countHandler], + ]; return createMockApollo(requestHandlers); }; const createComponent = ({ handler = successHandler, + countHandler = countSuccessHandler, mountFn = shallowMount, data = {}, } = {}) => { @@ -68,14 +76,10 @@ describe('Job table app', () => { provide: { fullPath: projectPath, }, - apolloProvider: createMockApolloProvider(handler), + apolloProvider: createMockApolloProvider(handler, countHandler), }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('loading state', () => { it('should display skeleton loader when loading', () => { createComponent(); @@ -148,12 +152,39 @@ describe('Job table app', () => { }); describe('error state', () => { - it('should show an alert if there is an error fetching the data', async () => { + it('should show an alert if there is an error fetching the jobs data', async () => { createComponent({ handler: failedHandler }); await waitForPromises(); - expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe('There was an error fetching the jobs for your project.'); + expect(findTable().exists()).toBe(false); + }); + + it('should show an alert if there is an error fetching the jobs count data', async () => { + createComponent({ handler: successHandler, countHandler: failedHandler }); + + await waitForPromises(); + + expect(findAlert().text()).toBe( + 'There was an error fetching the number of jobs for your project.', + ); + }); + + it('jobs table should still load if count query fails', async () => { + createComponent({ handler: successHandler, countHandler: failedHandler }); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + }); + + it('jobs count should be zero if count query fails', async () => { + createComponent({ handler: successHandler, countHandler: failedHandler }); + + await waitForPromises(); + + expect(findTabs().props('allJobsCount')).toBe(0); }); }); diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js index 3c4f2d624fe..06b13aa4372 100644 --- a/spec/frontend/jobs/components/table/jobs_table_spec.js +++ b/spec/frontend/jobs/components/table/jobs_table_spec.js @@ -30,10 +30,6 @@ describe('Jobs Table', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays the jobs table', () => { expect(findTable().exists()).toBe(true); }); diff --git a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js index 23632001060..70bcac82a3f 100644 --- a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js +++ b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js @@ -42,10 +42,6 @@ describe('Jobs Table Tabs', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays All tab with count', () => { expect(trimText(findAllTab().text())).toBe(`All ${defaultProps.allJobsCount}`); }); diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js index 1d3845b19bb..098a63719fe 100644 --- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js +++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js @@ -16,11 +16,6 @@ describe('DelayedJobMixin', () => { template: '<div>{{remainingTime}}</div>', }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('if job is empty object', () => { beforeEach(() => { wrapper = shallowMount(dummyComponent, { diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 9abd610c26d..483b4ca711f 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -1,3 +1,4 @@ +import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json'; import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json'; import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json'; import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json'; @@ -13,6 +14,7 @@ export const mockJobsResponsePaginated = mockJobsPaginated; export const mockJobsResponseEmpty = mockJobsEmpty; export const mockJobsNodes = mockJobs.data.project.jobs.nodes; export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes; +export const mockJobsCountResponse = mockJobsCount; export const stages = [ { diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js index 24a803d3f16..7654d218209 100644 --- a/spec/frontend/labels/components/delete_label_modal_spec.js +++ b/spec/frontend/labels/components/delete_label_modal_spec.js @@ -30,10 +30,6 @@ describe('~/labels/components/delete_label_modal', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - const findModal = () => wrapper.findComponent(GlModal); const findPrimaryModalButton = () => wrapper.findByTestId('delete-button'); diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js index 97913c20229..5983c16a9d1 100644 --- a/spec/frontend/labels/components/promote_label_modal_spec.js +++ b/spec/frontend/labels/components/promote_label_modal_spec.js @@ -41,7 +41,6 @@ describe('Promote label modal', () => { afterEach(() => { axiosMock.reset(); - wrapper.destroy(); }); describe('Modal title and description', () => { diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js index 7f6fb138d89..036ff55fef7 100644 --- a/spec/frontend/language_switcher/components/app_spec.js +++ b/spec/frontend/language_switcher/components/app_spec.js @@ -24,10 +24,6 @@ describe('<LanguageSwitcher />', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const getPreferredLanguage = () => wrapper.find('.gl-new-dropdown-button-text').text(); const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`); const findFooter = () => wrapper.findByTestId('footer'); diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js index 5ac7a7985a8..b8847f0fca3 100644 --- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js +++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js @@ -6,13 +6,8 @@ import { isNavigatingAway } from '~/lib/utils/is_navigating_away'; jest.mock('~/lib/utils/is_navigating_away'); describe('getSuppressNetworkErrorsDuringNavigationLink', () => { - const originalGon = window.gon; let subscription; - beforeEach(() => { - window.gon = originalGon; - }); - afterEach(() => { if (subscription) { subscription.unsubscribe(); diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js index f767a673553..fdc8789c1a8 100644 --- a/spec/frontend/lib/dompurify_spec.js +++ b/spec/frontend/lib/dompurify_spec.js @@ -49,8 +49,6 @@ const forbiddenDataAttrs = defaultConfig.FORBID_ATTR; const acceptedDataAttrs = ['data-random', 'data-custom']; describe('~/lib/dompurify', () => { - let originalGon; - it('uses local configuration when given', () => { // As dompurify uses a "Persistent Configuration", it might // ignore config, this check verifies we respect @@ -104,15 +102,10 @@ describe('~/lib/dompurify', () => { ${'root'} | ${rootGon} ${'absolute'} | ${absoluteGon} `('when gon contains $type icon urls', ({ type, gon }) => { - beforeAll(() => { - originalGon = window.gon; + beforeEach(() => { window.gon = gon; }); - afterAll(() => { - window.gon = originalGon; - }); - it('allows no href attrs', () => { const htmlHref = `<svg><use></use></svg>`; expect(sanitize(htmlHref)).toBe(htmlHref); @@ -137,14 +130,9 @@ describe('~/lib/dompurify', () => { describe('when gon does not contain icon urls', () => { beforeAll(() => { - originalGon = window.gon; window.gon = {}; }); - afterAll(() => { - window.gon = originalGon; - }); - it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', (url) => { const htmlHref = `<svg><use href="${url}"></use></svg>`; const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`; diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js index 4471b781446..3d063ff9b46 100644 --- a/spec/frontend/lib/utils/axios_startup_calls_spec.js +++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js @@ -113,17 +113,10 @@ describe('setupAxiosStartupCalls', () => { }); describe('startup call', () => { - let oldGon; - beforeEach(() => { - oldGon = window.gon; window.gon = { gitlab_url: 'https://example.org/gitlab' }; }); - afterEach(() => { - window.gon = oldGon; - }); - it('removes GitLab Base URL from startup call', async () => { window.gl.startup_calls = { '/startup': { diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 7b068f7d248..b4ec00ab766 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -534,18 +534,10 @@ describe('common_utils', () => { }); describe('spriteIcon', () => { - let beforeGon; - beforeEach(() => { - window.gon = window.gon || {}; - beforeGon = { ...window.gon }; window.gon.sprite_icons = 'icons.svg'; }); - afterEach(() => { - window.gon = beforeGon; - }); - it('should return the svg for a linked icon', () => { expect(commonUtils.spriteIcon('test')).toEqual( '<svg ><use xlink:href="icons.svg#test" /></svg>', diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js index 142c76f7bc0..9b790e739fb 100644 --- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js +++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js @@ -44,7 +44,6 @@ describe('confirmAction', () => { resetHTMLFixture(); Vue.prototype.$mount.mockRestore(); modalWrapper?.destroy(); - modalWrapper = null; modal?.destroy(); modal = null; }); 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 313e028d861..c135180c9df 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 @@ -28,10 +28,6 @@ describe('Confirm Modal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlModal = () => wrapper.findComponent(GlModal); describe('Modal events', () => { diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js index 1ef7047d959..c13d55f978e 100644 --- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js +++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js @@ -3,16 +3,6 @@ import { s__ } from '~/locale'; import '~/commons/bootstrap'; describe('TimeAgo utils', () => { - let oldGon; - - afterEach(() => { - window.gon = oldGon; - }); - - beforeEach(() => { - oldGon = window.gon; - }); - describe('getTimeago', () => { describe('with User Setting timeDisplayRelative: true', () => { beforeEach(() => { diff --git a/spec/frontend/lib/utils/error_message_spec.js b/spec/frontend/lib/utils/error_message_spec.js new file mode 100644 index 00000000000..17b5168c32f --- /dev/null +++ b/spec/frontend/lib/utils/error_message_spec.js @@ -0,0 +1,65 @@ +import { parseErrorMessage, USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message'; + +const defaultErrorMessage = 'Something caused this error'; +const userFacingErrorMessage = 'User facing error message'; +const nonUserFacingErrorMessage = 'NonUser facing error message'; +const genericErrorMessage = 'Some error message'; + +describe('error message', () => { + describe('when given an errormessage object', () => { + const errorMessageObject = { + options: { + cause: defaultErrorMessage, + }, + filename: 'error.js', + linenumber: 7, + }; + + it('returns the correct values for userfacing errors', () => { + const userFacingObject = errorMessageObject; + userFacingObject.message = `${USER_FACING_ERROR_MESSAGE_PREFIX} ${userFacingErrorMessage}`; + + expect(parseErrorMessage(userFacingObject)).toEqual({ + message: userFacingErrorMessage, + userFacing: true, + }); + }); + + it('returns the correct values for non userfacing errors', () => { + const nonUserFacingObject = errorMessageObject; + nonUserFacingObject.message = nonUserFacingErrorMessage; + + expect(parseErrorMessage(nonUserFacingObject)).toEqual({ + message: nonUserFacingErrorMessage, + userFacing: false, + }); + }); + }); + + describe('when given an errormessage string', () => { + it('returns the correct values for userfacing errors', () => { + expect( + parseErrorMessage(`${USER_FACING_ERROR_MESSAGE_PREFIX} ${genericErrorMessage}`), + ).toEqual({ + message: genericErrorMessage, + userFacing: true, + }); + }); + + it('returns the correct values for non userfacing errors', () => { + expect(parseErrorMessage(genericErrorMessage)).toEqual({ + message: genericErrorMessage, + userFacing: false, + }); + }); + }); + + describe('when given nothing', () => { + it('returns an empty error message', () => { + expect(parseErrorMessage()).toEqual({ + message: '', + userFacing: false, + }); + }); + }); +}); diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js index f63af2fe0a4..509ddc7ce86 100644 --- a/spec/frontend/lib/utils/file_upload_spec.js +++ b/spec/frontend/lib/utils/file_upload_spec.js @@ -1,5 +1,9 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload'; +import fileUpload, { + getFilename, + validateImageName, + validateFileFromAllowList, +} from '~/lib/utils/file_upload'; describe('File upload', () => { beforeEach(() => { @@ -89,3 +93,19 @@ describe('file name validator', () => { expect(validateImageName(file)).toBe('image.png'); }); }); + +describe('validateFileFromAllowList', () => { + it('returns true if the file type is in the allowed list', () => { + const allowList = ['.foo', '.bar']; + const fileName = 'file.foo'; + + expect(validateFileFromAllowList(fileName, allowList)).toBe(true); + }); + + it('returns false if the file type is in the allowed list', () => { + const allowList = ['.foo', '.bar']; + const fileName = 'file.baz'; + + expect(validateFileFromAllowList(fileName, allowList)).toBe(false); + }); +}); diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js index dc4aa0ea5ed..d2591cd2328 100644 --- a/spec/frontend/lib/utils/number_utility_spec.js +++ b/spec/frontend/lib/utils/number_utility_spec.js @@ -3,6 +3,7 @@ import { bytesToKiB, bytesToMiB, bytesToGiB, + numberToHumanSizeSplit, numberToHumanSize, numberToMetricPrefix, sum, @@ -13,6 +14,12 @@ import { isNumeric, isPositiveInteger, } from '~/lib/utils/number_utils'; +import { + BYTES_FORMAT_BYTES, + BYTES_FORMAT_KIB, + BYTES_FORMAT_MIB, + BYTES_FORMAT_GIB, +} from '~/lib/utils/constants'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { @@ -78,6 +85,28 @@ describe('Number Utils', () => { }); }); + describe('numberToHumanSizeSplit', () => { + it('should return bytes', () => { + expect(numberToHumanSizeSplit(654)).toEqual(['654', BYTES_FORMAT_BYTES]); + expect(numberToHumanSizeSplit(-654)).toEqual(['-654', BYTES_FORMAT_BYTES]); + }); + + it('should return KiB', () => { + expect(numberToHumanSizeSplit(1079)).toEqual(['1.05', BYTES_FORMAT_KIB]); + expect(numberToHumanSizeSplit(-1079)).toEqual(['-1.05', BYTES_FORMAT_KIB]); + }); + + it('should return MiB', () => { + expect(numberToHumanSizeSplit(10485764)).toEqual(['10.00', BYTES_FORMAT_MIB]); + expect(numberToHumanSizeSplit(-10485764)).toEqual(['-10.00', BYTES_FORMAT_MIB]); + }); + + it('should return GiB', () => { + expect(numberToHumanSizeSplit(10737418240)).toEqual(['10.00', BYTES_FORMAT_GIB]); + expect(numberToHumanSizeSplit(-10737418240)).toEqual(['-10.00', BYTES_FORMAT_GIB]); + }); + }); + describe('numberToHumanSize', () => { it('should return bytes', () => { expect(numberToHumanSize(654)).toEqual('654 bytes'); diff --git a/spec/frontend/lib/utils/ref_validator_spec.js b/spec/frontend/lib/utils/ref_validator_spec.js new file mode 100644 index 00000000000..7185ebf0a24 --- /dev/null +++ b/spec/frontend/lib/utils/ref_validator_spec.js @@ -0,0 +1,79 @@ +import { validateTag, validationMessages } from '~/lib/utils/ref_validator'; + +describe('~/lib/utils/ref_validator', () => { + describe('validateTag', () => { + describe.each([ + ['foo'], + ['FOO'], + ['foo/a.lockx'], + ['foo.123'], + ['foo/123'], + ['foo/bar/123'], + ['foo.bar.123'], + ['foo-bar_baz'], + ['head'], + ['"foo"-'], + ['foo@bar'], + ['\ud83e\udd8a'], + ['ünicöde'], + ['\x80}'], + ])('tag with the name "%s"', (tagName) => { + it('is valid', () => { + const result = validateTag(tagName); + expect(result.isValid).toBe(true); + expect(result.validationErrors).toEqual([]); + }); + }); + + describe.each([ + [' ', validationMessages.EmptyNameValidationMessage], + + ['refs/heads/tagName', validationMessages.DisallowedPrefixesValidationMessage], + ['/foo', validationMessages.DisallowedPrefixesValidationMessage], + ['-tagName', validationMessages.DisallowedPrefixesValidationMessage], + + ['HEAD', validationMessages.DisallowedNameValidationMessage], + ['@', validationMessages.DisallowedNameValidationMessage], + + ['tag name with spaces', validationMessages.DisallowedSubstringsValidationMessage], + ['tag\\name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag^name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag..name', validationMessages.DisallowedSubstringsValidationMessage], + ['..', validationMessages.DisallowedSubstringsValidationMessage], + ['tag?name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag*name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag[name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag@{name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag:name', validationMessages.DisallowedSubstringsValidationMessage], + ['tag~name', validationMessages.DisallowedSubstringsValidationMessage], + + ['/', validationMessages.DisallowedSequenceEmptyValidationMessage], + ['//', validationMessages.DisallowedSequenceEmptyValidationMessage], + ['foo//123', validationMessages.DisallowedSequenceEmptyValidationMessage], + + ['.', validationMessages.DisallowedSequencePrefixesValidationMessage], + ['/./', validationMessages.DisallowedSequencePrefixesValidationMessage], + ['./.', validationMessages.DisallowedSequencePrefixesValidationMessage], + ['.tagName', validationMessages.DisallowedSequencePrefixesValidationMessage], + ['tag/.Name', validationMessages.DisallowedSequencePrefixesValidationMessage], + ['foo/.123/bar', validationMessages.DisallowedSequencePrefixesValidationMessage], + + ['foo.', validationMessages.DisallowedSequencePostfixesValidationMessage], + ['a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage], + ['foo/a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage], + ['foo/a.lock/b', validationMessages.DisallowedSequencePostfixesValidationMessage], + ['foo.123.', validationMessages.DisallowedSequencePostfixesValidationMessage], + + ['foo/', validationMessages.DisallowedPostfixesValidationMessage], + + ['control-character\x7f', validationMessages.ControlCharactersValidationMessage], + ['control-character\x15', validationMessages.ControlCharactersValidationMessage], + ])('tag with name "%s"', (tagName, validationMessage) => { + it(`should be invalid with validation message "${validationMessage}"`, () => { + const result = validateTag(tagName); + expect(result.isValid).toBe(false); + expect(result.validationErrors).toContain(validationMessage); + }); + }); + }); +}); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 7aab1013fc0..2180ea7e6c2 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -1,12 +1,16 @@ import $ from 'jquery'; +import AxiosMockAdapter from 'axios-mock-adapter'; import { insertMarkdownText, keypressNoteText, compositionStartNoteText, compositionEndNoteText, updateTextForToolbarBtn, + resolveSelectedImage, } from '~/lib/utils/text_markdown'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import '~/lib/utils/jquery_at_who'; +import axios from '~/lib/utils/axios_utils'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; describe('init markdown', () => { @@ -14,6 +18,7 @@ describe('init markdown', () => { let textArea; let indentButton; let outdentButton; + let axiosMock; beforeAll(() => { setHTMLFixture( @@ -34,6 +39,14 @@ describe('init markdown', () => { document.execCommand = jest.fn(() => false); }); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + afterAll(() => { resetHTMLFixture(); }); @@ -707,6 +720,55 @@ describe('init markdown', () => { }); }); + describe('resolveSelectedImage', () => { + const markdownPreviewPath = '/markdown/preview'; + const imageMarkdown = '![image](/uploads/image.png)'; + const imageAbsoluteUrl = '/abs/uploads/image.png'; + + describe('when textarea cursor is positioned on an image', () => { + beforeEach(() => { + axiosMock.onPost(markdownPreviewPath, { text: imageMarkdown }).reply(HTTP_STATUS_OK, { + body: ` + <p><a href="${imageAbsoluteUrl}"><img src="${imageAbsoluteUrl}"></a></p> + `, + }); + }); + + it('returns the image absolute URL, markdown, and filename', async () => { + textArea.value = `image ${imageMarkdown}`; + textArea.setSelectionRange(8, 8); + expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toEqual({ + imageURL: imageAbsoluteUrl, + imageMarkdown, + filename: 'image.png', + }); + }); + }); + + describe('when textarea cursor is not positioned on an image', () => { + it.each` + markdown | selectionRange + ${`image ${imageMarkdown}`} | ${[4, 4]} + ${`!2 (issue)`} | ${[2, 2]} + `('returns null', async ({ markdown, selectionRange }) => { + textArea.value = markdown; + textArea.setSelectionRange(...selectionRange); + expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null); + }); + }); + + describe('when textarea cursor is positioned between images', () => { + it('returns null', async () => { + const position = imageMarkdown.length + 1; + + textArea.value = `${imageMarkdown}\n\n${imageMarkdown}`; + textArea.setSelectionRange(position, position); + + expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null); + }); + }); + }); + describe('Source Editor', () => { let editor; diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index f2572ca0ad2..71a84d56791 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -398,4 +398,36 @@ describe('text_utility', () => { expect(textUtils.base64DecodeUnicode('8J+YgA==')).toBe('😀'); }); }); + + describe('findInvalidBranchNameCharacters', () => { + const invalidChars = [' ', '~', '^', ':', '?', '*', '[', '..', '@{', '\\', '//']; + const badBranchName = 'branch-with all these ~ ^ : ? * [ ] \\ // .. @{ } //'; + const goodBranch = 'branch-with-no-errrors'; + + it('returns an array of invalid characters in a branch name', () => { + const chars = textUtils.findInvalidBranchNameCharacters(badBranchName); + chars.forEach((char) => { + expect(invalidChars).toContain(char); + }); + }); + + it('returns an empty array with no invalid characters', () => { + expect(textUtils.findInvalidBranchNameCharacters(goodBranch)).toEqual([]); + }); + }); + + describe('humanizeBranchValidationErrors', () => { + it.each` + errors | message + ${[' ']} | ${"Can't contain spaces"} + ${['?', '//', ' ']} | ${"Can't contain spaces, ?, //"} + ${['\\', '[', '..']} | ${"Can't contain \\, [, .."} + `('returns an $message with $errors', ({ errors, message }) => { + expect(textUtils.humanizeBranchValidationErrors(errors)).toEqual(message); + }); + + it('returns an empty string with no invalid characters', () => { + expect(textUtils.humanizeBranchValidationErrors([])).toEqual(''); + }); + }); }); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 6afdab455a6..72556e6bbe2 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -45,10 +45,6 @@ describe('URL utility', () => { }); describe('webIDEUrl', () => { - afterEach(() => { - gon.relative_url_root = ''; - }); - it('escapes special characters', () => { expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-#-foss/merge_requests/1')).toBe( '/-/ide/project/gitlab-org/gitlab-%23-foss/merge_requests/1', @@ -505,10 +501,6 @@ describe('URL utility', () => { gon.gitlab_url = gitlabUrl; }); - afterEach(() => { - gon.gitlab_url = ''; - }); - it.each` url | urlType | external ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false} diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js index d25a692dfea..abd5095c1d2 100644 --- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js +++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js @@ -96,10 +96,6 @@ describe('~/lib/utils/vuex_module_mappers', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('from module defined by prop', () => { it('maps state', () => { expect(getMappedState()).toEqual({ diff --git a/spec/frontend/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js index e0d0e117ea4..a7e245e2b78 100644 --- a/spec/frontend/locale/sprintf_spec.js +++ b/spec/frontend/locale/sprintf_spec.js @@ -84,5 +84,16 @@ describe('locale', () => { expect(output).toBe('contains duplicated 15%'); }); }); + + describe('ignores special replacements in the input', () => { + it.each(['$$', '$&', '$`', `$'`])('replacement "%s" is ignored', (replacement) => { + const input = 'My odd %{replacement} is preserved'; + + const parameters = { replacement }; + + const output = sprintf(input, parameters, false); + expect(output).toBe(`My odd ${replacement} is preserved`); + }); + }); }); }); 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 b94964dc482..c2e0e44f97f 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 @@ -20,10 +20,6 @@ describe('AccessRequestActionButtons', () => { const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton); const findApproveButton = () => wrapper.findComponent(ApproveAccessRequestButton); - afterEach(() => { - wrapper.destroy(); - }); - it('renders remove member button', () => { createComponent(); 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 15bb03480e1..7a4cd844425 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 @@ -38,7 +38,7 @@ describe('ApproveAccessRequestButton', () => { ...propsData, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -50,10 +50,6 @@ describe('ApproveAccessRequestButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays a tooltip', () => { const button = findButton(); 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 68009708c99..a852443844b 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 @@ -19,10 +19,6 @@ describe('InviteActionButtons', () => { const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton); const findResendInviteButton = () => wrapper.findComponent(ResendInviteButton); - afterEach(() => { - wrapper.destroy(); - }); - describe('when user has `canRemove` permissions', () => { beforeEach(() => { createComponent({ 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 b511cebdf28..1d83a2e0e71 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 @@ -37,7 +37,7 @@ describe('RemoveGroupLinkButton', () => { groupLink: group, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -48,11 +48,6 @@ describe('RemoveGroupLinkButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('displays a tooltip', () => { const button = findButton(); diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js index cca340169b7..3879279b559 100644 --- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js +++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js @@ -47,7 +47,7 @@ describe('RemoveMemberButton', () => { ...propsData, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -58,10 +58,6 @@ describe('RemoveMemberButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('sets attributes on button', () => { expect(wrapper.attributes()).toMatchObject({ 'aria-label': 'Remove member', 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 51cfd47ddf4..a6b5978b566 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 @@ -38,7 +38,7 @@ describe('ResendInviteButton', () => { ...propsData, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -50,10 +50,6 @@ describe('ResendInviteButton', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays a tooltip', () => { expect(getBinding(findButton().element, 'gl-tooltip')).not.toBeUndefined(); expect(findButton().attributes('title')).toBe('Resend invite'); diff --git a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js index 90f5b217007..679ad7897ed 100644 --- a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js +++ b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js @@ -18,7 +18,7 @@ describe('LeaveGroupDropdownItem', () => { ...propsData, }, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, slots: { default: text, @@ -32,10 +32,6 @@ describe('LeaveGroupDropdownItem', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a slot with red text', () => { expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`); }); diff --git a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js index e1c498249d7..125f1f8fff3 100644 --- a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js +++ b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js @@ -58,10 +58,6 @@ describe('RemoveMemberDropdownItem', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a slot with red text', () => { expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`); }); diff --git a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js index 5a2de1cac80..448c04bcb69 100644 --- a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js +++ b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js @@ -24,17 +24,13 @@ describe('UserActionDropdown', () => { ...propsData, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; const findRemoveMemberDropdownItem = () => wrapper.findComponent(RemoveMemberDropdownItem); - afterEach(() => { - wrapper.destroy(); - }); - describe('when user has `canRemove` permissions', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js index d105a4d9fde..b2147163233 100644 --- a/spec/frontend/members/components/app_spec.js +++ b/spec/frontend/members/components/app_spec.js @@ -49,7 +49,6 @@ describe('MembersApp', () => { }); afterEach(() => { - wrapper.destroy(); store = null; }); diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js index 13c50de9835..8e4263f88fe 100644 --- a/spec/frontend/members/components/avatars/group_avatar_spec.js +++ b/spec/frontend/members/components/avatars/group_avatar_spec.js @@ -25,10 +25,6 @@ describe('MemberList', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders link to group', () => { const link = wrapper.findComponent(GlAvatarLink); diff --git a/spec/frontend/members/components/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js index b197a46c0d1..84878fb9be2 100644 --- a/spec/frontend/members/components/avatars/invite_avatar_spec.js +++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js @@ -24,10 +24,6 @@ describe('MemberList', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders email as name', () => { expect(getByText(invite.email).exists()).toBe(true); }); diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js index 9172876e76f..4808bcb9363 100644 --- a/spec/frontend/members/components/avatars/user_avatar_spec.js +++ b/spec/frontend/members/components/avatars/user_avatar_spec.js @@ -26,10 +26,6 @@ describe('UserAvatar', () => { const findStatusEmoji = (emoji) => wrapper.find(`gl-emoji[data-name="${emoji}"]`); - afterEach(() => { - wrapper.destroy(); - }); - it("renders link to user's profile", () => { 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 ef3c8bde3cf..526f839ece8 100644 --- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js +++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js @@ -46,7 +46,7 @@ describe('SortDropdown', () => { const findSortingComponent = () => wrapper.findComponent(GlSorting); const findSortDirectionToggle = () => findSortingComponent().find('button[title^="Sort direction"]'); - const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); + const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]'); const findDropdownItemByText = (text) => wrapper .findAllComponents(GlSortingItem) diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js index 77af5e7293e..9078bd87d62 100644 --- a/spec/frontend/members/components/members_tabs_spec.js +++ b/spec/frontend/members/components/members_tabs_spec.js @@ -100,10 +100,6 @@ describe('MembersTabs', () => { setWindowLocation('https://localhost'); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', async () => { await createComponent(); diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js index ba587c6f0b3..cec5f192e59 100644 --- a/spec/frontend/members/components/modals/leave_modal_spec.js +++ b/spec/frontend/members/components/modals/leave_modal_spec.js @@ -60,10 +60,6 @@ describe('LeaveModal', () => { const findForm = () => findModal().findComponent(GlForm); const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList); - afterEach(() => { - wrapper.destroy(); - }); - it('sets modal ID', async () => { await createComponent(); 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 af96396f09f..e4782ac7f2e 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 @@ -52,11 +52,6 @@ describe('RemoveGroupLinkModal', () => { const getByText = (text, options) => createWrapper(within(findModal().element).getByText(text, options)); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when modal is open', () => { beforeEach(async () => { createComponent(); 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 47a03b5083a..baef0b30b02 100644 --- a/spec/frontend/members/components/modals/remove_member_modal_spec.js +++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js @@ -54,10 +54,6 @@ describe('RemoveMemberModal', () => { const findGlModal = () => wrapper.findComponent(GlModal); const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` state | memberModelType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall ${'removing a group member'} | ${MEMBER_MODEL_TYPE_GROUP_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false} diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js index fa31177564b..2c0493e7c59 100644 --- a/spec/frontend/members/components/table/created_at_spec.js +++ b/spec/frontend/members/components/table/created_at_spec.js @@ -20,10 +20,6 @@ describe('CreatedAt', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('created at text', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js index 9b8f053348b..15812ee6572 100644 --- a/spec/frontend/members/components/table/expiration_datepicker_spec.js +++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js @@ -58,10 +58,6 @@ describe('ExpirationDatepicker', () => { const findInput = () => wrapper.find('input'); const findDatepicker = () => wrapper.findComponent(GlDatepicker); - afterEach(() => { - wrapper.destroy(); - }); - describe('datepicker input', () => { it('sets `member.expiresAt` as initial date', async () => { createComponent({ member: { ...member, expiresAt: '2020-03-17T00:00:00Z' } }); 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 95db30a3683..3a04d1dcb0a 100644 --- a/spec/frontend/members/components/table/member_action_buttons_spec.js +++ b/spec/frontend/members/components/table/member_action_buttons_spec.js @@ -23,10 +23,6 @@ describe('MemberActions', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` memberType | member | expectedComponent | expectedComponentName ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionDropdown} | ${'UserActionDropdown'} diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js index dc5c97f41df..369f8a06cfd 100644 --- a/spec/frontend/members/components/table/member_avatar_spec.js +++ b/spec/frontend/members/components/table/member_avatar_spec.js @@ -18,10 +18,6 @@ describe('MemberList', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` memberType | member | expectedComponent | expectedComponentName ${MEMBER_TYPES.user} | ${memberMock} | ${UserAvatar} | ${'UserAvatar'} diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js index fbfd0ca7ae7..bbfbb19fd92 100644 --- a/spec/frontend/members/components/table/member_source_spec.js +++ b/spec/frontend/members/components/table/member_source_spec.js @@ -23,17 +23,13 @@ describe('MemberSource', () => { ...propsData, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip'); - afterEach(() => { - wrapper.destroy(); - }); - describe('direct member', () => { describe('when created by is available', () => { it('displays "Direct member by <user name>"', () => { 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 ac5d83d028d..1c6f1b086cf 100644 --- a/spec/frontend/members/components/table/members_table_cell_spec.js +++ b/spec/frontend/members/components/table/members_table_cell_spec.js @@ -97,11 +97,6 @@ describe('MembersTableCell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` member | expectedMemberType ${memberMock} | ${MEMBER_TYPES.user} diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index b8e0d73d8f6..e3c89bfed53 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -96,10 +96,6 @@ describe('MembersTable', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('fields', () => { const memberCanUpdate = { ...directMember, diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js index a11f67be8f5..d6e63b1930f 100644 --- a/spec/frontend/members/components/table/role_dropdown_spec.js +++ b/spec/frontend/members/components/table/role_dropdown_spec.js @@ -67,21 +67,13 @@ describe('RoleDropdown', () => { .findAllComponents(GlDropdownItem) .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked')); - const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); + const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]'); const findDropdown = () => wrapper.findComponent(GlDropdown); - let originalGon; - beforeEach(() => { - originalGon = window.gon; gon.features = { showOverageOnRolePromotion: true }; }); - afterEach(() => { - window.gon = originalGon; - wrapper.destroy(); - }); - describe('when dropdown is open', () => { beforeEach(() => { guestOverageConfirmAction.mockReturnValue(true); diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js index 5c813eb2a67..b1730cf3746 100644 --- a/spec/frontend/members/index_spec.js +++ b/spec/frontend/members/index_spec.js @@ -31,9 +31,6 @@ describe('initMembersApp', () => { afterEach(() => { el = null; - - wrapper.destroy(); - wrapper = null; }); it('renders `MembersTabs`', () => { diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js index 9b5641ef7b3..ab913b30f3c 100644 --- a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js +++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js @@ -37,10 +37,6 @@ describe('Merge Conflict Resolver App', () => { store.dispatch('setConflictsData', conflictsMock); }); - afterEach(() => { - wrapper.destroy(); - }); - const findLoadingSpinner = () => wrapper.findByTestId('loading-spinner'); const findConflictsCount = () => wrapper.findByTestId('conflicts-count'); const findFiles = () => wrapper.findAllByTestId('files'); diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js index 19ef4b7db25..d2c4c8b796c 100644 --- a/spec/frontend/merge_conflicts/store/actions_spec.js +++ b/spec/frontend/merge_conflicts/store/actions_spec.js @@ -4,13 +4,13 @@ import Cookies from '~/lib/utils/cookies'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '~/merge_conflicts/constants'; import * as actions from '~/merge_conflicts/store/actions'; import * as types from '~/merge_conflicts/store/mutation_types'; import { restoreFileLinesState, markLine, decorateFiles } from '~/merge_conflicts/utils'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); jest.mock('~/merge_conflicts/utils'); jest.mock('~/lib/utils/cookies'); @@ -114,7 +114,7 @@ describe('merge conflicts actions', () => { expect(window.location.assign).toHaveBeenCalledWith('hrefPath'); }); - it('on errors shows flash', async () => { + it('on errors shows an alert', async () => { mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_BAD_REQUEST); await testAction( actions.submitResolvedConflicts, diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js index 579cee8c022..be16b5ebfd2 100644 --- a/spec/frontend/merge_request_spec.js +++ b/spec/frontend/merge_request_spec.js @@ -3,12 +3,12 @@ import $ from 'jquery'; import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'spec/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_CONFLICT, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import MergeRequest from '~/merge_request'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('MergeRequest', () => { const test = {}; diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js index 6d434d7e654..76d1fa3a332 100644 --- a/spec/frontend/merge_request_tabs_spec.js +++ b/spec/frontend/merge_request_tabs_spec.js @@ -354,8 +354,6 @@ describe('MergeRequestTabs', () => { testContext.class.expandSidebar.forEach((el) => { expect(el.classList.contains('gl-display-none!')).toBe(hides); }); - - window.gon = {}; }); describe('when switching tabs', () => { diff --git a/spec/frontend/merge_requests/components/compare_app_spec.js b/spec/frontend/merge_requests/components/compare_app_spec.js index 8f84341b653..ba129363ffd 100644 --- a/spec/frontend/merge_requests/components/compare_app_spec.js +++ b/spec/frontend/merge_requests/components/compare_app_spec.js @@ -30,10 +30,6 @@ function factory(provideData = {}) { } describe('Merge requests compare app component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('shows commit box when selected branch is empty', () => { factory({ currentBranch: { diff --git a/spec/frontend/merge_requests/components/compare_dropdown_spec.js b/spec/frontend/merge_requests/components/compare_dropdown_spec.js index ab5c315816c..ce03b80bdcb 100644 --- a/spec/frontend/merge_requests/components/compare_dropdown_spec.js +++ b/spec/frontend/merge_requests/components/compare_dropdown_spec.js @@ -47,7 +47,6 @@ describe('Merge requests compare dropdown component', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js index 87235fa843a..f8730fd93a3 100644 --- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js +++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js @@ -6,10 +6,10 @@ import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal import eventHub from '~/milestones/event_hub'; import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; import { redirectTo } from '~/lib/utils/url_utility'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; jest.mock('~/lib/utils/url_utility'); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Delete milestone modal', () => { let wrapper; @@ -39,10 +39,6 @@ describe('Delete milestone modal', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('onSubmit', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js index f8ddca1a2ad..748e01d4291 100644 --- a/spec/frontend/milestones/components/milestone_combobox_spec.js +++ b/spec/frontend/milestones/components/milestone_combobox_spec.js @@ -85,11 +85,6 @@ describe('Milestone combobox component', () => { mock.onGet(`/api/v4/projects/${projectId}/search`).reply((config) => searchApiCallSpy(config)); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - // // Finders // diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js index d7ad3d29d0a..e91e792afe8 100644 --- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js +++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js @@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils'; import { setHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; import * as urlUtils from '~/lib/utils/url_utility'; import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue'; jest.mock('~/lib/utils/url_utility'); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Promote milestone modal', () => { let wrapper; @@ -33,10 +33,6 @@ describe('Promote milestone modal', () => { wrapper = shallowMount(PromoteMilestoneModal); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('Modal opener button', () => { it('button gets disabled when the modal opens', () => { expect(promoteButton().disabled).toBe(false); diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap index 7d7eee2bc2c..dc21db39259 100644 --- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap +++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MlCandidate renders correctly 1`] = ` +exports[`MlCandidatesShow renders correctly 1`] = ` <div> <div class="gl-alert gl-alert-warning" @@ -152,7 +152,24 @@ exports[`MlCandidate renders correctly 1`] = ` </td> </tr> - <!----> + <tr> + <td /> + + <td + class="gl-font-weight-bold" + > + Artifacts + </td> + + <td> + <a + class="gl-link" + href="path_to_artifact" + > + Artifacts + </a> + </td> + </tr> <tr class="divider" diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js index 483e454d7d7..36455339041 100644 --- a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js +++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js @@ -1,8 +1,8 @@ import { GlAlert } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue'; +import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show'; -describe('MlCandidate', () => { +describe('MlCandidatesShow', () => { let wrapper; const createWrapper = () => { @@ -21,14 +21,14 @@ describe('MlCandidate', () => { ], info: { iid: 'candidate_iid', - artifact_link: 'path_to_artifact', + path_to_artifact: 'path_to_artifact', experiment_name: 'The Experiment', experiment_path: 'path/to/experiment', status: 'SUCCESS', }, }; - return mountExtended(MlCandidate, { propsData: { candidate } }); + return mountExtended(MlCandidatesShow, { propsData: { candidate } }); }; const findAlert = () => wrapper.findComponent(GlAlert); diff --git a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js index f307d2c5a58..97a5049ea88 100644 --- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js +++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js @@ -1,87 +1,35 @@ -import { GlAlert, GlTable, GlLink } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import { GlAlert, GlTableLite, GlLink, GlEmptyState } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue'; +import MlExperimentsShow from '~/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import Pagination from '~/vue_shared/components/incubation/pagination.vue'; import setWindowLocation from 'helpers/set_window_location_helper'; import * as urlHelpers from '~/lib/utils/url_utility'; +import { MOCK_START_CURSOR, MOCK_PAGE_INFO, MOCK_CANDIDATES } from './mock_data'; -describe('MlExperiment', () => { +describe('MlExperimentsShow', () => { let wrapper; - const startCursor = 'eyJpZCI6IjE2In0'; - const defaultPageInfo = { - startCursor, - endCursor: 'eyJpZCI6IjIifQ', - hasNextPage: true, - hasPreviousPage: true, - }; - const createWrapper = ( candidates = [], metricNames = [], paramNames = [], - pageInfo = defaultPageInfo, + pageInfo = MOCK_PAGE_INFO, ) => { - wrapper = mountExtended(MlExperiment, { - provide: { candidates, metricNames, paramNames, pageInfo }, + wrapper = mountExtended(MlExperimentsShow, { + propsData: { candidates, metricNames, paramNames, pageInfo }, }); }; - const candidates = [ - { - rmse: 1, - l1_ratio: 0.4, - details: 'link_to_candidate1', - artifact: 'link_to_artifact', - name: 'aCandidate', - created_at: '2023-01-05T14:07:02.975Z', - user: { username: 'root', path: '/root' }, - }, - { - auc: 0.3, - l1_ratio: 0.5, - details: 'link_to_candidate2', - created_at: '2023-01-05T14:07:02.975Z', - name: null, - user: null, - }, - { - auc: 0.3, - l1_ratio: 0.5, - details: 'link_to_candidate3', - created_at: '2023-01-05T14:07:02.975Z', - name: null, - user: null, - }, - { - auc: 0.3, - l1_ratio: 0.5, - details: 'link_to_candidate4', - created_at: '2023-01-05T14:07:02.975Z', - name: null, - user: null, - }, - { - auc: 0.3, - l1_ratio: 0.5, - details: 'link_to_candidate5', - created_at: '2023-01-05T14:07:02.975Z', - name: null, - user: null, - }, - ]; - - const createWrapperWithCandidates = (pageInfo = defaultPageInfo) => { - createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo); + const createWrapperWithCandidates = (pageInfo = MOCK_PAGE_INFO) => { + createWrapper(MOCK_CANDIDATES, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo); }; const findAlert = () => wrapper.findComponent(GlAlert); const findPagination = () => wrapper.findComponent(Pagination); - const findEmptyState = () => wrapper.findByText('No candidates to display'); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findRegistrySearch = () => wrapper.findComponent(RegistrySearch); - const findTable = () => wrapper.findComponent(GlTable); + const findTable = () => wrapper.findComponent(GlTableLite); const findTableHeaders = () => findTable().findAll('th'); const findTableRows = () => findTable().findAll('tbody > tr'); const findNthTableRow = (idx) => findTableRows().at(idx); @@ -98,8 +46,6 @@ describe('MlExperiment', () => { describe('default inputs', () => { beforeEach(async () => { createWrapper(); - - await nextTick(); }); it('shows empty state', () => { @@ -110,8 +56,8 @@ describe('MlExperiment', () => { expect(findPagination().exists()).toBe(false); }); - it('there are no columns', () => { - expect(findTable().findAll('th')).toHaveLength(0); + it('does not show table', () => { + expect(findTable().exists()).toBe(false); }); it('initializes sorting correctly', () => { @@ -227,34 +173,33 @@ describe('MlExperiment', () => { it('Passes pagination to pagination component', () => { createWrapperWithCandidates(); - expect(findPagination().props('startCursor')).toBe(startCursor); + expect(findPagination().props('startCursor')).toBe(MOCK_START_CURSOR); }); }); describe('Candidate table', () => { const firstCandidateIndex = 0; const secondCandidateIndex = 1; - const firstCandidate = candidates[firstCandidateIndex]; + const firstCandidate = MOCK_CANDIDATES[firstCandidateIndex]; beforeEach(() => { createWrapperWithCandidates(); }); it('renders all rows', () => { - expect(findTableRows()).toHaveLength(candidates.length); + expect(findTableRows()).toHaveLength(MOCK_CANDIDATES.length); }); it('sets the correct columns in the table', () => { const expectedColumnNames = [ 'Name', 'Created at', - 'User', + 'Author', 'L1 Ratio', 'Rmse', 'Auc', 'Mae', - '', - '', + 'Artifacts', ]; expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames); @@ -270,7 +215,9 @@ describe('MlExperiment', () => { }); it('shows empty state when no artifact', () => { - expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe('-'); + expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe( + 'No artifacts', + ); }); }); @@ -301,15 +248,7 @@ describe('MlExperiment', () => { }); it('when there is no user shows nothing', () => { - expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe(''); - }); - }); - - describe('Detail column', () => { - const detailColumn = -2; - - it('is a link to details', () => { - expect(hrefInRowAndColumn(firstCandidateIndex, detailColumn)).toBe(firstCandidate.details); + expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('No name'); }); }); }); diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js new file mode 100644 index 00000000000..66378cd3f0d --- /dev/null +++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js @@ -0,0 +1,52 @@ +export const MOCK_START_CURSOR = 'eyJpZCI6IjE2In0'; + +export const MOCK_PAGE_INFO = { + startCursor: MOCK_START_CURSOR, + endCursor: 'eyJpZCI6IjIifQ', + hasNextPage: true, + hasPreviousPage: true, +}; + +export const MOCK_CANDIDATES = [ + { + rmse: 1, + l1_ratio: 0.4, + details: 'link_to_candidate1', + artifact: 'link_to_artifact', + name: 'aCandidate', + created_at: '2023-01-05T14:07:02.975Z', + user: { username: 'root', path: '/root' }, + }, + { + auc: 0.3, + l1_ratio: 0.5, + details: 'link_to_candidate2', + created_at: '2023-01-05T14:07:02.975Z', + name: null, + user: null, + }, + { + auc: 0.3, + l1_ratio: 0.5, + details: 'link_to_candidate3', + created_at: '2023-01-05T14:07:02.975Z', + name: null, + user: null, + }, + { + auc: 0.3, + l1_ratio: 0.5, + details: 'link_to_candidate4', + created_at: '2023-01-05T14:07:02.975Z', + name: null, + user: null, + }, + { + auc: 0.3, + l1_ratio: 0.5, + details: 'link_to_candidate5', + created_at: '2023-01-05T14:07:02.975Z', + name: null, + user: null, + }, +]; diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js index 0158966997f..cc38a3fd8a1 100644 --- a/spec/frontend/monitoring/components/charts/column_spec.js +++ b/spec/frontend/monitoring/components/charts/column_spec.js @@ -51,10 +51,6 @@ describe('Column component', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('xAxisLabel', () => { const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js index 484199698ea..33ea5e83598 100644 --- a/spec/frontend/monitoring/components/charts/gauge_spec.js +++ b/spec/frontend/monitoring/components/charts/gauge_spec.js @@ -21,11 +21,6 @@ describe('Gauge Chart component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('chart component', () => { it('is rendered when props are passed', () => { createWrapper(); diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js index e163d4e73a0..54245cbdbc1 100644 --- a/spec/frontend/monitoring/components/charts/heatmap_spec.js +++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js @@ -28,10 +28,6 @@ describe('Heatmap component', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should display a label on the x axis', () => { expect(wrapper.vm.xAxisName).toBe(graphData.xLabel); }); diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js index 62a0b7e6ad3..fa31b479296 100644 --- a/spec/frontend/monitoring/components/charts/single_stat_spec.js +++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js @@ -21,10 +21,6 @@ describe('Single Stat Chart component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('statValue', () => { it('should display the correct value', () => { diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 503dee7b937..c1b51f71a7e 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -58,10 +58,6 @@ describe('Time series component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('With a single time series', () => { describe('general functions', () => { const findChart = () => wrapper.findComponent({ ref: 'chart' }); diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js index 88de3467580..eb05b1f184a 100644 --- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js +++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js @@ -29,10 +29,6 @@ describe('Create dashboard modal', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('has button that links to the project url', async () => { findRepoButton().trigger('click'); diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js index bb57420d406..2758103fd6e 100644 --- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js +++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js @@ -55,11 +55,6 @@ describe('Actions menu', () => { store = createStore(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('add metric item', () => { it('is rendered when custom metrics are available', async () => { createShallowWrapper(); diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js index 18ccda2c41c..ab259249772 100644 --- a/spec/frontend/monitoring/components/dashboard_header_spec.js +++ b/spec/frontend/monitoring/components/dashboard_header_spec.js @@ -59,10 +59,6 @@ describe('Dashboard header', () => { store = createStore(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('dashboards dropdown', () => { beforeEach(() => { store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js index d71f6374967..1cfd132b123 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js @@ -49,8 +49,6 @@ describe('dashboard invalid url parameters', () => { jest.spyOn(store, 'dispatch').mockResolvedValue(); }); - afterEach(() => {}); - it('is mounted', () => { expect(wrapper.exists()).toBe(true); }); diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js index 339c1710a9e..491649e5b96 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js @@ -106,10 +106,6 @@ describe('Dashboard Panel', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the chart title', () => { expect(findTitle().text()).toBe(graphDataEmpty.title); }); @@ -134,10 +130,6 @@ describe('Dashboard Panel', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders no chart title', () => { expect(findTitle().text()).toBe(''); }); @@ -160,10 +152,6 @@ describe('Dashboard Panel', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the chart title', () => { expect(findTitle().text()).toBe(graphData.title); }); @@ -377,10 +365,6 @@ describe('Dashboard Panel', () => { await nextTick(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('csvText', () => { it('converts metrics data from json to csv', () => { const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`; diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 1d17a9116df..1f995965003 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -4,7 +4,7 @@ import { nextTick } from 'vue'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { ESC_KEY } from '~/lib/utils/keys'; import { objectToQuery } from '~/lib/utils/url_utility'; @@ -33,7 +33,7 @@ import { setupStoreWithLinks, } from '../store_utils'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Dashboard', () => { let store; @@ -75,7 +75,6 @@ describe('Dashboard', () => { if (store.dispatch.mockReset) { store.dispatch.mockReset(); } - wrapper.destroy(); }); describe('request information to the server', () => { diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js index 9873654bdda..98791906700 100644 --- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js +++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { queryToObject, @@ -18,7 +18,7 @@ import { defaultTimeRange } from '~/vue_shared/constants'; import { dashboardProps } from '../fixture_data'; import { mockProjectDir } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); describe('dashboard invalid url parameters', () => { diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js index 104263e73e0..593d832f297 100644 --- a/spec/frontend/monitoring/components/graph_group_spec.js +++ b/spec/frontend/monitoring/components/graph_group_spec.js @@ -18,10 +18,6 @@ describe('Graph group component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('When group is not collapsed', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js index e3cd26b0e48..d3a48be7939 100644 --- a/spec/frontend/monitoring/components/group_empty_state_spec.js +++ b/spec/frontend/monitoring/components/group_empty_state_spec.js @@ -23,10 +23,6 @@ function createComponent(props) { describe('GroupEmptyState', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - describe.each([ metricStates.NO_DATA, metricStates.TIMEOUT, diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js index cb300870689..f6cc6789b1f 100644 --- a/spec/frontend/monitoring/components/refresh_button_spec.js +++ b/spec/frontend/monitoring/components/refresh_button_spec.js @@ -40,6 +40,7 @@ describe('RefreshButton', () => { afterEach(() => { dispatch.mockReset(); + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy wrapper.destroy(); }); diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js index 012e2e9c3e2..96b228fd3b2 100644 --- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js +++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js @@ -1,6 +1,5 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import DropdownField from '~/monitoring/components/variables/dropdown_field.vue'; describe('Custom variable component', () => { @@ -56,11 +55,8 @@ describe('Custom variable component', () => { it('changing dropdown items triggers update', async () => { createShallowWrapper(); - jest.spyOn(wrapper.vm, '$emit'); - findDropdownItems().at(1).vm.$emit('click'); - await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary'); + expect(wrapper.emitted('input')).toEqual([['canary']]); }); }); diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js index 3073b3968aa..20e1937c5ac 100644 --- a/spec/frontend/monitoring/components/variables/text_field_spec.js +++ b/spec/frontend/monitoring/components/variables/text_field_spec.js @@ -33,25 +33,23 @@ describe('Text variable component', () => { it('triggers keyup enter', async () => { createShallowWrapper(); - jest.spyOn(wrapper.vm, '$emit'); findInput().element.value = 'prod-pod'; findInput().trigger('input'); findInput().trigger('keyup.enter'); await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod'); + expect(wrapper.emitted('input')).toEqual([['prod-pod']]); }); it('triggers blur enter', async () => { createShallowWrapper(); - jest.spyOn(wrapper.vm, '$emit'); findInput().element.value = 'canary-pod'; findInput().trigger('input'); findInput().trigger('blur'); await nextTick(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod'); + expect(wrapper.emitted('input')).toEqual([['canary-pod']]); }); }); diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js index fa112fca2db..98ee6c1cb29 100644 --- a/spec/frontend/monitoring/pages/panel_new_page_spec.js +++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js @@ -49,10 +49,6 @@ describe('monitoring/pages/panel_new_page', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('back to dashboard button', () => { it('is rendered', () => { expect(findBackButton().exists()).toBe(true); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index 8eda46a2ff1..8097857f226 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { backoffMockImplementation } from 'helpers/backoff_helper'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import { @@ -61,7 +61,7 @@ import { mockDashboardsErrorResponse, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Monitoring store actions', () => { const { convertObjectPropsToCamelCase } = commonUtils; @@ -177,7 +177,6 @@ describe('Monitoring store actions', () => { }); it('dispatches when feature metricsDashboardAnnotations is on', () => { - const origGon = window.gon; window.gon = { features: { metricsDashboardAnnotations: true } }; return testAction( @@ -190,9 +189,7 @@ describe('Monitoring store actions', () => { { type: 'fetchDashboard' }, { type: 'fetchAnnotations' }, ], - ).then(() => { - window.gon = origGon; - }); + ); }); }); @@ -263,7 +260,7 @@ describe('Monitoring store actions', () => { }); }); - it('does not show a flash error when showErrorBanner is disabled', async () => { + it('does not show an alert error when showErrorBanner is disabled', async () => { state.showErrorBanner = false; await result(); diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js index bad24345f9d..fe543a346b5 100644 --- a/spec/frontend/nav/components/new_nav_toggle_spec.js +++ b/spec/frontend/nav/components/new_nav_toggle_spec.js @@ -1,16 +1,16 @@ import { mount, createWrapper } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { getByText as getByTextHelper } from '@testing-library/dom'; -import { GlToggle } from '@gitlab/ui'; +import { GlDisclosureDropdownItem, GlToggle } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import NewNavToggle from '~/nav/components/new_nav_toggle.vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { s__ } from '~/locale'; -jest.mock('~/flash'); +jest.mock('~/alert'); const TEST_ENDPONT = 'https://example.com/toggle'; @@ -20,6 +20,7 @@ describe('NewNavToggle', () => { let wrapper; const findToggle = () => wrapper.findComponent(GlToggle); + const findDisclosureItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const createComponent = (propsData = { enabled: false }) => { wrapper = mount(NewNavToggle, { @@ -30,83 +31,156 @@ describe('NewNavToggle', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const getByText = (text, options) => createWrapper(getByTextHelper(wrapper.element, text, options)); - it('renders its title', () => { - createComponent(); - expect(getByText('Navigation redesign').exists()).toBe(true); - }); + describe('When rendered in scope of the new navigation', () => { + it('renders the disclosure item', () => { + createComponent({ newNavigation: true, enabled: true }); + expect(findDisclosureItem().exists()).toBe(true); + }); - describe('when user preference is enabled', () => { - beforeEach(() => { - createComponent({ enabled: true }); + describe('when user preference is enabled', () => { + beforeEach(() => { + createComponent({ newNavigation: true, enabled: true }); + }); + + it('renders the toggle as enabled', () => { + expect(findToggle().props('value')).toBe(true); + }); }); - it('renders the toggle as enabled', () => { - expect(findToggle().props('value')).toBe(true); + describe('when user preference is disabled', () => { + beforeEach(() => { + createComponent({ enabled: false }); + }); + + it('renders the toggle as disabled', () => { + expect(findToggle().props('value')).toBe(false); + }); + }); + + describe.each` + desc | actFn + ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} + ${'on menu item action'} | ${() => findDisclosureItem().vm.$emit('action')} + `('$desc', ({ actFn }) => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + createComponent({ enabled: false, newNavigation: true }); + }); + + it('reloads the page on success', async () => { + mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK); + + actFn(); + await waitForPromises(); + + expect(window.location.reload).toHaveBeenCalled(); + }); + + it('shows an alert on error', async () => { + mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + + actFn(); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: s__( + 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.', + ), + }), + ); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it('changes the toggle', async () => { + await actFn(); + + expect(findToggle().props('value')).toBe(true); + }); + + afterEach(() => { + mock.restore(); + }); }); }); - describe('when user preference is disabled', () => { - beforeEach(() => { - createComponent({ enabled: false }); + describe('When rendered in scope of the current navigation', () => { + it('renders its title', () => { + createComponent(); + expect(getByText('Navigation redesign').exists()).toBe(true); }); - it('renders the toggle as disabled', () => { - expect(findToggle().props('value')).toBe(false); + describe('when user preference is enabled', () => { + beforeEach(() => { + createComponent({ enabled: true }); + }); + + it('renders the toggle as enabled', () => { + expect(findToggle().props('value')).toBe(true); + }); }); - }); - describe.each` - desc | actFn - ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} - ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} - `('$desc', ({ actFn }) => { - let mock; + describe('when user preference is disabled', () => { + beforeEach(() => { + createComponent({ enabled: false }); + }); - beforeEach(() => { - mock = new MockAdapter(axios); - createComponent({ enabled: false }); + it('renders the toggle as disabled', () => { + expect(findToggle().props('value')).toBe(false); + }); }); - it('reloads the page on success', async () => { - mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK); + describe.each` + desc | actFn + ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} + ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} + `('$desc', ({ actFn }) => { + let mock; - actFn(); - await waitForPromises(); + beforeEach(() => { + mock = new MockAdapter(axios); + createComponent({ enabled: false }); + }); - expect(window.location.reload).toHaveBeenCalled(); - }); + it('reloads the page on success', async () => { + mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK); - it('shows an alert on error', async () => { - mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + actFn(); + await waitForPromises(); - actFn(); - await waitForPromises(); + expect(window.location.reload).toHaveBeenCalled(); + }); - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: s__( - 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.', - ), - }), - ); - expect(window.location.reload).not.toHaveBeenCalled(); - }); + it('shows an alert on error', async () => { + mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - it('changes the toggle', async () => { - await actFn(); + actFn(); + await waitForPromises(); - expect(findToggle().props('value')).toBe(true); - }); + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: s__( + 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.', + ), + }), + ); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it('changes the toggle', async () => { + await actFn(); + + expect(findToggle().props('value')).toBe(true); + }); - afterEach(() => { - mock.restore(); + afterEach(() => { + mock.restore(); + }); }); }); }); diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js index 76b8ebdc92f..9d3b43520ec 100644 --- a/spec/frontend/nav/components/responsive_app_spec.js +++ b/spec/frontend/nav/components/responsive_app_spec.js @@ -33,10 +33,6 @@ describe('~/nav/components/responsive_app.vue', () => { document.body.className = 'test-class'; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js index f87de0afb14..2514035270a 100644 --- a/spec/frontend/nav/components/responsive_header_spec.js +++ b/spec/frontend/nav/components/responsive_header_spec.js @@ -14,7 +14,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { default: TEST_SLOT_CONTENT, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -25,10 +25,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders slot', () => { expect(wrapper.text()).toBe(TEST_SLOT_CONTENT); }); diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js index 8f198d92747..5a5cfc93607 100644 --- a/spec/frontend/nav/components/responsive_home_spec.js +++ b/spec/frontend/nav/components/responsive_home_spec.js @@ -29,7 +29,7 @@ describe('~/nav/components/responsive_home.vue', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, listeners: { 'menu-item-click': menuItemClickListener, @@ -45,10 +45,6 @@ describe('~/nav/components/responsive_home.vue', () => { menuItemClickListener = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js index e70f70afc97..7f39552eb42 100644 --- a/spec/frontend/nav/components/top_nav_app_spec.js +++ b/spec/frontend/nav/components/top_nav_app_spec.js @@ -28,10 +28,6 @@ describe('~/nav/components/top_nav_app.vue', () => { const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle'); const findMenu = () => wrapper.findComponent(TopNavDropdownMenu); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponentShallow(); diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js index 293fe361fa9..388ac243648 100644 --- a/spec/frontend/nav/components/top_nav_container_view_spec.js +++ b/spec/frontend/nav/components/top_nav_container_view_spec.js @@ -48,10 +48,6 @@ describe('~/nav/components/top_nav_container_view.vue', () => { }; const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]'); - afterEach(() => { - wrapper.destroy(); - }); - it.each(['projects', 'groups'])( 'emits frequent items event to event hub (%s)', async (frequentItemsDropdownType) => { 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 8a0340087ec..08d6650b5bb 100644 --- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js +++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js @@ -36,10 +36,6 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { active: idx === activeIndex, })); - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(); }); diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js index 7a5a8475ab7..7a3e58fd964 100644 --- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js +++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js @@ -54,10 +54,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { menuItems: findMenuItemModels(x), })); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js index 18210658b89..2cd65307b0b 100644 --- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js +++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js @@ -1,6 +1,8 @@ import { GlDropdown } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; +import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants'; const TEST_VIEW_MODEL = { title: 'Dropdown', @@ -18,6 +20,16 @@ const TEST_VIEW_MODEL = { menu_items: [ { id: 'bar-1', title: 'Bar 1', href: '/bar/1' }, { id: 'bar-2', title: 'Bar 2', href: '/bar/2' }, + { + id: 'invite', + title: '_invite members title_', + component: TOP_NAV_INVITE_MEMBERS_COMPONENT, + icon: '_icon_', + data: { + trigger_element: '_trigger_element_', + trigger_source: '_trigger_source_', + }, + }, ], }, ], @@ -36,6 +48,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }; const findDropdown = () => wrapper.findComponent(GlDropdown); + const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger); const findDropdownContents = () => findDropdown() .findAll('[data-testid]') @@ -55,10 +68,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); @@ -73,6 +82,10 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }); it('renders dropdown content', () => { + const hrefItems = TEST_VIEW_MODEL.menu_sections[1].menu_items.filter((item) => + Boolean(item.href), + ); + expect(findDropdownContents()).toEqual([ { type: 'header', @@ -90,12 +103,18 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { type: 'header', text: TEST_VIEW_MODEL.menu_sections[1].title, }, - ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({ + ...hrefItems.map(({ title, href }) => ({ type: 'item', href, text: title, })), ]); + expect(findInviteMembersTrigger().props()).toMatchObject({ + displayText: '_invite members title_', + icon: '_icon_', + triggerElement: 'dropdown-_trigger_element_', + triggerSource: '_trigger_source_', + }); }); }); diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js index 10762a1c3a2..9836400a366 100644 --- a/spec/frontend/notebook/cells/code_spec.js +++ b/spec/frontend/notebook/cells/code_spec.js @@ -13,10 +13,6 @@ describe('Code component', () => { json = JSON.parse(JSON.stringify(fixture)); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('without output', () => { beforeEach(() => { wrapper = mountComponent(json.cells[0]); diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js index a7776bd5b69..f226776212a 100644 --- a/spec/frontend/notebook/cells/markdown_spec.js +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -1,18 +1,16 @@ import { mount } from '@vue/test-utils'; import katex from 'katex'; -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json'; import basicJson from 'test_fixtures/blob/notebook/basic.json'; import mathJson from 'test_fixtures/blob/notebook/math.json'; import MarkdownComponent from '~/notebook/cells/markdown.vue'; import Prompt from '~/notebook/cells/prompt.vue'; -const Component = Vue.extend(MarkdownComponent); - window.katex = katex; function buildCellComponent(cell, relativePath = '', hidePrompt) { - return mount(Component, { + return mount(MarkdownComponent, { propsData: { cell, hidePrompt, diff --git a/spec/frontend/notebook/cells/output/error_spec.js b/spec/frontend/notebook/cells/output/error_spec.js new file mode 100644 index 00000000000..2e4ca8c1761 --- /dev/null +++ b/spec/frontend/notebook/cells/output/error_spec.js @@ -0,0 +1,48 @@ +import { mount } from '@vue/test-utils'; +import ErrorOutput from '~/notebook/cells/output/error.vue'; +import Prompt from '~/notebook/cells/prompt.vue'; +import Markdown from '~/notebook/cells/markdown.vue'; +import { errorOutputContent, relativeRawPath } from '../../mock_data'; + +describe('notebook/cells/output/error.vue', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount(ErrorOutput, { + propsData: { + rawCode: errorOutputContent, + index: 1, + count: 2, + }, + provide: { relativeRawPath }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + const findPrompt = () => wrapper.findComponent(Prompt); + const findMarkdown = () => wrapper.findComponent(Markdown); + + it('renders the prompt', () => { + expect(findPrompt().props()).toMatchObject({ count: 2, showOutput: true, type: 'Out' }); + }); + + it('renders the markdown', () => { + const expectedParsedMarkdown = + '```error\n' + + '---------------------------------------------------------------------------\n' + + 'NameError Traceback (most recent call last)\n' + + '/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py in <module>\n' + + '----> 1 To\n' + + '\n' + + "NameError: name 'To' is not defined\n" + + '```'; + + expect(findMarkdown().props()).toMatchObject({ + cell: { source: [expectedParsedMarkdown] }, + hidePrompt: true, + }); + }); +}); diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js index 585cbb68eeb..1241c133b89 100644 --- a/spec/frontend/notebook/cells/output/index_spec.js +++ b/spec/frontend/notebook/cells/output/index_spec.js @@ -17,10 +17,6 @@ describe('Output component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('text output', () => { beforeEach(() => { const textType = json.cells[2]; diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js index 0cda0c5bc2b..4c864a9b930 100644 --- a/spec/frontend/notebook/cells/prompt_spec.js +++ b/spec/frontend/notebook/cells/prompt_spec.js @@ -6,10 +6,6 @@ describe('Prompt component', () => { const mountComponent = ({ type }) => shallowMount(Prompt, { propsData: { type, count: 1 } }); - afterEach(() => { - wrapper.destroy(); - }); - describe('input', () => { beforeEach(() => { wrapper = mountComponent({ type: 'In' }); diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js index b79000a3505..3c73d420703 100644 --- a/spec/frontend/notebook/index_spec.js +++ b/spec/frontend/notebook/index_spec.js @@ -1,16 +1,14 @@ import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; import json from 'test_fixtures/blob/notebook/basic.json'; import jsonWithWorksheet from 'test_fixtures/blob/notebook/worksheets.json'; import Notebook from '~/notebook/index.vue'; -const Component = Vue.extend(Notebook); - describe('Notebook component', () => { let vm; function buildComponent(notebook) { - return mount(Component, { + return mount(Notebook, { propsData: { notebook }, provide: { relativeRawPath: '' }, }).vm; diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js index b1419e1256f..5c47cb5aa9b 100644 --- a/spec/frontend/notebook/mock_data.js +++ b/spec/frontend/notebook/mock_data.js @@ -1,2 +1,8 @@ export const relativeRawPath = '/test'; export const markdownCellContent = ['# Test']; +export const errorOutputContent = [ + '\u001b[0;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)', + '\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mTo\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m', + "\u001b[0;31mNameError\u001b[0m: name 'To' is not defined", +]; diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index dfb05c85fc8..062cd098640 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -5,11 +5,12 @@ import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import Autosave from '~/autosave'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; +import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; import axios from '~/lib/utils/axios_utils'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; import CommentForm from '~/notes/components/comment_form.vue'; import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue'; @@ -21,8 +22,7 @@ import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } jest.mock('autosize'); jest.mock('~/commons/nav/user_merge_requests'); -jest.mock('~/flash'); -jest.mock('~/autosave'); +jest.mock('~/alert'); Vue.use(Vuex); @@ -32,7 +32,8 @@ describe('issue_comment_form component', () => { let axiosMock; const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button'); - const findTextArea = () => wrapper.findByTestId('comment-field'); + const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); + const findMarkdownEditorTextarea = () => findMarkdownEditor().find('textarea'); const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button'); const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button'); const findConfidentialNoteCheckbox = () => wrapper.findByTestId('internal-note-checkbox'); @@ -127,7 +128,6 @@ describe('issue_comment_form component', () => { afterEach(() => { axiosMock.restore(); - wrapper.destroy(); }); describe('user is logged in', () => { @@ -136,7 +136,6 @@ describe('issue_comment_form component', () => { mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } }); jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue(); - jest.spyOn(wrapper.vm, 'resizeTextarea'); jest.spyOn(wrapper.vm, 'stopPolling'); findCloseReopenButton().trigger('click'); @@ -145,7 +144,6 @@ describe('issue_comment_form component', () => { expect(wrapper.vm.note).toBe(''); expect(wrapper.vm.saveNote).toHaveBeenCalled(); expect(wrapper.vm.stopPolling).toHaveBeenCalled(); - expect(wrapper.vm.resizeTextarea).toHaveBeenCalled(); }); it('does not report errors in the UI when the save succeeds', async () => { @@ -260,6 +258,18 @@ describe('issue_comment_form component', () => { }); }); + it('hides content editor switcher if feature flag content_editor_on_issues is off', () => { + mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } }); + + expect(wrapper.text()).not.toContain('Rich text'); + }); + + it('shows content editor switcher if feature flag content_editor_on_issues is on', () => { + mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } }); + + expect(wrapper.text()).toContain('Rich text'); + }); + describe('textarea', () => { describe('general', () => { it.each` @@ -268,13 +278,13 @@ describe('issue_comment_form component', () => { ${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'} `( 'should render textarea with placeholder for $noteType', - ({ noteIsInternal, placeholder }) => { - mountComponent({ - mountFunction: mount, - initialData: { noteIsInternal }, - }); + async ({ noteIsInternal, placeholder }) => { + mountComponent(); + + wrapper.vm.noteIsInternal = noteIsInternal; + await nextTick(); - expect(findTextArea().attributes('placeholder')).toBe(placeholder); + expect(findMarkdownEditor().props('formFieldProps').placeholder).toBe(placeholder); }, ); @@ -290,13 +300,13 @@ describe('issue_comment_form component', () => { await findCommentButton().trigger('click'); - expect(findTextArea().attributes('disabled')).toBe('disabled'); + expect(findMarkdownEditor().find('textarea').attributes('disabled')).toBe('disabled'); }); it('should support quick actions', () => { mountComponent({ mountFunction: mount }); - expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true'); + expect(findMarkdownEditor().props('supportsQuickActions')).toBe(true); }); it('should link to markdown docs', () => { @@ -336,63 +346,51 @@ describe('issue_comment_form component', () => { it('should enter edit mode when arrow up is pressed', () => { jest.spyOn(wrapper.vm, 'editCurrentUserLastNote'); - findTextArea().trigger('keydown.up'); + findMarkdownEditorTextarea().trigger('keydown.up'); expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled(); }); - it('inits autosave', () => { - expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [ - 'Note', - 'Issue', - noteableDataMock.id, - ]); - }); - }); + describe('event enter', () => { + describe('when no draft exists', () => { + it('should save note when cmd+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSave'); - describe('event enter', () => { - beforeEach(() => { - mountComponent({ mountFunction: mount }); - }); - - describe('when no draft exists', () => { - it('should save note when cmd+enter is pressed', () => { - jest.spyOn(wrapper.vm, 'handleSave'); - - findTextArea().trigger('keydown.enter', { metaKey: true }); + findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true }); - expect(wrapper.vm.handleSave).toHaveBeenCalledWith(); - }); + expect(wrapper.vm.handleSave).toHaveBeenCalledWith(); + }); - it('should save note when ctrl+enter is pressed', () => { - jest.spyOn(wrapper.vm, 'handleSave'); + it('should save note when ctrl+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSave'); - findTextArea().trigger('keydown.enter', { ctrlKey: true }); + findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true }); - expect(wrapper.vm.handleSave).toHaveBeenCalledWith(); + expect(wrapper.vm.handleSave).toHaveBeenCalledWith(); + }); }); - }); - describe('when a draft exists', () => { - beforeEach(() => { - store.registerModule('batchComments', batchComments()); - store.state.batchComments.drafts = [{ note: 'A' }]; - }); + describe('when a draft exists', () => { + beforeEach(() => { + store.registerModule('batchComments', batchComments()); + store.state.batchComments.drafts = [{ note: 'A' }]; + }); - it('should save note draft when cmd+enter is pressed', () => { - jest.spyOn(wrapper.vm, 'handleSaveDraft'); + it('should save note draft when cmd+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSaveDraft'); - findTextArea().trigger('keydown.enter', { metaKey: true }); + findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true }); - expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith(); - }); + expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith(); + }); - it('should save note draft when ctrl+enter is pressed', () => { - jest.spyOn(wrapper.vm, 'handleSaveDraft'); + it('should save note draft when ctrl+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSaveDraft'); - findTextArea().trigger('keydown.enter', { ctrlKey: true }); + findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true }); - expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith(); + expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith(); + }); }); }); }); @@ -482,7 +480,7 @@ describe('issue_comment_form component', () => { it(`makes an API call to open it`, () => { mountComponent({ noteableType, - noteableData: { ...noteableDataMock, state: constants.OPENED }, + noteableData: { ...noteableDataMock, state: STATUS_OPEN }, mountFunction: mount, }); @@ -496,7 +494,7 @@ describe('issue_comment_form component', () => { it(`shows an error when the API call fails`, async () => { mountComponent({ noteableType, - noteableData: { ...noteableDataMock, state: constants.OPENED }, + noteableData: { ...noteableDataMock, state: STATUS_OPEN }, mountFunction: mount, }); @@ -517,7 +515,7 @@ describe('issue_comment_form component', () => { it('makes an API call to close it', () => { mountComponent({ noteableType, - noteableData: { ...noteableDataMock, state: constants.CLOSED }, + noteableData: { ...noteableDataMock, state: STATUS_CLOSED }, mountFunction: mount, }); @@ -532,7 +530,7 @@ describe('issue_comment_form component', () => { it(`shows an error when the API call fails`, async () => { mountComponent({ noteableType, - noteableData: { ...noteableDataMock, state: constants.CLOSED }, + noteableData: { ...noteableDataMock, state: STATUS_CLOSED }, mountFunction: mount, }); @@ -661,7 +659,7 @@ describe('issue_comment_form component', () => { }); it('should not render submission form', () => { - expect(findTextArea().exists()).toBe(false); + expect(findMarkdownEditor().exists()).toBe(false); }); }); diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js index cabf551deba..b891c1f553d 100644 --- a/spec/frontend/notes/components/comment_type_dropdown_spec.js +++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js @@ -24,10 +24,6 @@ describe('CommentTypeDropdown component', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` isInternalNote | buttonText ${false} | ${COMMENT_FORM.comment} diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js index bb44563b87a..66b86ed3ce0 100644 --- a/spec/frontend/notes/components/diff_discussion_header_spec.js +++ b/spec/frontend/notes/components/diff_discussion_header_spec.js @@ -22,10 +22,6 @@ describe('diff_discussion_header component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('Avatar', () => { const firstNoteAuthor = discussionMock.notes[0].author; const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index e414ada1854..a9a20bd8bc3 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -38,15 +38,12 @@ describe('DiscussionActions', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { const createComponent = createComponentFactory(); it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => { createComponent(); + expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true); expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(true); expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(true); @@ -94,17 +91,15 @@ describe('DiscussionActions', () => { it('emits showReplyForm event when clicking on reply placeholder', () => { createComponent({}, { attachTo: document.body }); - jest.spyOn(wrapper.vm, '$emit'); wrapper.findComponent(ReplyPlaceholder).find('textarea').trigger('focus'); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm'); + expect(wrapper.emitted().showReplyForm).toHaveLength(1); }); it('emits resolve event when clicking on resolve button', () => { createComponent(); - jest.spyOn(wrapper.vm, '$emit'); wrapper.findComponent(ResolveDiscussionButton).find('button').trigger('click'); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve'); + expect(wrapper.emitted().resolve).toHaveLength(1); }); }); }); diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index f4ec7f835bb..ac677841ee1 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -40,7 +40,6 @@ describe('DiscussionCounter component', () => { afterEach(() => { wrapper.vm.$destroy(); - wrapper = null; }); describe('has no discussions', () => { @@ -119,8 +118,6 @@ describe('DiscussionCounter component', () => { toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]'); }; - afterEach(() => wrapper.destroy()); - it('calls button handler when clicked', async () => { await updateStoreWithExpanded(true); diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js index 48f5030aa1a..e31155a028f 100644 --- a/spec/frontend/notes/components/discussion_filter_note_spec.js +++ b/spec/frontend/notes/components/discussion_filter_note_spec.js @@ -18,11 +18,6 @@ describe('DiscussionFilterNote component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('timelineContent renders a string containing instruction for switching feed type', () => { expect(wrapper.find('[data-testid="discussion-filter-timeline-content"]').html()).toBe( '<div data-testid="discussion-filter-timeline-content">You\'re only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.</div>', diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js index 77ae7b2c3b5..6e095f63003 100644 --- a/spec/frontend/notes/components/discussion_navigator_spec.js +++ b/spec/frontend/notes/components/discussion_navigator_spec.js @@ -37,7 +37,6 @@ describe('notes/components/discussion_navigator', () => { if (wrapper) { wrapper.destroy(); } - wrapper = null; }); describe('on create', () => { diff --git a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js index 8d5ea108b50..d11ca7ad1ec 100644 --- a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js +++ b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js @@ -19,10 +19,6 @@ describe('DiscussionNotesRepliesWrapper', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when normal discussion', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index add2ed1ba8a..bc0c04f2d8a 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -53,11 +53,6 @@ describe('DiscussionNotes', () => { store.dispatch('setNotesData', notesDataMock); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('rendering', () => { it('renders an element for each note in the discussion', () => { createComponent(); diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js index 971e3987929..a9201b78669 100644 --- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js +++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js @@ -17,10 +17,6 @@ describe('ReplyPlaceholder', () => { const findTextarea = () => wrapper.findComponent({ ref: 'textarea' }); - afterEach(() => { - wrapper.destroy(); - }); - it('emits focus event on button click', async () => { createComponent({ options: { attachTo: document.body } }); diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js index 17c3523cf48..4bd21842fec 100644 --- a/spec/frontend/notes/components/discussion_resolve_button_spec.js +++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js @@ -23,10 +23,6 @@ describe('resolveDiscussionButton', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should emit a onClick event on button click', async () => { const button = wrapper.findComponent(GlButton); 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 a185f11ffaa..3dfae45ec49 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 @@ -15,10 +15,6 @@ describe('ResolveWithIssueButton', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should have a link with the provided link property as href', () => { const button = wrapper.findComponent(GlButton); diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js index ab1a6b152a4..34b7524d8fb 100644 --- a/spec/frontend/notes/components/email_participants_warning_spec.js +++ b/spec/frontend/notes/components/email_participants_warning_spec.js @@ -4,11 +4,6 @@ import EmailParticipantsWarning from '~/notes/components/email_participants_warn describe('Email Participants Warning Component', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findMoreButton = () => wrapper.find('button'); const createWrapper = (emails) => { 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 20b32b8c178..68b11fb3b1a 100644 --- a/spec/frontend/notes/components/note_actions/reply_button_spec.js +++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js @@ -9,11 +9,6 @@ describe('ReplyButton', () => { wrapper = shallowMount(ReplyButton); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('emits startReplying on click', () => { wrapper.findComponent(GlButton).vm.$emit('click'); diff --git a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js index 658e844a9b1..bee08ee0605 100644 --- a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js +++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js @@ -20,10 +20,6 @@ describe('NoteTimelineEventButton', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - const findTimelineButton = () => wrapper.findComponent(GlButton); it('emits click-promote-comment-to-event', async () => { diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index 8630b7b7d07..63286927d53 100644 --- a/spec/frontend/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js @@ -77,7 +77,6 @@ describe('noteActions', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); @@ -203,7 +202,6 @@ describe('noteActions', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); @@ -226,7 +224,6 @@ describe('noteActions', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); @@ -248,10 +245,6 @@ describe('noteActions', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should not be possible to assign the comment author', testButtonDoesNotRender); it('should not be possible to unassign the comment author', testButtonDoesNotRender); }); diff --git a/spec/frontend/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js index 24632f8e427..7f44171f6cc 100644 --- a/spec/frontend/notes/components/note_attachment_spec.js +++ b/spec/frontend/notes/components/note_attachment_spec.js @@ -15,11 +15,6 @@ describe('Issue note attachment', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders attachment image if it is passed in attachment prop', () => { createComponent({ image: 'test-image', diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index c71cf7666ab..b4f185004bb 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -49,10 +49,6 @@ describe('issue_note_body component', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render the note', () => { expect(wrapper.find('.note-text').html()).toContain(note.note_html); }); diff --git a/spec/frontend/notes/components/note_edited_text_spec.js b/spec/frontend/notes/components/note_edited_text_spec.js index 0a5fe48ef94..577e1044588 100644 --- a/spec/frontend/notes/components/note_edited_text_spec.js +++ b/spec/frontend/notes/components/note_edited_text_spec.js @@ -1,3 +1,4 @@ +import { GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import NoteEditedText from '~/notes/components/note_edited_text.vue'; @@ -5,41 +6,63 @@ const propsData = { actionText: 'Edited', className: 'foo-bar', editedAt: '2017-08-04T09:52:31.062Z', - editedBy: { - avatar_url: 'path', - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, + editedBy: null, }; describe('NoteEditedText', () => { let wrapper; - beforeEach(() => { + const createWrapper = (props = {}) => { wrapper = shallowMount(NoteEditedText, { - propsData, + propsData: { + ...propsData, + ...props, + }, + stubs: { + GlSprintf, + }, }); - }); + }; - afterEach(() => { - wrapper.destroy(); - }); + const findUserElement = () => wrapper.findComponent(GlLink); - it('should render block with provided className', () => { - expect(wrapper.classes()).toContain(propsData.className); - }); + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); - it('should render provided actionText', () => { - expect(wrapper.text().trim()).toContain(propsData.actionText); + it('should render block with provided className', () => { + expect(wrapper.classes()).toContain(propsData.className); + }); + + it('should render provided actionText', () => { + expect(wrapper.text().trim()).toContain(propsData.actionText); + }); + + it('should not render user information', () => { + expect(findUserElement().exists()).toBe(false); + }); }); - it('should render provided user information', () => { - const authorLink = wrapper.find('.js-user-link'); + describe('edited note', () => { + const editedBy = { + avatar_url: 'path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }; + + beforeEach(() => { + createWrapper({ editedBy }); + }); + + it('should render user information', () => { + const authorLink = findUserElement(); - expect(authorLink.attributes('href')).toEqual(propsData.editedBy.path); - expect(authorLink.text().trim()).toEqual(propsData.editedBy.name); + expect(authorLink.attributes('href')).toEqual(editedBy.path); + expect(authorLink.text().trim()).toEqual(editedBy.name); + }); }); }); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 90473e7ccba..59362e18098 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -48,10 +48,6 @@ describe('issue_note_form component', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('noteHash', () => { beforeEach(() => { wrapper = createComponentWrapper(); diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js index 56c22b09e1b..b3d6fab7f91 100644 --- a/spec/frontend/notes/components/note_header_spec.js +++ b/spec/frontend/notes/components/note_header_spec.js @@ -44,11 +44,6 @@ describe('NoteHeader component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('does not render discussion actions when includeToggle is false', () => { createComponent({ includeToggle: false, diff --git a/spec/frontend/notes/components/note_signed_out_widget_spec.js b/spec/frontend/notes/components/note_signed_out_widget_spec.js index 84f20e4ad58..d56ee234cd9 100644 --- a/spec/frontend/notes/components/note_signed_out_widget_spec.js +++ b/spec/frontend/notes/components/note_signed_out_widget_spec.js @@ -12,10 +12,6 @@ describe('NoteSignedOutWidget component', () => { wrapper = shallowMount(NoteSignedOutWidget, { store }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders sign in link provided in the store', () => { expect(wrapper.find(`a[href="${notesDataMock.newSessionPath}"]`).text()).toBe('sign in'); }); diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index a90d8bdde06..ac0c037fe36 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -22,7 +22,6 @@ jest.mock('~/behaviors/markdown/render_gfm'); describe('noteable_discussion component', () => { let store; let wrapper; - let originalGon; beforeEach(() => { window.mrTabs = {}; @@ -36,10 +35,6 @@ describe('noteable_discussion component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should not render thread header for non diff threads', () => { expect(wrapper.find('.discussion-header').exists()).toBe(false); }); @@ -167,16 +162,6 @@ describe('noteable_discussion component', () => { }); describe('signout widget', () => { - beforeEach(() => { - originalGon = { ...window.gon }; - window.gon = window.gon || {}; - }); - - afterEach(() => { - wrapper.destroy(); - window.gon = originalGon; - }); - describe('user is logged in', () => { beforeEach(() => { window.gon.current_user_id = userDataMock.id; diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index af1b4f64037..b158cfff10d 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -71,10 +71,6 @@ describe('issue_note', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('mutiline comments', () => { beforeEach(() => { createWrapper(); diff --git a/spec/frontend/notes/components/notes_activity_header_spec.js b/spec/frontend/notes/components/notes_activity_header_spec.js index 5b3165bf401..2de491477b6 100644 --- a/spec/frontend/notes/components/notes_activity_header_spec.js +++ b/spec/frontend/notes/components/notes_activity_header_spec.js @@ -24,10 +24,6 @@ describe('~/notes/components/notes_activity_header.vue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index b08a22f8674..832264aa7d3 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -90,8 +90,9 @@ describe('note_app', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy + wrapper.destroy(); }); describe('render', () => { diff --git a/spec/frontend/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js index 8c3696e88b7..ef5f06ad2fa 100644 --- a/spec/frontend/notes/components/toggle_replies_widget_spec.js +++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js @@ -30,10 +30,6 @@ describe('toggle replies widget for notes', () => { const mountComponent = ({ collapsed = false }) => mountExtended(ToggleRepliesWidget, { propsData: { replies, collapsed } }); - afterEach(() => { - wrapper.destroy(); - }); - describe('collapsed state', () => { beforeEach(() => { wrapper = mountComponent({ collapsed: true }); diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js index 6d3bc19bd45..40f10ca901b 100644 --- a/spec/frontend/notes/deprecated_notes_spec.js +++ b/spec/frontend/notes/deprecated_notes_spec.js @@ -23,7 +23,6 @@ const fixture = 'snippets/show.html'; let mockAxios; window.project_uploads_path = `${TEST_HOST}/uploads`; -window.gon = window.gon || {}; window.gl = window.gl || {}; gl.utils = gl.utils || {}; gl.utils.disableButtonIfEmptyField = () => {}; diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index c4c0dc58b0d..0d3ebea7af2 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -3,7 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import testAction from 'helpers/vuex_action_helper'; import { TEST_HOST } from 'spec/test_constants'; import Api from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import toast from '~/vue_shared/plugins/global_toast'; import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants'; import axios from '~/lib/utils/axios_utils'; @@ -36,7 +36,7 @@ import { const TEST_ERROR_MESSAGE = 'Test error message'; const mockAlertDismiss = jest.fn(); -jest.mock('~/flash', () => ({ +jest.mock('~/alert', () => ({ createAlert: jest.fn().mockImplementation(() => ({ dismiss: mockAlertDismiss, })), @@ -876,7 +876,7 @@ describe('Actions Notes Store', () => { const res = { errors: { base: ['something went wrong'] } }; const error = { message: 'Unprocessable entity', response: { data: res } }; - it('sets flash alert using errors.base message', async () => { + it('sets an alert using errors.base message', async () => { const resp = await actions.saveNote( { commit() {}, @@ -906,6 +906,20 @@ describe('Actions Notes Store', () => { expect(data).toBe(res); expect(createAlert).not.toHaveBeenCalled(); }); + + it('dispatches clearDrafts is command names contains submit_review', async () => { + const response = { command_names: ['submit_review'], valid: true }; + dispatch = jest.fn().mockResolvedValue(response); + await actions.saveNote( + { + commit() {}, + dispatch, + }, + payload, + ); + + expect(dispatch).toHaveBeenCalledWith('batchComments/clearDrafts'); + }); }); }); @@ -946,7 +960,7 @@ describe('Actions Notes Store', () => { }); }); - it('when service fails, flashes error message', () => { + it('when service fails, creates an alert with error message', () => { const response = { response: { data: { message: TEST_ERROR_MESSAGE } } }; Api.applySuggestion.mockReturnValue(Promise.reject(response)); @@ -1439,10 +1453,6 @@ describe('Actions Notes Store', () => { describe('fetchDiscussions', () => { const discussion = { notes: [] }; - afterEach(() => { - window.gon = {}; - }); - it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', () => { axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion }); return testAction( diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js index 70749557e61..0fbd073191e 100644 --- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js +++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js @@ -2,7 +2,6 @@ import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from ' import { shallowMount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; @@ -66,8 +65,6 @@ describe('CustomNotificationsModal', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mockAxios.restore(); }); @@ -87,24 +84,23 @@ describe('CustomNotificationsModal', () => { describe('checkbox items', () => { beforeEach(async () => { + const endpointUrl = '/api/v4/notification_settings'; + + mockAxios + .onGet(endpointUrl) + .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default); + wrapper = createComponent(); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - events: [ - { id: 'new_release', enabled: true, name: 'New release', loading: false }, - { id: 'new_note', enabled: false, name: 'New note', loading: true }, - ], - }); + wrapper.findComponent(GlModal).vm.$emit('show'); - await nextTick(); + await waitForPromises(); }); it.each` index | eventId | eventName | enabled | loading ${0} | ${'new_release'} | ${'New release'} | ${true} | ${false} - ${1} | ${'new_note'} | ${'New note'} | ${false} | ${true} + ${1} | ${'new_note'} | ${'New note'} | ${false} | ${false} `( 'renders a checkbox for "$eventName" with checked=$enabled', async ({ index, eventName, enabled, loading }) => { @@ -214,16 +210,9 @@ describe('CustomNotificationsModal', () => { wrapper = createComponent({ injectedProperties }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - events: [ - { id: 'new_release', enabled: true, name: 'New release', loading: false }, - { id: 'new_note', enabled: false, name: 'New note', loading: false }, - ], - }); + wrapper.findComponent(GlModal).vm.$emit('show'); - await nextTick(); + await waitForPromises(); findCheckboxAt(1).vm.$emit('change', true); @@ -241,19 +230,18 @@ describe('CustomNotificationsModal', () => { ); it('shows a toast message when the request fails', async () => { - mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {}); + const endpointUrl = '/api/v4/notification_settings'; + + mockAxios + .onGet(endpointUrl) + .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default); + + mockAxios.onPut(endpointUrl).reply(HTTP_STATUS_NOT_FOUND, {}); wrapper = createComponent(); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - events: [ - { id: 'new_release', enabled: true, name: 'New release', loading: false }, - { id: 'new_note', enabled: false, name: 'New note', loading: false }, - ], - }); + wrapper.findComponent(GlModal).vm.$emit('show'); - await nextTick(); + await waitForPromises(); findCheckboxAt(1).vm.$emit('change', true); diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js index 0f13de0e6d8..bae9b028cf7 100644 --- a/spec/frontend/notifications/components/notifications_dropdown_spec.js +++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js @@ -25,7 +25,7 @@ describe('NotificationsDropdown', () => { CustomNotificationsModal, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, provide: { dropdownItems: mockDropdownItems, @@ -61,8 +61,6 @@ describe('NotificationsDropdown', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mockAxios.restore(); }); diff --git a/spec/frontend/observability/index_spec.js b/spec/frontend/observability/index_spec.js new file mode 100644 index 00000000000..83f72ff72b5 --- /dev/null +++ b/spec/frontend/observability/index_spec.js @@ -0,0 +1,64 @@ +import { createWrapper } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import renderObservability from '~/observability/index'; +import ObservabilityApp from '~/observability/components/observability_app.vue'; +import { SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants'; + +describe('renderObservability', () => { + let element; + let vueInstance; + let component; + + const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE); + const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE); + + beforeEach(() => { + element = document.createElement('div'); + element.setAttribute('id', 'js-observability-app'); + element.dataset.observabilityIframeSrc = 'https://observe.gitlab.com/'; + document.body.appendChild(element); + + vueInstance = renderObservability(); + component = createWrapper(vueInstance).findComponent(ObservabilityApp); + }); + + afterEach(() => { + element.remove(); + }); + + it('should return a Vue instance', () => { + expect(vueInstance).toEqual(expect.any(Vue)); + }); + + it('should render the ObservabilityApp component', () => { + expect(component.props('observabilityIframeSrc')).toBe('https://observe.gitlab.com/'); + }); + + describe('skeleton variant', () => { + it.each` + pathDescription | path | variant + ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]} + ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]} + ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]} + ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]} + `( + 'renders the $variant skeleton variant for $pathDescription path', + async ({ path, variant }) => { + component.vm.$router.push(path); + await nextTick(); + + expect(component.props('skeletonVariant')).toBe(variant); + }, + ); + }); + + it('handle route-update events', async () => { + component.vm.$router.push('/something?foo=bar'); + component.vm.$emit('route-update', { url: '/some_path' }); + expect(component.vm.$router.currentRoute.path).toBe('/something'); + expect(component.vm.$router.currentRoute.query).toEqual({ + foo: 'bar', + observability_path: '/some_path', + }); + }); +}); diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js index e3bcd140d60..4a9be71b880 100644 --- a/spec/frontend/observability/observability_app_spec.js +++ b/spec/frontend/observability/observability_app_spec.js @@ -1,19 +1,20 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ObservabilityApp from '~/observability/components/observability_app.vue'; import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue'; - -import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants'; +import { + MESSAGE_EVENT_TYPE, + INLINE_EMBED_DIMENSIONS, + FULL_APP_DIMENSIONS, + SKELETON_VARIANT_EMBED, +} from '~/observability/constants'; import { darkModeEnabled } from '~/lib/utils/color_utils'; jest.mock('~/lib/utils/color_utils'); -describe('Observability root app', () => { +describe('ObservabilityApp', () => { let wrapper; - const replace = jest.fn(); - const $router = { - replace, - }; + const $route = { pathname: 'https://gitlab.com/gitlab-org/', path: 'https://gitlab.com/gitlab-org/-/observability/dashboards', @@ -26,21 +27,19 @@ describe('Observability root app', () => { const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840'; - const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE); - - const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE); + const TEST_USERNAME = 'test-user'; - const mountComponent = (route = $route) => { + const mountComponent = (props) => { wrapper = shallowMountExtended(ObservabilityApp, { propsData: { observabilityIframeSrc: TEST_IFRAME_SRC, + ...props, }, stubs: { 'observability-skeleton': ObservabilitySkeleton, }, mocks: { - $router, - $route: route, + $route, }, }); }; @@ -48,17 +47,11 @@ describe('Observability root app', () => { const dispatchMessageEvent = (message) => window.dispatchEvent(new MessageEvent('message', message)); - afterEach(() => { - wrapper.destroy(); + beforeEach(() => { + gon.current_username = TEST_USERNAME; }); describe('iframe src', () => { - const TEST_USERNAME = 'test-user'; - - beforeAll(() => { - gon.current_username = TEST_USERNAME; - }); - it('should render an iframe with observabilityIframeSrc, decorated with light theme and username', () => { darkModeEnabled.mockReturnValueOnce(false); mountComponent(); @@ -92,48 +85,70 @@ describe('Observability root app', () => { }); }); - describe('on GOUI_ROUTE_UPDATE', () => { - it('should not call replace method from vue router if message event does not have url', () => { - mountComponent(); - dispatchMessageEvent({ - type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, - payload: { data: 'some other data' }, + describe('iframe kiosk query param', () => { + it('when inlineEmbed, it should set the proper kiosk query parameter', () => { + mountComponent({ + inlineEmbed: true, }); - expect(replace).not.toHaveBeenCalled(); + + const iframe = findIframe(); + + expect(iframe.attributes('src')).toBe( + `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}&kiosk=inline-embed`, + ); }); + }); - it.each` - condition | origin | observability_path | url - ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'} - ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'} - `( - 'should not call replace method from vue router if $condition', - async ({ origin, observability_path, url }) => { - mountComponent({ ...$route, query: { observability_path } }); - dispatchMessageEvent({ - data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url } }, - origin, - }); - expect(replace).not.toHaveBeenCalled(); - }, - ); + describe('iframe size', () => { + it('should set the specified size', () => { + mountComponent({ + height: INLINE_EMBED_DIMENSIONS.HEIGHT, + width: INLINE_EMBED_DIMENSIONS.WIDTH, + }); + + const iframe = findIframe(); + + expect(iframe.attributes('width')).toBe(INLINE_EMBED_DIMENSIONS.WIDTH); + expect(iframe.attributes('height')).toBe(INLINE_EMBED_DIMENSIONS.HEIGHT); + }); + + it('should fallback to default size', () => { + mountComponent({}); + + const iframe = findIframe(); - it('should call replace method from vue router on message event callback', () => { + expect(iframe.attributes('width')).toBe(FULL_APP_DIMENSIONS.WIDTH); + expect(iframe.attributes('height')).toBe(FULL_APP_DIMENSIONS.HEIGHT); + }); + }); + + describe('skeleton variant', () => { + it('sets the specified skeleton variant', () => { + mountComponent({ skeletonVariant: SKELETON_VARIANT_EMBED }); + const props = wrapper.findComponent(ObservabilitySkeleton).props(); + + expect(props.variant).toBe(SKELETON_VARIANT_EMBED); + }); + + it('should have a default skeleton variant', () => { + mountComponent(); + const props = wrapper.findComponent(ObservabilitySkeleton).props(); + + expect(props.variant).toBe('dashboards'); + }); + }); + + describe('on GOUI_ROUTE_UPDATE', () => { + it('should emit a route-update event', () => { mountComponent(); + const payload = { url: '/explore' }; dispatchMessageEvent({ - data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } }, + data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload }, origin: 'https://observe.gitlab.com', }); - expect(replace).toHaveBeenCalled(); - expect(replace).toHaveBeenCalledWith({ - name: 'https://gitlab.com/gitlab-org/', - query: { - otherQuery: 100, - observability_path: '/explore', - }, - }); + expect(wrapper.emitted('route-update')[0]).toEqual([payload]); }); }); @@ -167,34 +182,17 @@ describe('Observability root app', () => { }); }); - describe('skeleton variant', () => { - it.each` - pathDescription | path | variant - ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]} - ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]} - ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]} - ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]} - `('renders the $variant skeleton variant for $pathDescription path', ({ path, variant }) => { - mountComponent({ ...$route, path }); - const props = wrapper.findComponent(ObservabilitySkeleton).props(); - - expect(props.variant).toBe(variant); - }); - }); - - describe('on observability ui unmount', () => { - it('should remove message event and should not call replace method from vue router', () => { + describe('on unmount', () => { + it('should not emit any even on route update', () => { mountComponent(); wrapper.destroy(); - // testing event cleanup logic, should not call on messege event after component is destroyed - dispatchMessageEvent({ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } }, origin: 'https://observe.gitlab.com', }); - expect(replace).not.toHaveBeenCalled(); + expect(wrapper.emitted('route-update')).toBeUndefined(); }); }); }); diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js index a95597d8516..65dbb003743 100644 --- a/spec/frontend/observability/skeleton_spec.js +++ b/spec/frontend/observability/skeleton_spec.js @@ -6,8 +6,13 @@ import Skeleton from '~/observability/components/skeleton/index.vue'; import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue'; import ExploreSkeleton from '~/observability/components/skeleton/explore.vue'; import ManageSkeleton from '~/observability/components/skeleton/manage.vue'; +import EmbedSkeleton from '~/observability/components/skeleton/embed.vue'; -import { SKELETON_VARIANTS_BY_ROUTE, DEFAULT_TIMERS } from '~/observability/constants'; +import { + SKELETON_VARIANTS_BY_ROUTE, + DEFAULT_TIMERS, + SKELETON_VARIANT_EMBED, +} from '~/observability/constants'; describe('Skeleton component', () => { let wrapper; @@ -22,6 +27,8 @@ describe('Skeleton component', () => { const findManageSkeleton = () => wrapper.findComponent(ManageSkeleton); + const findEmbedSkeleton = () => wrapper.findComponent(EmbedSkeleton); + const findAlert = () => wrapper.findComponent(GlAlert); const mountComponent = ({ ...props } = {}) => { @@ -97,16 +104,20 @@ describe('Skeleton component', () => { ${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANTS[0]} ${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]} ${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]} + ${'embed'} | ${'variant is embed'} | ${SKELETON_VARIANT_EMBED} ${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'} `('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => { mountComponent({ variant }); jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS); await nextTick(); - const showsDefaultSkeleton = !SKELETON_VARIANTS.includes(variant); + const showsDefaultSkeleton = ![...SKELETON_VARIANTS, SKELETON_VARIANT_EMBED].includes( + variant, + ); expect(findDashboardsSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[0]); expect(findExploreSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[1]); expect(findManageSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[2]); + expect(findEmbedSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANT_EMBED); expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton); }); diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js index 732dfdd42fb..ee450dfc851 100644 --- a/spec/frontend/operation_settings/components/metrics_settings_spec.js +++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js @@ -2,7 +2,7 @@ import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitla import { mount, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { timezones } from '~/monitoring/format_date'; @@ -13,7 +13,7 @@ import MetricsSettings from '~/operation_settings/components/metrics_settings.vu import store from '~/operation_settings/store'; jest.mock('~/lib/utils/url_utility'); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('operation settings external dashboard component', () => { let wrapper; @@ -198,7 +198,7 @@ describe('operation settings external dashboard component', () => { expect(refreshCurrentPage).toHaveBeenCalled(); }); - it('creates flash banner on error', async () => { + it('creates alert banner on error', async () => { mountComponent(false); const message = 'mockErrorMessage'; axios.patch.mockRejectedValue({ response: { data: { message } } }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js index ff11c8843bb..8ba7e40d728 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js @@ -27,11 +27,6 @@ describe('delete_button', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('tooltip', () => { it('the title is controlled by tooltipTitle prop', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js index 620c96e8c9e..5a7cbdcff5b 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js @@ -46,11 +46,6 @@ describe('Delete Image', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('executes apollo mutate on doDelete', () => { const mutate = jest.fn().mockResolvedValue({}); mountComponent({ mutate }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js index d45b993b5a2..9d187439ca3 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js @@ -19,11 +19,6 @@ describe('Delete alert', () => { wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when deleteAlertType is null', () => { it('does not show the alert', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js index 16c9485e69e..860f9b3a0c1 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js @@ -30,15 +30,10 @@ describe('Delete Modal', () => { const expectPrimaryActionStatus = (disabled = true) => expect(findModal().props('actionPrimary')).toMatchObject( expect.objectContaining({ - attributes: [{ variant: 'danger' }, { disabled }], + attributes: { variant: 'danger', disabled }, }), ); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('contains a GlModal', () => { mountComponent(); expect(findModal().exists()).toBe(true); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js index b37edac83f7..9e443234c34 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js @@ -73,7 +73,7 @@ describe('Details Header', () => { apolloProvider, propsData, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { TitleArea, @@ -85,9 +85,7 @@ describe('Details Header', () => { afterEach(() => { // if we want to mix createMockApollo and manual mocks we need to reset everything - wrapper.destroy(); apolloProvider = undefined; - wrapper = null; }); describe('image name', () => { diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js index ce5ecfe4608..d6c1b2c3f51 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js @@ -23,11 +23,6 @@ describe('Partial Cleanup alert', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it(`gl-alert has the correct properties`, () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js index d83a5099bcd..3e1fd14475d 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js @@ -27,11 +27,6 @@ describe('Status Alert', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` status | title | variant | message | link ${DELETE_SCHEDULED} | ${SCHEDULED_FOR_DELETION_STATUS_TITLE} | ${'info'} | ${SCHEDULED_FOR_DELETION_STATUS_MESSAGE} | ${PACKAGE_DELETE_HELP_PAGE_PATH} diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js index fa0d76762df..bfefe46c09b 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js @@ -50,16 +50,11 @@ describe('tags list row', () => { }, propsData, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('checkbox', () => { it('exists', () => { mountComponent(); @@ -283,26 +278,30 @@ describe('tags list row', () => { textSrOnly: true, category: 'tertiary', right: true, + disabled: false, }); }); - it.each` - canDelete | digest | disabled | buttonDisabled - ${true} | ${null} | ${true} | ${true} - ${false} | ${'foo'} | ${true} | ${true} - ${false} | ${null} | ${true} | ${true} - ${true} | ${'foo'} | ${true} | ${true} - ${true} | ${'foo'} | ${false} | ${false} - `( - 'is $visible that is visible when canDelete is $canDelete and digest is $digest and disabled is $disabled', - ({ canDelete, digest, disabled, buttonDisabled }) => { - mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled }); + it('has the correct classes', () => { + mountComponent(); - expect(findAdditionalActionsMenu().props('disabled')).toBe(buttonDisabled); - expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(buttonDisabled); - expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(buttonDisabled); - }, - ); + expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(false); + expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(false); + }); + + it('is not rendered when tag.canDelete is false', () => { + mountComponent({ ...defaultProps, tag: { ...tag, canDelete: false } }); + + expect(findAdditionalActionsMenu().exists()).toBe(false); + }); + + it('is hidden when disabled prop is set to true', () => { + mountComponent({ ...defaultProps, disabled: true }); + + expect(findAdditionalActionsMenu().props('disabled')).toBe(true); + expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(true); + expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(true); + }); describe('delete button', () => { it('exists and has the correct attrs', () => { 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 1017ff06a25..09d0370efbf 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 @@ -68,15 +68,11 @@ describe('Tags List', () => { resolver = jest.fn().mockResolvedValue(imageTagsMock()); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('registry list', () => { - beforeEach(() => { + beforeEach(async () => { mountComponent(); fireFirstSortUpdate(); - return waitForApolloRequestRender(); + await waitForApolloRequestRender(); }); it('has a persisted search', () => { @@ -98,6 +94,7 @@ describe('Tags List', () => { pagination: tagsPageInfo, items: tags, idProperty: 'name', + hiddenDelete: false, }); }); @@ -186,12 +183,23 @@ describe('Tags List', () => { }); }); + describe('when user does not have permission to delete list rows', () => { + it('sets registry list hiddenDelete prop to true', async () => { + resolver = jest.fn().mockResolvedValue(imageTagsMock({ canDelete: false })); + mountComponent(); + fireFirstSortUpdate(); + await waitForApolloRequestRender(); + + expect(findRegistryList().props('hiddenDelete')).toBe(true); + }); + }); + describe('when the list of tags is empty', () => { - beforeEach(() => { - resolver = jest.fn().mockResolvedValue(imageTagsMock([])); + beforeEach(async () => { + resolver = jest.fn().mockResolvedValue(imageTagsMock({ nodes: [] })); mountComponent(); fireFirstSortUpdate(); - return waitForApolloRequestRender(); + await waitForApolloRequestRender(); }); it('does not show the loader', () => { diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js index 88e79c513bc..8896185ce67 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js @@ -20,11 +20,6 @@ describe('TagsLoader component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('produces the correct amount of loaders', () => { mountComponent(); expect(findGlSkeletonLoaders().length).toBe(1); 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 535faebdd4e..0d1d2c53cab 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 @@ -36,10 +36,6 @@ describe('cleanup_status', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` status | visible | text ${UNFINISHED_STATUS} | ${true} | ${CLEANUP_STATUS_UNFINISHED} diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js index d2086943e4f..900ea61e4ea 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js @@ -26,10 +26,6 @@ describe('Registry Group Empty state', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('to match the default snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js index 75068591007..7da9c7533a0 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js @@ -1,4 +1,4 @@ -import { GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui'; +import { GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { createMockDirective } from 'helpers/vue_mock_directive'; import { mockTracking } from 'helpers/tracking_helper'; @@ -49,16 +49,11 @@ describe('Image List Row', () => { config: {}, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('image title and path', () => { it('renders shortened name of image and contains a link to the details page', () => { mountComponent(); @@ -206,13 +201,6 @@ describe('Image List Row', () => { expect(findTagsCount().exists()).toBe(true); }); - it('contains a tag icon', () => { - mountComponent(); - const icon = findTagsCount().findComponent(GlIcon); - expect(icon.exists()).toBe(true); - expect(icon.props('name')).toBe('tag'); - }); - describe('loading state', () => { it('shows a loader when metadataLoading is true', () => { mountComponent({ metadataLoading: true }); @@ -231,12 +219,12 @@ describe('Image List Row', () => { it('with one tag in the image', () => { mountComponent({ item: { ...item, tagsCount: 1 } }); - expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag'); + expect(findTagsCount().text()).toMatchInterpolatedText('1 tag'); }); it('with more than one tag in the image', () => { mountComponent({ item: { ...item, tagsCount: 3 } }); - expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags'); + expect(findTagsCount().text()).toMatchInterpolatedText('3 tags'); }); }); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js index 042b8383571..6c771887b88 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js @@ -21,11 +21,6 @@ describe('Image List', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('list', () => { it('contains one list element for each image', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js index 8cfa8128021..e4d13143484 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js @@ -34,10 +34,6 @@ describe('Registry Project Empty state', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('to match the default snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js index bcc8e41fce8..45304cc2329 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js @@ -35,11 +35,6 @@ describe('registry_header', () => { await nextTick(); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('header', () => { it('has a title', () => { mountComponent({ metadataLoading: true }); 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 e5b99f15e8c..cd54b856c97 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 @@ -177,11 +177,12 @@ export const tagsMock = [ }, ]; -export const imageTagsMock = (nodes = tagsMock) => ({ +export const imageTagsMock = ({ nodes = tagsMock, canDelete = true } = {}) => ({ data: { containerRepository: { id: containerRepositoryMock.id, tagsCount: nodes.length, + canDelete, tags: { nodes, pageInfo: { ...tagsPageInfo }, diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js index 26f0e506829..888c3e5bffa 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js @@ -127,11 +127,6 @@ describe('Details Page', () => { jest.spyOn(Tracking, 'event'); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when isLoading is true', () => { it('shows the loader', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js index 1e514d85e82..acc61157ab5 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js @@ -113,10 +113,6 @@ describe('List Page', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('contains registry header', async () => { mountComponent(); fireFirstSortUpdate(); 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 601f8abd34d..c2ae34ce697 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js @@ -31,11 +31,6 @@ import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock const dummyApiVersion = 'v3000'; const dummyGrouptId = 1; const dummyUrlRoot = '/gitlab'; -const dummyGon = { - api_version: dummyApiVersion, - relative_url_root: dummyUrlRoot, -}; -let originalGon; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${dummyGrouptId}/dependency_proxy/cache`; Vue.use(VueApollo); @@ -89,16 +84,16 @@ describe('DependencyProxyApp', () => { beforeEach(() => { resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()); - originalGon = window.gon; - window.gon = { ...dummyGon }; + window.gon = { + api_version: dummyApiVersion, + relative_url_root: dummyUrlRoot, + }; mock = new MockAdapter(axios); mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {}); }); afterEach(() => { - wrapper.destroy(); - window.gon = originalGon; mock.restore(); }); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js index 2f415bfd6f9..639a4fbb99d 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js @@ -25,10 +25,6 @@ describe('Manifests List', () => { const findRows = () => wrapper.findAllComponents(ManifestRow); const findPagination = () => wrapper.findComponent(GlKeysetPagination); - afterEach(() => { - wrapper.destroy(); - }); - it('has the correct title', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js index be3236d1f9c..ace5ce3a58d 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js @@ -29,10 +29,6 @@ describe('Manifest Row', () => { const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip); const findStatus = () => wrapper.findByTestId('status'); - afterEach(() => { - wrapper.destroy(); - }); - describe('With a manifest on the DEFAULT status', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js index a2e5cbdce8b..1e9b9b1ce47 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js @@ -63,10 +63,6 @@ describe('Harbor artifact list row', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('list item', () => { beforeEach(() => { mountComponent({ diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js index b9d6dc2679e..786a4715731 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js @@ -26,10 +26,6 @@ describe('Harbor artifacts list', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when isLoading is true', () => { beforeEach(() => { mountComponent({ diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js index e8cc2b2e22d..d8fb91c085c 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js @@ -20,10 +20,6 @@ describe('Harbor Details Header', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('artifact name', () => { describe('missing image name', () => { beforeEach(() => { diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js index 7a6169d300c..9a7ad759dba 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js @@ -29,10 +29,6 @@ describe('harbor_list_header', () => { await nextTick(); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('header', () => { it('has a title', () => { mountComponent({ metadataLoading: true }); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js index b62d4e8836b..1e031e0557a 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js @@ -28,10 +28,6 @@ describe('Harbor List Row', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('image title and path', () => { it('contains a link to the details page', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js index e7e74a0da58..a1803ecf7fb 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js @@ -20,10 +20,6 @@ describe('Harbor List', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('list', () => { it('contains one list element for each image', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js index 5e299a269e3..9370ff1fdd4 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js @@ -26,10 +26,6 @@ describe('Harbor Tags Header', () => { totalPages: 1, }; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { mountComponent({ propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false }, diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js index 849215e286b..0b2ce01ebf6 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js @@ -37,10 +37,6 @@ describe('Harbor tag list row', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('list item', () => { beforeEach(() => { mountComponent({ diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js index 4c6b2b6daaa..e2a2a584b7d 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js @@ -24,10 +24,6 @@ describe('Harbor Tags List', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when isLoading is true', () => { beforeEach(() => { mountComponent({ diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js index 69765d31674..90c3d9082f7 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js @@ -74,10 +74,6 @@ describe('Harbor Details Page', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when isLoading is true', () => { it('shows the loader', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js index 97d30e6fe99..63ea8feb1e7 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js @@ -60,10 +60,6 @@ describe('Harbor List Page', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains harbor registry header', async () => { mountComponent(); fireFirstSortUpdate(); diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js index 10901c6ec1e..6002faa1fa3 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js @@ -60,10 +60,6 @@ describe('Harbor Tags page', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains tags header', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js index e74375b7705..f8130287c12 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js @@ -86,10 +86,6 @@ describe('PackagesApp', () => { const findTerraformInstallation = () => wrapper.findComponent(TerraformInstallation); const findPackageFiles = () => wrapper.findComponent(PackageFiles); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the app and displays the package title', async () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js index b504f7489ab..148e87699f1 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js @@ -39,10 +39,6 @@ describe('PackageTitle', () => { const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]'); const packageRef = () => wrapper.find('[data-testid="package-ref"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('module title', () => { it('is correctly bound', async () => { await createComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js index d7caa8ca2d8..7352afff051 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js @@ -23,10 +23,6 @@ describe('FileSha', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js index b76d7c2b57b..c3e0818fc11 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js @@ -37,11 +37,6 @@ describe('Package Files', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('rows', () => { it('renders a single file for an npm package', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js index 0cbe2755f7e..a650aba464e 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js @@ -30,11 +30,6 @@ describe('Package History', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findHistoryElement = (testId) => wrapper.find(`[data-testid="${testId}"]`); const findElementLink = (container) => container.findComponent(GlLink); const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js index 78c1b840dbc..94797f01d16 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js @@ -30,10 +30,6 @@ describe('TerraformInstallation', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all the messages', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js index bb970336b94..ea4d268d84e 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js @@ -1,6 +1,6 @@ import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages_and_registries/infrastructure_registry/details/constants'; import { fetchPackageVersions, @@ -15,7 +15,7 @@ import { } from '~/packages_and_registries/shared/constants'; import { npmPackage as packageEntity } from '../../mock_data'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); jest.mock('~/api.js'); describe('Actions Package details store', () => { @@ -53,7 +53,7 @@ describe('Actions Package details store', () => { expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id); }); - it('should create flash on API error', async () => { + it('should create alert on API error', async () => { Api.projectPackage = jest.fn().mockRejectedValue(); await testAction( @@ -83,7 +83,7 @@ describe('Actions Package details store', () => { packageEntity.id, ); }); - it('should create flash on API error', async () => { + it('should create alert on API error', async () => { Api.deleteProjectPackage = jest.fn().mockRejectedValue(); await testAction(deletePackage, undefined, { packageEntity }, [], []); @@ -118,7 +118,7 @@ describe('Actions Package details store', () => { }); }); - it('should create flash on API error', async () => { + it('should create alert on API error', async () => { Api.deleteProjectPackageFile = jest.fn().mockRejectedValue(); await testAction(deletePackageFile, fileId, { packageEntity }, [], []); expect(createAlert).toHaveBeenCalledWith({ diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap index 801cde8582e..d0841c6110f 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -49,9 +49,7 @@ exports[`packages_list_app renders 1`] = ` Learn how to <b-link-stub class="gl-link" - event="click" href="helpUrl" - routertag="a" target="_blank" > publish and share your packages diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js index a086c20a5e7..a89247c0a97 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js @@ -55,11 +55,6 @@ describe('Infrastructure Search', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('has a registry search component', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js index aca6b0942cc..7c7faa8a3b0 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js @@ -22,11 +22,6 @@ describe('Infrastructure Title', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('title area', () => { beforeEach(() => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js index d237023d0cd..47d36d11e35 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import setWindowLocation from 'helpers/set_window_location_helper'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_INFO } from '~/alert'; import * as commonUtils from '~/lib/utils/common_utils'; import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants'; @@ -14,7 +14,7 @@ import InfrastructureSearch from '~/packages_and_registries/infrastructure_regis import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; jest.mock('~/lib/utils/common_utils'); -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(Vuex); @@ -72,10 +72,6 @@ describe('packages_list_app', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { createStore({ packageCount: 1 }); mountComponent(); @@ -217,7 +213,7 @@ describe('packages_list_app', () => { setWindowLocation(originalLocation); }); - it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => { + it(`creates an alert if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => { mountComponent(); expect(createAlert).toHaveBeenCalledWith({ diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js index 0164d92ce34..51445942eaa 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js @@ -4,13 +4,13 @@ import Vue from 'vue'; import { last } from 'lodash'; import Vuex from 'vuex'; import stubChildren from 'helpers/stub_children'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import PackagesList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue'; import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue'; import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants'; import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants'; -import Tracking from '~/tracking'; import { packageList } from '../../mock_data'; Vue.use(Vuex); @@ -72,11 +72,6 @@ describe('packages_list', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when is loading', () => { beforeEach(() => { mountComponent({ @@ -179,23 +174,23 @@ describe('packages_list', () => { }); describe('tracking', () => { - let eventSpy; + let trackingSpy = null; beforeEach(() => { mountComponent(); - eventSpy = jest.spyOn(Tracking, 'event'); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } }); - }); - - it('deleteItemConfirmation calls event', () => { - wrapper.vm.deleteItemConfirmation(); - expect(eventSpy).toHaveBeenCalledWith( - TRACK_CATEGORY, - TRACKING_ACTIONS.DELETE_PACKAGE, - expect.any(Object), - ); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('deleteItemConfirmation calls event', async () => { + await findPackageListDeleteModal().vm.$emit('ok'); + + expect(trackingSpy).toHaveBeenCalledWith(TRACK_CATEGORY, TRACKING_ACTIONS.DELETE_PACKAGE, { + category: TRACK_CATEGORY, + }); }); }); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js index 2c185e040f4..4f051264172 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js @@ -2,14 +2,14 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants'; import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions'; import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types'; import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); jest.mock('~/api.js'); describe('Actions Package list store', () => { @@ -96,7 +96,7 @@ describe('Actions Package list store', () => { }); }); - it('should create flash on API error', async () => { + it('should create alert on API error', async () => { Api.projectPackages = jest.fn().mockRejectedValue(); await testAction( actions.requestPackagesList, @@ -198,7 +198,7 @@ describe('Actions Package list store', () => { ); }); - it('should stop the loading and call create flash on api error', async () => { + it('should stop the loading and call create alert on api error', async () => { mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_BAD_REQUEST); await testAction( actions.requestDeletePackage, 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 721bdd34a4f..37ca420ae77 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 @@ -49,16 +49,11 @@ describe('packages_list_row', () => { disableDelete, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders', () => { mountComponent(); expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js index 9c1ebf5a2eb..d0817a8678e 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js @@ -45,7 +45,7 @@ describe('DeleteModal', () => { it('passes actionPrimary prop', () => { expect(findModal().props('actionPrimary')).toStrictEqual({ text: 'Permanently delete', - attributes: [{ variant: 'danger' }, { category: 'primary' }], + attributes: { variant: 'danger', category: 'primary' }, }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap index 67f1906f6fd..9b429c39faa 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap @@ -89,8 +89,9 @@ exports[`MavenInstallation maven renders all the messages 1`] = ` /> <code-instruction-stub + class="gl-w-20 gl-mt-5" copytext="Copy Maven command" - instruction="mvn dependency:get -Dartifact=appGroup:appName:appVersion" + instruction="mvn install" label="Maven Command" trackingaction="copy_maven_command" trackinglabel="code_instruction" 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 b2375da7b11..5d390730ef1 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 @@ -20,7 +20,7 @@ exports[`PypiInstallation renders all the messages 1`] = ` <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" id="__BVID__27__BV_toggle_" type="button" @@ -59,7 +59,6 @@ exports[`PypiInstallation renders all the messages 1`] = ` </div> <fieldset - aria-describedby="installation-pip-command-group__BV_description_" class="form-group gl-form-group" id="installation-pip-command-group" > @@ -75,12 +74,7 @@ exports[`PypiInstallation renders all the messages 1`] = ` <!----> </legend> - <div - aria-labelledby="installation-pip-command-group__BV_label_" - class="bv-no-focus-ring" - role="group" - tabindex="-1" - > + <div> <div data-testid="pip-command" id="installation-pip-command" diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js index 4f3d780b149..2e59c27cc1b 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js @@ -65,11 +65,6 @@ describe('Package Additional metadata', () => { jest.spyOn(Sentry, 'captureException').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findTitle = () => wrapper.findByTestId('title'); const findMainArea = () => wrapper.findByTestId('main'); const findComponentIs = () => wrapper.findByTestId('component-is'); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js index 0aba8f7efc7..a6298ebdea7 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js @@ -34,10 +34,6 @@ describe('ComposerInstallation', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('install command switch', () => { it('has the installation title component', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js index bf9425def9a..70534b1d0a6 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js @@ -33,10 +33,6 @@ describe('ConanInstallation', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all the messages', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js index 9aed5b90c73..19aedf120b2 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js @@ -19,10 +19,6 @@ describe('DependencyRow', () => { const dependencyVersion = () => wrapper.findByTestId('version-pattern'); const dependencyFramework = () => wrapper.findByTestId('target-framework'); - afterEach(() => { - wrapper.destroy(); - }); - describe('renders', () => { it('full dependency', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js index feed7a7c46c..a9428773a60 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js @@ -23,10 +23,6 @@ describe('FileSha', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js index 5fe795f768e..a2d30be13c2 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js @@ -20,10 +20,6 @@ describe('InstallationTitle', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('has a title', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js index 8bb05b00e65..d35d95e319f 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js @@ -40,10 +40,6 @@ describe('InstallationCommands', () => { const pypiInstallation = () => wrapper.findComponent(PypiInstallation); const composerInstallation = () => wrapper.findComponent(ComposerInstallation); - afterEach(() => { - wrapper.destroy(); - }); - describe('installation instructions', () => { describe.each` packageEntity | selector diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js index fc60039db30..5ea81dccf7d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js @@ -35,7 +35,7 @@ describe('MavenInstallation', () => { <artifactId>appName</artifactId> <version>appVersion</version> </dependency>`; - const mavenCommandStr = 'mvn dependency:get -Dartifact=appGroup:appName:appVersion'; + const mavenCommandStr = 'mvn install'; const mavenSetupXml = `<repositories> <repository> <id>gitlab-maven</id> @@ -79,10 +79,6 @@ describe('MavenInstallation', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('install command switch', () => { it('has the installation title component', () => { createComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js index bb6846d354f..f2f3b8507c3 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js @@ -18,11 +18,6 @@ describe('Composer Metadata', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findComposerTargetSha = () => wrapper.findByTestId('composer-target-sha'); const findComposerTargetShaCopyButton = () => wrapper.findComponent(ClipboardButton); const findComposerJson = () => wrapper.findByTestId('composer-json'); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js index e7e47401aa1..2832dc3a712 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js @@ -19,11 +19,6 @@ describe('Conan Metadata', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findConanRecipe = () => wrapper.findByTestId('conan-recipe'); beforeEach(() => { diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js index 8680d983042..7b253a26fc7 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js @@ -19,11 +19,6 @@ describe('Maven Metadata', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findMavenApp = () => wrapper.findByTestId('maven-app'); const findMavenGroup = () => wrapper.findByTestId('maven-group'); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js index af3692023f0..9fb467f9af1 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js @@ -19,11 +19,6 @@ describe('Nuget Metadata', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findNugetSource = () => wrapper.findByTestId('nuget-source'); const findNugetLicense = () => wrapper.findByTestId('nuget-license'); const findElementLink = (container) => container.findComponent(GlLink); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js index d7c6ea8379d..67f5fbc9e80 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js @@ -20,11 +20,6 @@ describe('Package Additional Metadata', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findPypiRequiredPython = () => wrapper.findByTestId('pypi-required-python'); beforeEach(() => { diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js index 8c0e2d948ca..e711f9ee45d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js @@ -51,10 +51,6 @@ describe('NpmInstallation', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all the messages', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js index 9449c40c7c6..bcc0b78bfce 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js @@ -36,10 +36,6 @@ describe('NugetInstallation', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders all the messages', () => { expect(wrapper.element).toMatchSnapshot(); }); 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 529a6a22ddf..1dcac017ccf 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 @@ -48,10 +48,6 @@ describe('Package Files', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('rows', () => { it('renders a single file for an npm package', () => { createComponent(); 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 bb2fa9eb6f5..ed470f63b8a 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 @@ -63,11 +63,6 @@ describe('Package History', () => { jest.spyOn(Sentry, 'captureException').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader); const findHistoryElement = (testId) => wrapper.findByTestId(testId); const findElementLink = (container) => container.findComponent(GlLink); 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 1fda77f2aaa..ba21cdaca3b 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 @@ -38,7 +38,7 @@ describe('PackageTitle', () => { }, provide, directives: { - GlResizeObserver: createMockDirective(), + GlResizeObserver: createMockDirective('gl-resize-observer'), }, }); await nextTick(); @@ -55,10 +55,6 @@ describe('PackageTitle', () => { const findSubHeaderText = () => wrapper.findByTestId('sub-header'); const findSubHeaderTimeAgo = () => wrapper.findComponent(TimeAgoTooltip); - afterEach(() => { - wrapper.destroy(); - }); - describe('renders', () => { it('without tags', async () => { await createComponent({ ...packageData(), packageFiles: { nodes: packageFiles() } }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js index 27c0ab96cfc..fc7f5c80d45 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js @@ -1,14 +1,18 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; +import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue'; import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import Tracking from '~/tracking'; import { + CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + DELETE_PACKAGE_VERSION_TRACKING_ACTION, DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, } from '~/packages_and_registries/package_registry/constants'; import { packageData } from '../../mock_data'; @@ -22,7 +26,7 @@ describe('PackageVersionsList', () => { name: 'version 1', }), packageData({ - id: `gid://gitlab/Packages::Package/112`, + id: 'gid://gitlab/Packages::Package/112', name: 'version 2', }), ]; @@ -31,8 +35,10 @@ describe('PackageVersionsList', () => { findLoader: () => wrapper.findComponent(PackagesListLoader), findRegistryList: () => wrapper.findComponent(RegistryList), findEmptySlot: () => wrapper.findComponent(EmptySlotStub), - findListRow: () => wrapper.findAllComponents(VersionRow), + findListRow: () => wrapper.findComponent(VersionRow), + findAllListRow: () => wrapper.findAllComponents(VersionRow), findDeletePackagesModal: () => wrapper.findComponent(DeleteModal), + findPackageListDeleteModal: () => wrapper.findComponent(DeletePackageModal), }; const mountComponent = (props) => { wrapper = shallowMountExtended(PackageVersionsList, { @@ -118,16 +124,16 @@ describe('PackageVersionsList', () => { }); it('displays package version rows', () => { - expect(uiElements.findListRow().exists()).toEqual(true); - expect(uiElements.findListRow()).toHaveLength(packageList.length); + expect(uiElements.findAllListRow().exists()).toEqual(true); + expect(uiElements.findAllListRow()).toHaveLength(packageList.length); }); it('binds the correct props', () => { - expect(uiElements.findListRow().at(0).props()).toMatchObject({ + expect(uiElements.findAllListRow().at(0).props()).toMatchObject({ packageEntity: expect.objectContaining(packageList[0]), }); - expect(uiElements.findListRow().at(1).props()).toMatchObject({ + expect(uiElements.findAllListRow().at(1).props()).toMatchObject({ packageEntity: expect.objectContaining(packageList[1]), }); }); @@ -159,6 +165,68 @@ describe('PackageVersionsList', () => { }); }); + describe.each` + description | finderFunction | deletePayload + ${'when the user can destroy the package'} | ${uiElements.findListRow} | ${packageList[0]} + ${'when the user can bulk destroy packages and deletes only one package'} | ${uiElements.findRegistryList} | ${[packageList[0]]} + `('$description', ({ finderFunction, deletePayload }) => { + let eventSpy; + const category = 'UI::NpmPackages'; + const { findPackageListDeleteModal } = uiElements; + + beforeEach(() => { + eventSpy = jest.spyOn(Tracking, 'event'); + mountComponent({ canDestroy: true }); + finderFunction().vm.$emit('delete', deletePayload); + }); + + it('passes itemToBeDeleted to the modal', () => { + expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(packageList[0]); + }); + + it('requesting delete tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + category, + REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION, + expect.any(Object), + ); + }); + + describe('when modal confirms', () => { + beforeEach(() => { + findPackageListDeleteModal().vm.$emit('ok'); + }); + + it('emits delete when modal confirms', () => { + expect(wrapper.emitted('delete')[0][0]).toEqual([packageList[0]]); + }); + + it('tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + category, + DELETE_PACKAGE_VERSION_TRACKING_ACTION, + expect.any(Object), + ); + }); + }); + + it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => { + await findPackageListDeleteModal().vm.$emit(event); + + expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull(); + }); + + it('canceling delete tracks the right action', () => { + findPackageListDeleteModal().vm.$emit('cancel'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION, + expect.any(Object), + ); + }); + }); + describe('when the user can bulk destroy versions', () => { let eventSpy; const { findDeletePackagesModal, findRegistryList } = uiElements; 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 4a27f8011df..3f4358bb3b0 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 @@ -29,10 +29,13 @@ password = <your personal access token>`; const findInstallationTitle = () => wrapper.findComponent(InstallationTitle); const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link'); - function createComponent() { + function createComponent(props = {}) { wrapper = mountExtended(PypiInstallation, { propsData: { - packageEntity, + packageEntity: { + ...packageEntity, + ...props, + }, }, stubs: { GlSprintf, @@ -44,10 +47,6 @@ password = <your personal access token>`; createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('install command switch', () => { it('has the installation title component', () => { expect(findInstallationTitle().exists()).toBe(true); @@ -86,6 +85,12 @@ password = <your personal access token>`; }); }); + it('does not have a link to personal access token docs when package is public', () => { + createComponent({ publicPackage: true }); + + expect(findAccessTokenLink().exists()).toBe(false); + }); + 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/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js index 67340822fa5..f7c8e909ff6 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js @@ -1,4 +1,4 @@ -import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; @@ -24,6 +24,7 @@ describe('VersionRow', () => { const findPackageName = () => wrapper.findComponent(GlTruncate); const findWarningIcon = () => wrapper.findComponent(GlIcon); const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); + const findDeleteDropdownItem = () => wrapper.findComponent(GlDropdownItem); function createComponent({ packageEntity = packageVersion, selected = false } = {}) { wrapper = shallowMountExtended(VersionRow, { @@ -36,15 +37,11 @@ describe('VersionRow', () => { GlTruncate, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); } - afterEach(() => { - wrapper.destroy(); - }); - it('has a link to the version detail', () => { createComponent(); @@ -112,6 +109,31 @@ describe('VersionRow', () => { }); }); + describe('delete button', () => { + it('does not exist when package cannot be destroyed', () => { + createComponent({ packageEntity: { ...packageVersion, canDestroy: false } }); + + expect(findDeleteDropdownItem().exists()).toBe(false); + }); + + it('exists and has the correct props', () => { + createComponent(); + + expect(findDeleteDropdownItem().exists()).toBe(true); + expect(findDeleteDropdownItem().attributes()).toMatchObject({ + variant: 'danger', + }); + }); + + it('emits the delete event when the delete button is clicked', () => { + createComponent(); + + findDeleteDropdownItem().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toHaveLength(1); + }); + }); + describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js index 689b53fa2a4..04546c4cea4 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js @@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert'; import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue'; import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; @@ -14,7 +14,7 @@ import { packagesListQuery, } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('DeletePackages', () => { let wrapper; @@ -67,10 +67,6 @@ describe('DeletePackages', () => { mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation()); }); - afterEach(() => { - wrapper.destroy(); - }); - it('binds deletePackages method to the default slot', () => { createComponent(); 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 2a78cfb13f9..91417d2fc9f 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 @@ -1,5 +1,5 @@ import { GlFormCheckbox, GlSprintf, GlTruncate } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; @@ -67,15 +67,11 @@ describe('packages_list_row', () => { selected, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { mountComponent(); expect(wrapper.element).toMatchSnapshot(); @@ -141,7 +137,6 @@ describe('packages_list_row', () => { findDeleteDropdown().vm.$emit('click'); - await nextTick(); expect(wrapper.emitted('delete')).toHaveLength(1); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js index 610640e0ca3..ae990f3ea00 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js @@ -73,10 +73,6 @@ describe('packages_list', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when is loading', () => { beforeEach(() => { mountComponent({ isLoading: true }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js index a884959ab62..1250ecaf61f 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js @@ -46,10 +46,6 @@ describe('Package Search', () => { extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock); }); - afterEach(() => { - wrapper.destroy(); - }); - it('has a registry search component', async () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js index b47515e15c3..1296458155a 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js @@ -20,11 +20,6 @@ describe('PackageTitle', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('title area', () => { it('exists', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js index fcbd7cc6a50..e9119b736c2 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js @@ -19,10 +19,6 @@ describe('publish_method', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders', () => { mountComponent(); expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js index 8f3c8667c47..c98f5f32344 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js @@ -19,11 +19,6 @@ describe('packages_filter', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('binds all of his attrs to filtered search token', () => { mountComponent({ attrs: { foo: 'bar' } }); 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 d897be1f344..19c098e1f82 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -147,6 +147,7 @@ export const packageData = (extend) => ({ conanUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/conan', pypiUrl: 'http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple', + publicPackage: false, pypiSetupUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/pypi', ...extend, }); 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 b494965a3cb..49f69a46395 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 @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue'; @@ -45,7 +45,7 @@ import { pagination, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); useMockLocationHelper(); describe('PackagesApp', () => { @@ -131,10 +131,6 @@ describe('PackagesApp', () => { const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1); const findDeletePackages = () => wrapper.findComponent(DeletePackages); - afterEach(() => { - wrapper.destroy(); - }); - it('renders an empty state component', async () => { createComponent({ resolver: jest.fn().mockResolvedValue(emptyPackageDetailsQuery) }); diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js index a2ec527ce12..60bb055b1db 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js @@ -4,14 +4,13 @@ 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 { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import ListPage from '~/packages_and_registries/package_registry/pages/list.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue'; import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue'; import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue'; import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue'; import { - PROJECT_RESOURCE_TYPE, - GROUP_RESOURCE_TYPE, GRAPHQL_PAGE_SIZE, EMPTY_LIST_HELP_URL, PACKAGE_HELP_URL, @@ -21,7 +20,7 @@ import getPackagesQuery from '~/packages_and_registries/package_registry/graphql import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; import { packagesListQuery, packageData, pagination } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('PackagesListApp', () => { let wrapper; @@ -78,10 +77,6 @@ describe('PackagesListApp', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const waitForFirstRequest = async () => { // emit a search update so the query is executed findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] }); @@ -171,14 +166,14 @@ describe('PackagesListApp', () => { }); describe.each` - type | sortType - ${PROJECT_RESOURCE_TYPE} | ${'sort'} - ${GROUP_RESOURCE_TYPE} | ${'groupSort'} + type | sortType + ${WORKSPACE_PROJECT} | ${'sort'} + ${WORKSPACE_GROUP} | ${'groupSort'} `('$type query', ({ type, sortType }) => { let provide; let resolver; - const isGroupPage = type === GROUP_RESOURCE_TYPE; + const isGroupPage = type === WORKSPACE_GROUP; beforeEach(() => { provide = { ...defaultProvide, isGroupPage }; @@ -198,9 +193,13 @@ describe('PackagesListApp', () => { }); }); - describe('empty state', () => { + describe.each` + description | resolverResponse + ${'empty response'} | ${packagesListQuery({ extend: { nodes: [] } })} + ${'error response'} | ${{ data: { group: null } }} + `(`$description renders empty state`, ({ resolverResponse }) => { beforeEach(() => { - const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } })); + const resolver = jest.fn().mockResolvedValue(resolverResponse); mountComponent({ resolver }); return waitForFirstRequest(); diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js index 796d89231f4..6dd4b9f2d20 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js @@ -28,7 +28,7 @@ import { dependencyProxyUpdateTllPolicyMutationMock, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'); describe('DependencyProxySettings', () => { @@ -82,10 +82,6 @@ describe('DependencyProxySettings', () => { .mockResolvedValue(dependencyProxyUpdateTllPolicyMutationMock()); }); - afterEach(() => { - wrapper.destroy(); - }); - const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle'); const findEnableTtlPoliciesToggle = () => diff --git a/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js index 86f14961690..461200e6983 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js @@ -23,10 +23,6 @@ describe('Exceptions Input', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findInputGroup = () => wrapper.findComponent(GlFormGroup); const findInput = () => wrapper.findComponent(GlFormInput); diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js index 7edc321867c..3ce8e91d43d 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js @@ -19,7 +19,7 @@ import { dependencyProxyImageTtlPolicy, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Group Settings App', () => { let wrapper; @@ -55,10 +55,6 @@ describe('Group Settings App', () => { show = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findAlert = () => wrapper.findComponent(GlAlert); const findPackageSettings = () => wrapper.findComponent(PackagesSettings); const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings); diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js index 807f332f4d3..22e42f8c0ab 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js @@ -23,7 +23,7 @@ import { groupPackageSettingsMutationErrorMock, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'); describe('Packages Settings', () => { @@ -56,10 +56,6 @@ describe('Packages Settings', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); const findDescription = () => wrapper.findByTestId('description'); const findMavenSettings = () => wrapper.findByTestId('maven-settings'); diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js index a0b257a9496..d57077b31c8 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js @@ -25,7 +25,7 @@ import { mavenProps, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'); describe('Packages Forwarding Settings', () => { diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js index 2bb99fb8e8f..49e8601da88 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js @@ -61,10 +61,6 @@ describe('Cleanup image tags project settings', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('isEdited status', () => { it.each` description | apiResponse | workingCopy | result diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js index cbb5aa52694..57b48407174 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js @@ -124,10 +124,6 @@ describe('Container Expiration Policy Settings Form', () => { jest.spyOn(Tracking, 'event'); }); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` model | finder | fieldName | type | defaultValue ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false} diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js index 43484d26d76..19f25d0aef7 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js @@ -63,10 +63,6 @@ describe('Container expiration policy project settings', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders the setting form', async () => { mountComponentWithApollo({ resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js index ae41fdf65e0..058fe427106 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js @@ -32,11 +32,6 @@ describe('ExpirationDropdown', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('structure', () => { it('has a form-select component', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js index 1cea0704154..be12d108d1e 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js @@ -38,11 +38,6 @@ describe('ExpirationInput', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('structure', () => { it('has a label', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js index 653f2a8b40e..f950a9d5add 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js @@ -23,11 +23,6 @@ describe('ExpirationToggle', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('structure', () => { it('has an input component', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js index 55a66cebd83..ec7b89aa927 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js @@ -23,11 +23,6 @@ describe('ExpirationToggle', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('structure', () => { it('has a toggle component', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js index 0fbbf4ae58f..b9c0c38bf9e 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js @@ -115,7 +115,6 @@ describe('Packages Cleanup Policy Settings Form', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js index 6dfeeca6862..94277d34f30 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js @@ -47,7 +47,6 @@ describe('Packages cleanup policy project settings', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js index 07d13839c61..54655acdf2a 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js @@ -20,11 +20,6 @@ describe('Registry Settings app', () => { const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy); const findAlert = () => wrapper.findComponent(GlAlert); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const defaultProvide = { showContainerRegistrySettings: true, showPackageRegistrySettings: true, diff --git a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js index 18084766db9..41482e6e681 100644 --- a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js @@ -38,11 +38,6 @@ describe('cli_commands', () => { mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('shows the correct text on the button', () => { expect(findDropdownButton().text()).toContain(QUICK_START); }); diff --git a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js index 357dab593e8..ba5ba8f9884 100644 --- a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js @@ -19,11 +19,6 @@ describe('DeletePackageModal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when itemToBeDeleted prop is defined', () => { beforeEach(() => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js index 93425d4f399..2490e9a1f6a 100644 --- a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js @@ -9,7 +9,7 @@ describe('PackagePath', () => { wrapper = shallowMount(PackagePath, { propsData, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -24,11 +24,6 @@ describe('PackagePath', () => { const findItem = (name) => wrapper.find(`[data-testid="${name}"]`); const findTooltip = (w) => getBinding(w.element, 'gl-tooltip'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` path | rootUrl | shouldExist | shouldNotExist ${'foo/bar'} | ${'/foo/bar'} | ${[]} | ${[ROOT_CHEVRON, ELLIPSIS_ICON, ELLIPSIS_CHEVRON, LEAF_LINK]} diff --git a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js index 0005162e0bb..e43a9f57255 100644 --- a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js @@ -17,11 +17,6 @@ describe('PackagesListLoader', () => { beforeEach(createComponent); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('desktop loader', () => { it('produces the right loader', () => { expect(findDesktopShapes().findAll('rect[width="1000"]')).toHaveLength(20); diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js index db9f96bff39..1484377a475 100644 --- a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js @@ -43,10 +43,6 @@ describe('Persisted Search', () => { extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock); }); - afterEach(() => { - wrapper.destroy(); - }); - it('has a registry search component', async () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js index fa8f8f7641a..167599a54ea 100644 --- a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js @@ -20,11 +20,6 @@ describe('publish_method', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders', () => { mountComponent(packageWithPipeline); expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js index 15db454ac68..c1f1a25d53b 100644 --- a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js @@ -31,10 +31,6 @@ describe('Registry Breadcrumb', () => { nameGenerator.mockClear(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when is rootRoute', () => { beforeEach(() => { mountComponent(routes[0]); diff --git a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js index 2e2d5e26d33..a4e0d267023 100644 --- a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js @@ -36,10 +36,6 @@ describe('Registry List', () => { const findScopedSlotFirstValue = (index) => findScopedSlots().at(index).find('span'); const findScopedSlotIsSelectedValue = (index) => findScopedSlots().at(index).find('p'); - afterEach(() => { - wrapper.destroy(); - }); - describe('header', () => { it('renders the title passed in the prop', () => { mountComponent(); @@ -111,10 +107,21 @@ describe('Registry List', () => { expect(findDeleteSelected().text()).toBe(component.i18n.deleteSelected); }); - it('is hidden when hiddenDelete is true', () => { - mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } }); + describe('when hiddenDelete is true', () => { + beforeEach(() => { + mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } }); + }); - expect(findDeleteSelected().exists()).toBe(false); + it('is hidden', () => { + expect(findDeleteSelected().exists()).toBe(false); + }); + + it('populates the first slot prop correctly', async () => { + expect(findScopedSlots().at(0).exists()).toBe(true); + + // it's the first slot + expect(findScopedSlotFirstValue(0).text()).toBe('false'); + }); }); it('is disabled when isLoading is true', () => { diff --git a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js index a4c1b989dac..664a821c275 100644 --- a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js @@ -15,10 +15,6 @@ describe('SettingsBlock', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDefaultSlot = () => wrapper.findByTestId('default-slot'); const findTitleSlot = () => wrapper.findByTestId('title-slot'); const findDescriptionSlot = () => wrapper.findByTestId('description-slot'); diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js index ec6369e7119..de6d44eabdc 100644 --- a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js +++ b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js @@ -19,8 +19,8 @@ describe('CancelJobs component', () => { const createComponent = (props = {}) => { wrapper = shallowMountExtended(CancelJobs, { directives: { - GlModal: createMockDirective(), - GlTooltip: createMockDirective(), + GlModal: createMockDirective('gl-modal'), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`, diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js index ab483316086..7ccded9b0f1 100644 --- a/spec/frontend/pages/groups/new/components/app_spec.js +++ b/spec/frontend/pages/groups/new/components/app_spec.js @@ -6,7 +6,7 @@ describe('App component', () => { let wrapper; const createComponent = (propsData = {}) => { - wrapper = shallowMount(App, { propsData }); + wrapper = shallowMount(App, { propsData: { groupsUrl: '/dashboard/groups', ...propsData } }); }; const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage); @@ -16,24 +16,31 @@ describe('App component', () => { .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(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([ + { href: '/dashboard/groups', text: 'Groups' }, + { href: '#', text: 'New group' }, + ]); expect(findCreateGroupPanel().title).toBe('Create group'); }); it('creates correct component for subgroup creation', () => { - const props = { parentGroupName: 'parent', importExistingGroupPath: '/path' }; + const detailProps = { + parentGroupName: 'parent', + importExistingGroupPath: '/path', + }; + + const props = { ...detailProps, parentGroupUrl: '/parent' }; createComponent(props); - expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('parent'); + expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([ + { href: '/parent', text: 'parent' }, + { href: '#', text: 'New subgroup' }, + ]); expect(findCreateGroupPanel().title).toBe('Create subgroup'); - expect(findCreateGroupPanel().detailProps).toEqual(props); + expect(findCreateGroupPanel().detailProps).toEqual(detailProps); }); }); 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 index 56a1fd03f71..35015d84085 100644 --- 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 @@ -15,10 +15,6 @@ describe('CreateGroupDescriptionDetails component', () => { const findLinkHref = (at) => wrapper.findAllComponents(GlLink).at(at); - afterEach(() => { - wrapper.destroy(); - }); - it('creates correct component for group creation', () => { createComponent(); diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js index da3954b4918..477511cde64 100644 --- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js +++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js @@ -68,15 +68,10 @@ describe('BulkImportsHistoryApp', () => { const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - const originalApiVersion = gon.api_version; - beforeAll(() => { + beforeEach(() => { gon.api_version = 'v4'; }); - afterAll(() => { - gon.api_version = originalApiVersion; - }); - beforeEach(() => { mock = new MockAdapter(axios); mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS); @@ -84,7 +79,6 @@ describe('BulkImportsHistoryApp', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('general behavior', () => { diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js index 628ee8d7999..239826c1458 100644 --- a/spec/frontend/pages/import/history/components/import_error_details_spec.js +++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js @@ -21,22 +21,13 @@ describe('ImportErrorDetails', () => { }); } - const originalApiVersion = gon.api_version; - beforeAll(() => { - gon.api_version = 'v4'; - }); - - afterAll(() => { - gon.api_version = originalApiVersion; - }); - beforeEach(() => { + gon.api_version = 'v4'; mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('general behavior', () => { diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js index 7d79583be19..43cbac25fe8 100644 --- a/spec/frontend/pages/import/history/components/import_history_app_spec.js +++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js @@ -59,23 +59,14 @@ describe('ImportHistoryApp', () => { }); } - const originalApiVersion = gon.api_version; - beforeAll(() => { + beforeEach(() => { gon.api_version = 'v4'; gon.features = { fullPathProjectSearch: true }; - }); - - afterAll(() => { - gon.api_version = originalApiVersion; - }); - - beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('general behavior', () => { diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js index c30b996437d..18a0098a715 100644 --- a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js +++ b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js @@ -25,8 +25,7 @@ describe('Password prompt modal', () => { const findField = () => wrapper.findByTestId('password-prompt-field'); const findModal = () => wrapper.findComponent(GlModal); const findConfirmBtn = () => findModal().props('actionPrimary'); - const findConfirmBtnDisabledState = () => - findModal().props('actionPrimary').attributes[2].disabled; + const findConfirmBtnDisabledState = () => findModal().props('actionPrimary').attributes.disabled; const findCancelBtn = () => findModal().props('actionCancel'); @@ -41,10 +40,6 @@ describe('Password prompt modal', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the password field', () => { expect(findField().exists()).toBe(true); }); diff --git a/spec/frontend/pages/projects/forks/new/components/app_spec.js b/spec/frontend/pages/projects/forks/new/components/app_spec.js index 0342b94a44d..e9a94878867 100644 --- a/spec/frontend/pages/projects/forks/new/components/app_spec.js +++ b/spec/frontend/pages/projects/forks/new/components/app_spec.js @@ -22,10 +22,6 @@ describe('App component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays the correct svg illustration', () => { expect(wrapper.find('img').attributes('src')).toBe('illustrations/project-create-new-sm.svg'); }); diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js index f0593a854b2..9dce6fde6f6 100644 --- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js +++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js @@ -6,7 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import { kebabCase } from 'lodash'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import * as urlUtility from '~/lib/utils/url_utility'; import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -14,7 +14,7 @@ import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_name import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue'; import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); describe('ForkForm component', () => { @@ -111,7 +111,6 @@ describe('ForkForm component', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); @@ -553,7 +552,7 @@ describe('ForkForm component', () => { expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl); }); - it('display flash when POST is unsuccessful', async () => { + it('displays an alert when POST is unsuccessful', async () => { const dummyError = 'Fork project failed'; jest.spyOn(axios, 'post').mockRejectedValue(dummyError); diff --git a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js index 82f451ed6ef..af578b69a81 100644 --- a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js +++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js @@ -3,15 +3,14 @@ import { mount, shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql'; import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('ProjectNamespace component', () => { let wrapper; - let originalGon; const data = { project: { @@ -85,14 +84,8 @@ describe('ProjectNamespace component', () => { findListBox().vm.$emit('shown'); }; - beforeAll(() => { - originalGon = window.gon; - window.gon = { gitlab_url: gitlabUrl }; - }); - - afterAll(() => { - window.gon = originalGon; - wrapper.destroy(); + beforeEach(() => { + gon.gitlab_url = gitlabUrl; }); describe('Initial state', () => { @@ -152,7 +145,7 @@ describe('ProjectNamespace component', () => { await nextTick(); }); - it('creates a flash message and captures the error', () => { + it('creates an alert message and captures the error', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'Something went wrong while loading data. Please refresh the page to try again.', captureError: true, diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js index 5356953060a..882730d90ae 100644 --- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js +++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlListbox, GlListboxItem } from '@gitlab/ui'; +import { GlAlert, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; @@ -22,7 +22,7 @@ describe('Code Coverage', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findAreaChart = () => wrapper.findComponent(GlAreaChart); - const findListBox = () => wrapper.findComponent(GlListbox); + const findListBox = () => wrapper.findComponent(GlCollapsibleListbox); const findListBoxItems = () => wrapper.findAllComponents(GlListboxItem); const findFirstListBoxItem = () => findListBoxItems().at(0); const findSecondListBoxItem = () => findListBoxItems().at(1); @@ -37,15 +37,10 @@ describe('Code Coverage', () => { graphRef, graphCsvPath, }, - stubs: { GlListbox }, + stubs: { GlCollapsibleListbox }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when fetching data is successful', () => { beforeEach(() => { mockAxios = new MockAdapter(axios); diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js index 2d3b9afa8f6..07d05293a3c 100644 --- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js +++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js @@ -37,10 +37,6 @@ describe('Interval Pattern Input Component', () => { const selectCustomRadio = () => findCustomRadio().setChecked(true); const createWrapper = (props = {}, data = {}) => { - if (wrapper) { - throw new Error('A wrapper already exists'); - } - wrapper = mount(IntervalPatternInput, { propsData: { ...props }, data() { @@ -64,8 +60,6 @@ describe('Interval Pattern Input Component', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; window.gl = oldWindowGl; }); diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js index a633332ab65..e20c2fa47a7 100644 --- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js +++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js @@ -31,10 +31,6 @@ describe('Pipeline Schedule Callout', () => { await nextTick(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('does not render the callout', () => { expect(findInnerContentOfCallout().exists()).toBe(false); }); @@ -46,10 +42,6 @@ describe('Pipeline Schedule Callout', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the callout container', () => { expect(findInnerContentOfCallout().exists()).toBe(true); }); diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js index 5771e1b88e8..03c65ab4c9c 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js @@ -31,11 +31,6 @@ describe('Project Feature Settings', () => { }, }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('Hidden name input', () => { it('should set the hidden name input if the name exists', () => { wrapper = mountComponent(); diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js index 6230809a6aa..91d3057aec5 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js @@ -15,10 +15,6 @@ describe('Project Setting Row', () => { wrapper = mountComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should show the label if it is set', async () => { wrapper.setProps({ label: 'Test label' }); 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 ff20b72c72c..0812be9745e 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 @@ -140,11 +140,6 @@ describe('Settings Panel', () => { const findMonitorVisibilityInput = () => findMonitorSettings().findComponent(ProjectFeatureSetting); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('Project Visibility', () => { it('should set the project visibility help path', () => { wrapper = mountComponent(); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js index 6a18473b1a7..1858a56b0e1 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js @@ -15,11 +15,6 @@ describe('WikiAlert', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlLink = () => wrapper.findComponent(GlLink); const findGlSprintf = () => wrapper.findComponent(GlSprintf); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js index c8e9a31b526..8e26453b564 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js @@ -31,11 +31,6 @@ describe('pages/shared/wikis/components/wiki_content', () => { mock = new MockAdapter(axios); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findContent = () => wrapper.find('[data-testid="wiki-page-content"]'); 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 ffcfd1d9f78..1be4a974f7a 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -103,8 +103,6 @@ describe('WikiForm', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); - wrapper = null; }); it('displays markdown editor', () => { @@ -116,7 +114,6 @@ describe('WikiForm', () => { expect.objectContaining({ value: pageInfoPersisted.content, renderMarkdownPath: pageInfoPersisted.markdownPreviewPath, - markdownDocsPath: pageInfoPersisted.markdownHelpPath, uploadsPath: pageInfoPersisted.uploadsPath, autofocus: pageInfoPersisted.persisted, }), @@ -126,6 +123,10 @@ describe('WikiForm', () => { id: 'wiki_content', name: 'wiki[content]', }); + + expect(markdownEditor.vm.$attrs['markdown-docs-path']).toEqual( + pageInfoPersisted.markdownHelpPath, + ); }); it.each` @@ -172,7 +173,7 @@ describe('WikiForm', () => { nextTick(); - expect(findMarkdownEditor().props('enablePreview')).toBe(enabled); + expect(findMarkdownEditor().vm.$attrs['enable-preview']).toBe(enabled); }); it.each` diff --git a/spec/frontend/pdf/index_spec.js b/spec/frontend/pdf/index_spec.js index 98946412264..23477c73ba0 100644 --- a/spec/frontend/pdf/index_spec.js +++ b/spec/frontend/pdf/index_spec.js @@ -7,10 +7,6 @@ describe('PDFLab component', () => { const mountComponent = ({ pdf }) => shallowMount(PDFLab, { propsData: { pdf } }); - afterEach(() => { - wrapper.destroy(); - }); - describe('without PDF data', () => { beforeEach(() => { wrapper = mountComponent({ pdf: '' }); diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js index 4cf83a3252d..1d5c5cd98c4 100644 --- a/spec/frontend/pdf/page_spec.js +++ b/spec/frontend/pdf/page_spec.js @@ -9,10 +9,6 @@ jest.mock('pdfjs-dist/webpack', () => { describe('Page component', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - it('renders the page when mounting', async () => { const testPage = { render: jest.fn().mockReturnValue({ promise: Promise.resolve() }), diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js index 5460feb66fe..de9cc1e8008 100644 --- a/spec/frontend/performance_bar/components/add_request_spec.js +++ b/spec/frontend/performance_bar/components/add_request_spec.js @@ -13,10 +13,6 @@ describe('add request form', () => { wrapper = mount(AddRequest); }); - afterEach(() => { - wrapper.destroy(); - }); - it('hides the input on load', () => { expect(findGlFormInput().exists()).toBe(false); }); diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js index 5ab2c9abe5d..4194639fffe 100644 --- a/spec/frontend/performance_bar/components/detailed_metric_spec.js +++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js @@ -38,10 +38,6 @@ describe('detailedMetric', () => { const findAllSummaryItems = () => wrapper.findAllByTestId('performance-bar-summary-item').wrappers.map((w) => w.text()); - afterEach(() => { - wrapper.destroy(); - }); - describe('when the current request has no details', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js index 9dd8ea9f933..7b6d8ff695d 100644 --- a/spec/frontend/performance_bar/components/request_warning_spec.js +++ b/spec/frontend/performance_bar/components/request_warning_spec.js @@ -5,10 +5,6 @@ describe('request warning', () => { let wrapper; const htmlId = 'request-123'; - afterEach(() => { - wrapper.destroy(); - }); - describe('when the request has warnings', () => { beforeEach(() => { wrapper = shallowMount(RequestWarning, { diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js index 6519989661f..376575a8acb 100644 --- a/spec/frontend/persistent_user_callout_spec.js +++ b/spec/frontend/persistent_user_callout_spec.js @@ -1,12 +1,12 @@ import MockAdapter from 'axios-mock-adapter'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import PersistentUserCallout from '~/persistent_user_callout'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('PersistentUserCallout', () => { const dismissEndpoint = '/dismiss'; diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js index fa30b9c2b97..8f44a6c085b 100644 --- a/spec/frontend/pipeline_wizard/components/commit_spec.js +++ b/spec/frontend/pipeline_wizard/components/commit_spec.js @@ -74,10 +74,6 @@ describe('Pipeline Wizard - Commit Page', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('shows a commit message input with the correct label', () => { expect(wrapper.findByTestId('commit_message').exists()).toBe(true); expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel); @@ -121,10 +117,6 @@ describe('Pipeline Wizard - Commit Page', () => { expect(wrapper.findByTestId('load-error').exists()).toBe(true); expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError); }); - - afterEach(() => { - wrapper.destroy(); - }); }); describe('commit result handling', () => { @@ -151,7 +143,6 @@ describe('Pipeline Wizard - Commit Page', () => { }); afterEach(() => { - wrapper.destroy(); jest.clearAllMocks(); }); }); @@ -178,7 +169,6 @@ describe('Pipeline Wizard - Commit Page', () => { }); afterEach(() => { - wrapper.destroy(); jest.clearAllMocks(); }); }); @@ -246,10 +236,6 @@ describe('Pipeline Wizard - Commit Page', () => { await waitForPromises(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('sets up without error', async () => { expect(consoleSpy).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js index dd0a609043a..6d7d4363189 100644 --- a/spec/frontend/pipeline_wizard/components/editor_spec.js +++ b/spec/frontend/pipeline_wizard/components/editor_spec.js @@ -9,10 +9,6 @@ describe('Pages Yaml Editor wrapper', () => { propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' }, }; - afterEach(() => { - wrapper.destroy(); - }); - describe('mount hook', () => { beforeEach(() => { wrapper = mount(YamlEditor, defaultOptions); diff --git a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js index f288264a11e..7f521e2523e 100644 --- a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js +++ b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js @@ -33,10 +33,6 @@ describe('Pipeline Wizard -- Input Wrapper', () => { inputChild = wrapper.findComponent(TextWidget); }); - afterEach(() => { - wrapper.destroy(); - }); - it('will replace its value in compiled', async () => { await inputChild.vm.$emit('input', inputValue); const expected = new Document({ @@ -54,10 +50,6 @@ describe('Pipeline Wizard -- Input Wrapper', () => { }); describe('Target Path Discovery', () => { - afterEach(() => { - wrapper.destroy(); - }); - it.each` scenario | template | target | expected ${'simple nested object'} | ${{ foo: { bar: { baz: '$BOO' } } }} | ${'$BOO'} | ${['foo', 'bar', 'baz']} diff --git a/spec/frontend/pipeline_wizard/components/step_nav_spec.js b/spec/frontend/pipeline_wizard/components/step_nav_spec.js index c6eac1386fa..8e2f0ab0281 100644 --- a/spec/frontend/pipeline_wizard/components/step_nav_spec.js +++ b/spec/frontend/pipeline_wizard/components/step_nav_spec.js @@ -19,10 +19,6 @@ describe('Pipeline Wizard - Step Navigation Component', () => { nextButton = wrapper.findByTestId('next-button'); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` scenario | showBackButton | showNextButton ${'does not show prev button'} | ${false} | ${false} diff --git a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js index b8e194015b0..52e5d49ec99 100644 --- a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js +++ b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js @@ -39,10 +39,6 @@ describe('Pipeline Wizard - Checklist Widget', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('creates the component', () => { createComponent(); expect(wrapper.exists()).toBe(true); diff --git a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js index c9e9f5caebe..b0eb7279a94 100644 --- a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js +++ b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js @@ -39,10 +39,6 @@ describe('Pipeline Wizard - List Widget', () => { }; describe('component setup and interface', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('prints the label inside the legend', () => { createComponent(); @@ -168,10 +164,6 @@ describe('Pipeline Wizard - List Widget', () => { }); describe('form validation', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('does not show validation state when untouched', async () => { createComponent({}, mountExtended); expect(findGlFormGroup().classes()).not.toContain('is-valid'); diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js index 33c6394eb41..1056602c912 100644 --- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js +++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js @@ -48,10 +48,6 @@ describe('Pipeline Wizard - wrapper.vue', () => { wrapper.find(`[data-input-target="${target}"]`).find('input'); describe('display', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('shows the steps', () => { createComponent(); @@ -145,10 +141,6 @@ describe('Pipeline Wizard - wrapper.vue', () => { } }); - afterEach(() => { - wrapper.destroy(); - }); - if (expectCommitStepShown) { it('does not show the step wrapper', async () => { expect(wrapper.findComponent(WizardStep).isVisible()).toBe(false); @@ -188,10 +180,6 @@ describe('Pipeline Wizard - wrapper.vue', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('initially shows a placeholder', async () => { const editorContent = getEditorContent(); @@ -240,10 +228,6 @@ describe('Pipeline Wizard - wrapper.vue', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('highlight requests by the step get passed on to the editor', async () => { const highlight = 'foo'; @@ -309,7 +293,6 @@ describe('Pipeline Wizard - wrapper.vue', () => { }); afterEach(() => { - wrapper.destroy(); inputField = undefined; }); @@ -331,10 +314,6 @@ describe('Pipeline Wizard - wrapper.vue', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('emits done', () => { expect(wrapper.emitted('done')).toBeUndefined(); diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js index 13234525159..e7bd7f686b6 100644 --- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js +++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js @@ -24,10 +24,6 @@ describe('PipelineWizard', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('mounts without error', () => { const consoleSpy = jest.spyOn(console, 'error'); diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js index 28a08b6da0f..aecaa640266 100644 --- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js @@ -28,11 +28,6 @@ describe('The DAG annotations', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when there is one annotation', () => { const currentNote = singleNote['dag-link103']; diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js index 4619548d1bb..6b46be3dd49 100644 --- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js @@ -36,11 +36,6 @@ describe('The DAG graph', () => { createComponent({ graphData: parsedData }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('in the basic case', () => { beforeEach(() => { /* diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js index b0c26976c85..e2dc8120309 100644 --- a/spec/frontend/pipelines/components/dag/dag_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_spec.js @@ -51,11 +51,6 @@ describe('Pipeline DAG graph wrapper', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when a query argument is undefined', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js index d1da7cb3acf..169e3666cbd 100644 --- a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js +++ b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue'; import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue'; import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql'; @@ -12,7 +12,7 @@ import { mockFailedJobsQueryResponse, mockFailedJobsSummaryData } from '../../mo Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Failed Jobs App', () => { let wrapper; @@ -44,10 +44,6 @@ describe('Failed Jobs App', () => { resolverSpy = jest.fn().mockResolvedValue(mockFailedJobsQueryResponse); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('loading spinner', () => { it('displays loading spinner when fetching failed jobs', () => { createComponent(resolverSpy); diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js index 0df15afd70d..0ac3b6c9074 100644 --- a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js +++ b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue'; import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql'; @@ -15,7 +15,7 @@ import { mockPreparedFailedJobsDataNoPermission, } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); Vue.use(VueApollo); @@ -45,10 +45,6 @@ describe('Failed Jobs Table', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('displays the failed jobs table', () => { createComponent(); diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js index 9bc14266593..52df7b4500b 100644 --- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js +++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import JobsApp from '~/pipelines/components/jobs/jobs_app.vue'; import JobsTable from '~/jobs/components/table/jobs_table.vue'; import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql'; @@ -12,7 +12,7 @@ import { mockPipelineJobsQueryResponse } from '../../mock_data'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Jobs app', () => { let wrapper; @@ -45,10 +45,6 @@ describe('Jobs app', () => { resolverSpy = jest.fn().mockResolvedValue(mockPipelineJobsQueryResponse); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('loading spinner', () => { const setup = async () => { createComponent(resolverSpy); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js index 5ea57c51e70..a4ecb9041c9 100644 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js @@ -19,7 +19,7 @@ describe('Linked pipeline mini list', () => { const createComponent = (props = {}) => { wrapper = mount(LinkedPipelinesMiniList, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { ...props, @@ -34,11 +34,6 @@ describe('Linked pipeline mini list', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render one linked pipeline item', () => { expect(findLinkedPipelineMiniItem().exists()).toBe(true); }); @@ -102,11 +97,6 @@ describe('Linked pipeline mini list', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render three linked pipeline items', () => { expect(findLinkedPipelineMiniItems().exists()).toBe(true); expect(findLinkedPipelineMiniItems().length).toBe(3); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js index 036b82530d5..e7415a6c596 100644 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js @@ -33,11 +33,6 @@ describe('Pipeline Mini Graph', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render the pipeline stages', () => { expect(findPipelineStages().exists()).toBe(true); }); @@ -71,11 +66,6 @@ describe('Pipeline Mini Graph', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should have the correct props', () => { expect(findPipelineMiniGraph().props()).toMatchObject({ downstreamPipelines: [], @@ -118,11 +108,6 @@ describe('Pipeline Mini Graph', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render the downstream linked pipelines mini list only', () => { expect(findLinkedPipelineDownstream().exists()).toBe(true); expect(findLinkedPipelineUpstream().exists()).toBe(false); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js index ab2056b4035..864f2d66f60 100644 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js @@ -45,11 +45,10 @@ describe('Pipelines stage component', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; - eventHub.$emit.mockRestore(); mock.restore(); + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy + wrapper.destroy(); }); const findCiActionBtn = () => wrapper.find('.js-ci-action'); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js index c123f53886e..73e810bde99 100644 --- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js @@ -60,9 +60,4 @@ describe('Pipeline Stages', () => { expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(true); expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(true); }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); }); diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js index c2cb95d4320..337af6c1f60 100644 --- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js +++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js @@ -39,10 +39,6 @@ describe('The Pipeline Tabs', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Tabs', () => { it.each` tabName | tabComponent diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js index ba7262353f0..51a4487a3ef 100644 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js @@ -51,8 +51,6 @@ describe('Pipelines filtered search', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); - wrapper = null; }); it('displays UI elements', () => { diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js index 6531a15ab8e..b560eea4882 100644 --- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js @@ -29,11 +29,6 @@ describe('CI Templates', () => { const findTemplateName = () => wrapper.findByTestId('template-name'); const findTemplateLogo = () => wrapper.findByTestId('template-logo'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('renders template list', () => { beforeEach(() => { createWrapper(); diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js index 0c2938921d6..700be076e0c 100644 --- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js @@ -37,11 +37,6 @@ describe('iOS Templates', () => { const findSetupRunnerLink = () => wrapper.findByText('Set up a runner'); const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when ios runners are not available', () => { beforeEach(() => { wrapper = createWrapper({ iosRunnersAvailable: false }); diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js index f255e0d857f..0f4a2b1d02f 100644 --- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js @@ -42,11 +42,6 @@ describe('Pipelines CI Templates', () => { const findDocumentationLink = () => wrapper.findByTestId('documentation-link'); const findSettingsButton = () => wrapper.findByTestId('settings-button'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('renders test template', () => { beforeEach(() => { wrapper = createWrapper(); diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js index 0abf7f59717..5465e4d77da 100644 --- a/spec/frontend/pipelines/empty_state_spec.js +++ b/spec/frontend/pipelines/empty_state_spec.js @@ -35,11 +35,6 @@ describe('Pipelines Empty State', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when user can configure CI', () => { describe('when the ios_specific_templates experiment is active', () => { beforeEach(() => { diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index e3eea503b46..890255f225e 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -33,7 +33,6 @@ describe('pipeline graph action component', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('render', () => { diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js index 99bccd21656..42e47a23db8 100644 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -101,10 +101,6 @@ describe('Pipeline graph wrapper', () => { createComponent({ apolloProvider, data, provide, mountFn }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when data is loading', () => { it('displays the loading icon', () => { createComponentWithApollo(); diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js index 43587bebedf..78265165d1f 100644 --- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js +++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js @@ -42,10 +42,6 @@ describe('the graph view selector component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when showing stage view', () => { beforeEach(() => { createComponent({ mountFn: mount }); diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js index d8afb33e148..1419a7b9982 100644 --- a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js +++ b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js @@ -69,10 +69,6 @@ describe('job group dropdown component', () => { wrapper = mountFn(JobGroupDropdown, { propsData: { group } }); }; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { createComponent({ mountFn: mount }); }); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 3224c87ab6b..5cc2c76f3dd 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -1,10 +1,11 @@ import MockAdapter from 'axios-mock-adapter'; -import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { GlBadge, GlModal } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import { GlBadge, GlModal, GlToast } from '@gitlab/ui'; import JobItem from '~/pipelines/components/graph/job_item.vue'; import axios from '~/lib/utils/axios_utils'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { @@ -19,12 +20,14 @@ import { describe('pipeline graph job item', () => { useLocalStorageSpy(); + Vue.use(GlToast); let wrapper; let mockAxios; const findJobWithoutLink = () => wrapper.findByTestId('job-without-link'); const findJobWithLink = () => wrapper.findByTestId('job-with-link'); + const findActionVueComponent = () => wrapper.findComponent(ActionComponent); const findActionComponent = () => wrapper.findByTestId('ci-action-component'); const findBadge = () => wrapper.findComponent(GlBadge); const findJobLink = () => wrapper.findByTestId('job-with-link'); @@ -41,9 +44,9 @@ describe('pipeline graph job item', () => { job: mockJob, }; - const createWrapper = ({ props, data } = {}) => { + const createWrapper = ({ props, data, mountFn = mount, mocks = {} } = {}) => { wrapper = extendedWrapper( - mount(JobItem, { + mountFn(JobItem, { data() { return { ...data, @@ -53,6 +56,9 @@ describe('pipeline graph job item', () => { ...defaultProps, ...props, }, + mocks: { + ...mocks, + }, }), ); }; @@ -238,6 +244,37 @@ describe('pipeline graph job item', () => { }); }); + describe('when retrying', () => { + const mockToastShow = jest.fn(); + + beforeEach(async () => { + createWrapper({ + mountFn: shallowMount, + data: { + currentSkipModalValue: true, + }, + props: { + skipRetryModal: true, + job: triggerJobWithRetryAction, + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, + }); + + jest.spyOn(wrapper.vm.$toast, 'show'); + + await findActionVueComponent().vm.$emit('pipelineActionRequestComplete'); + await nextTick(); + }); + + it('shows a toast message that the downstream is being created', () => { + expect(mockToastShow).toHaveBeenCalledTimes(1); + }); + }); + describe('highlighting', () => { it.each` job | jobName | expanded | link diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index f396fe2aff4..b5ef10dee12 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -58,10 +58,6 @@ describe('Linked pipeline', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('rendered output', () => { const props = { pipeline: mockPipeline, diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js index 63e2d8707ea..6e4b9498918 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -65,10 +65,6 @@ describe('Linked Pipelines Column', () => { createComponent({ apolloProvider, mountFn, props }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('it renders correctly', () => { beforeEach(() => { createComponentWithApollo(); diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js index 19f597a7267..d4d7f1618c5 100644 --- a/spec/frontend/pipelines/graph/stage_column_component_spec.js +++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js @@ -54,10 +54,6 @@ describe('stage column component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when mounted', () => { beforeEach(() => { createComponent({ method: mount }); diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js index 2c6d126e12c..50f754393fe 100644 --- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js +++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js @@ -81,7 +81,6 @@ describe('Links Inner component', () => { afterEach(() => { jest.restoreAllMocks(); - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js index e2699d6ff2e..9d39c86ed5e 100644 --- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js +++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js @@ -35,10 +35,6 @@ describe('links layer component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with show links off', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js index e583c0798f5..a4d7d0e30f8 100644 --- a/spec/frontend/pipelines/header_component_spec.js +++ b/spec/frontend/pipelines/header_component_spec.js @@ -71,11 +71,6 @@ describe('Pipeline details header', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('initial loading', () => { beforeEach(() => { wrapper = createComponent(null, { isLoading: true }); diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js index 2c4740df174..15de7dc51f1 100644 --- a/spec/frontend/pipelines/nav_controls_spec.js +++ b/spec/frontend/pipelines/nav_controls_spec.js @@ -1,23 +1,20 @@ -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; describe('Pipelines Nav Controls', () => { let wrapper; const createComponent = (props) => { - wrapper = shallowMount(NavControls, { + wrapper = shallowMountExtended(NavControls, { propsData: { ...props, }, }); }; - const findRunPipeline = () => wrapper.find('.js-run-pipeline'); - - afterEach(() => { - wrapper.destroy(); - }); + const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button'); + const findCiLintButton = () => wrapper.findByTestId('ci-lint-button'); + const findClearCacheButton = () => wrapper.findByTestId('clear-cache-button'); it('should render link to create a new pipeline', () => { const mockData = { @@ -28,9 +25,9 @@ describe('Pipelines Nav Controls', () => { createComponent(mockData); - const runPipeline = findRunPipeline(); - expect(runPipeline.text()).toContain('Run pipeline'); - expect(runPipeline.attributes('href')).toBe(mockData.newPipelinePath); + const runPipelineButton = findRunPipelineButton(); + expect(runPipelineButton.text()).toContain('Run pipeline'); + expect(runPipelineButton.attributes('href')).toBe(mockData.newPipelinePath); }); it('should not render link to create pipeline if no path is provided', () => { @@ -42,7 +39,7 @@ describe('Pipelines Nav Controls', () => { createComponent(mockData); - expect(findRunPipeline().exists()).toBe(false); + expect(findRunPipelineButton().exists()).toBe(false); }); it('should render link for CI lint', () => { @@ -54,9 +51,10 @@ describe('Pipelines Nav Controls', () => { }; createComponent(mockData); + const ciLintButton = findCiLintButton(); - expect(wrapper.find('.js-ci-lint').text().trim()).toContain('CI lint'); - expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(mockData.ciLintPath); + expect(ciLintButton.text()).toContain('CI lint'); + expect(ciLintButton.attributes('href')).toBe(mockData.ciLintPath); }); describe('Reset Runners Cache', () => { @@ -70,16 +68,13 @@ describe('Pipelines Nav Controls', () => { }); it('should render button for resetting runner caches', () => { - expect(wrapper.find('.js-clear-cache').text().trim()).toContain('Clear runner caches'); + expect(findClearCacheButton().text()).toContain('Clear runner caches'); }); - it('should emit postAction event when reset runner cache button is clicked', async () => { - jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); - - wrapper.find('.js-clear-cache').vm.$emit('click'); - await nextTick(); + it('should emit postAction event when reset runner cache button is clicked', () => { + findClearCacheButton().vm.$emit('click'); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo'); + expect(wrapper.emitted('resetRunnersCache')).toEqual([['foo']]); }); }); }); diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js index df10742fd93..123f2e011c3 100644 --- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js +++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js @@ -39,10 +39,6 @@ describe('pipeline graph component', () => { const findLinksLayer = () => wrapper.findComponent(LinksLayer); const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('with `VALID` status', () => { beforeEach(() => { wrapper = createComponent({ diff --git a/spec/frontend/pipelines/pipeline_labels_spec.js b/spec/frontend/pipelines/pipeline_labels_spec.js index ca0229b1cbe..6a37e36352b 100644 --- a/spec/frontend/pipelines/pipeline_labels_spec.js +++ b/spec/frontend/pipelines/pipeline_labels_spec.js @@ -30,10 +30,6 @@ describe('Pipeline label component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should not render tags when flags are not set', () => { createComponent(); diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js index bedde71c48d..e3c9983aa52 100644 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js @@ -67,8 +67,6 @@ describe('Pipeline Multi Actions Dropdown', () => { afterEach(() => { mockAxios.restore(); - - wrapper.destroy(); }); it('should render the dropdown', () => { diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js index 58bfb68e85c..856c0484075 100644 --- a/spec/frontend/pipelines/pipeline_triggerer_spec.js +++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js @@ -22,15 +22,11 @@ describe('Pipelines Triggerer', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); const findAvatar = () => wrapper.findComponent(GlAvatar); const findTriggerer = () => wrapper.findByText('API'); diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index c62898f0c83..f00ee4a6367 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -35,10 +35,6 @@ describe('Pipeline Url Component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should render pipeline url table cell', () => { createComponent(); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index e034d52a33c..2db9f5c2a83 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -5,7 +5,7 @@ import { nextTick } from 'vue'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; @@ -13,7 +13,7 @@ import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipeli import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import { TRACKING_CATEGORIES } from '~/pipelines/constants'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); describe('Pipelines Actions dropdown', () => { @@ -37,9 +37,6 @@ describe('Pipelines Actions dropdown', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; - mock.restore(); confirmAction.mockReset(); }); diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js index e3e54716a7b..9fedbaf9b56 100644 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -34,11 +34,6 @@ describe('Pipelines Artifacts dropdown', () => { const findAllGlDropdownItems = () => wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render a dropdown with all the provided artifacts', () => { createComponent(); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 2523b901506..48539d84024 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -11,7 +11,7 @@ import { mockTracking } from 'helpers/tracking_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import Api from '~/api'; -import { createAlert, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_WARNING } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; @@ -25,7 +25,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination import { stageReply, users, mockSearch, branches } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const mockProjectPath = 'twitter/flight'; const mockProjectId = '21'; @@ -114,7 +114,6 @@ describe('Pipelines', () => { }); afterEach(() => { - wrapper.destroy(); mock.reset(); window.history.pushState.mockReset(); }); diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index 6ec8901038b..8d2a52eb6d0 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -69,12 +69,6 @@ describe('Pipelines Table', () => { pipeline = createMockPipeline(); }); - afterEach(() => { - wrapper.destroy(); - - wrapper = null; - }); - describe('Pipelines Table', () => { beforeEach(() => { createComponent({ pipelines: [pipeline], viewType: 'root' }); diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js index f6287107ed0..e05d2151f0a 100644 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js @@ -2,13 +2,13 @@ import MockAdapter from 'axios-mock-adapter'; import testReports from 'test_fixtures/pipelines/test_report.json'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import * as actions from '~/pipelines/stores/test_reports/actions'; import * as types from '~/pipelines/stores/test_reports/mutation_types'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('Actions TestReports Store', () => { let mock; @@ -49,7 +49,7 @@ describe('Actions TestReports Store', () => { ); }); - it('should create flash on API error', async () => { + it('should create alert on API error', async () => { await testAction( actions.fetchSummary, null, diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index ed0cc71eb97..9c374ea817a 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -1,9 +1,9 @@ import testReports from 'test_fixtures/pipelines/test_report.json'; import * as types from '~/pipelines/stores/test_reports/mutation_types'; import mutations from '~/pipelines/stores/test_reports/mutations'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('Mutations TestReports Store', () => { let mockState; @@ -58,7 +58,7 @@ describe('Mutations TestReports Store', () => { expect(mockState.errorMessage).toBe(message); }); - it('should show a flash message otherwise', () => { + it('should show an alert message otherwise', () => { mutations[types.SET_SUITE_ERROR](mockState, {}); expect(createAlert).toHaveBeenCalled(); diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js index f194864447c..f8663408817 100644 --- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js +++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js @@ -45,11 +45,6 @@ describe('Test case details', () => { ); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('required details', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js index 9b9ee4172f9..c8c917a1b9e 100644 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js @@ -60,10 +60,6 @@ describe('Test reports app', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when component is created', () => { it('should call fetchSummary when pipeline has test report', () => { createComponent(); diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js index da13df833e7..8eb83f17f4d 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -65,10 +65,6 @@ describe('Test reports suite table', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should render a message when there are no test cases', () => { createComponent({ suite: [] }); diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js index f0da0df2ba6..efb1bf09d20 100644 --- a/spec/frontend/pipelines/time_ago_spec.js +++ b/spec/frontend/pipelines/time_ago_spec.js @@ -30,11 +30,6 @@ describe('Timeago component', () => { ); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const duration = () => wrapper.find('.duration'); const finishedAt = () => wrapper.find('.finished-at'); const findInProgress = () => wrapper.findByTestId('pipeline-in-progress'); diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js index caa66502e11..d518519a424 100644 --- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js @@ -71,11 +71,6 @@ describe('Pipeline Branch Name Token', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('passes config correctly', () => { expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); }); diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js index c090fd353f7..cf4ccb5ce43 100644 --- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js @@ -45,11 +45,6 @@ describe('Pipeline Status Token', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('passes config correctly', () => { expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); }); diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js index 7311a5d2f5a..88c88d8f16f 100644 --- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js @@ -53,11 +53,6 @@ describe('Pipeline Branch Name Token', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('passes config correctly', () => { expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); }); diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js index c763bfe1b27..e9ec684a350 100644 --- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js @@ -52,11 +52,6 @@ describe('Pipeline Trigger Author Token', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('passes config correctly', () => { expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); }); diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js index 1299e7277d1..7f247fbbd4f 100644 --- a/spec/frontend/popovers/components/popovers_spec.js +++ b/spec/frontend/popovers/components/popovers_spec.js @@ -33,11 +33,6 @@ describe('popovers/components/popovers.vue', () => { const allPopovers = () => wrapper.findAllComponents(GlPopover); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('addPopovers', () => { it('attaches popovers to the targets specified', async () => { const target = createPopoverTarget(); diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js index e4a316e1ee7..9a8f82f0028 100644 --- a/spec/frontend/profile/account/components/delete_account_modal_spec.js +++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js @@ -40,12 +40,6 @@ describe('DeleteAccountModal component', () => { vm = wrapper.vm; }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - vm = null; - }); - const findElements = () => { const confirmation = vm.confirmWithPassword ? 'password' : 'username'; return { diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js index fa0e86a7b05..d922820601e 100644 --- a/spec/frontend/profile/account/components/update_username_spec.js +++ b/spec/frontend/profile/account/components/update_username_spec.js @@ -1,14 +1,15 @@ import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'helpers/test_constants'; -import { createAlert } from '~/flash'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import UpdateUsername from '~/profile/account/components/update_username.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('UpdateUsername component', () => { const rootUrl = TEST_HOST; @@ -21,8 +22,10 @@ describe('UpdateUsername component', () => { let wrapper; let axiosMock; + const findNewUsernameInput = () => wrapper.findByTestId('new-username-input'); + const createComponent = (props = {}) => { - wrapper = shallowMount(UpdateUsername, { + wrapper = shallowMountExtended(UpdateUsername, { propsData: { ...defaultProps, ...props, @@ -39,8 +42,8 @@ describe('UpdateUsername component', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); + Vue.config.errorHandler = null; }); const findElements = () => { @@ -56,6 +59,13 @@ describe('UpdateUsername component', () => { }; }; + const clickModalWithErrorResponse = () => { + Vue.config.errorHandler = jest.fn(); // silence thrown error + const { modal } = findElements(); + modal.vm.$emit('primary'); + return waitForPromises(); + }; + it('has a disabled button if the username was not changed', async () => { const { openModalBtn } = findElements(); @@ -80,11 +90,7 @@ describe('UpdateUsername component', () => { beforeEach(async () => { createComponent(); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ newUsername }); - - await nextTick(); + await findNewUsernameInput().setValue(newUsername); }); it('confirmation modal contains proper header and body', async () => { @@ -100,14 +106,15 @@ describe('UpdateUsername component', () => { axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]); jest.spyOn(axios, 'put'); - await wrapper.vm.onConfirm(); - await nextTick(); + const { modal } = findElements(); + modal.vm.$emit('primary'); + await waitForPromises(); expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } }); }); it('sets the username after a successful update', async () => { - const { input, openModalBtn } = findElements(); + const { input, openModalBtn, modal } = findElements(); axiosMock.onPut(actionUrl).replyOnce(() => { expect(input.attributes('disabled')).toBe('disabled'); @@ -117,8 +124,8 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_OK, { message: 'Username changed' }]; }); - await wrapper.vm.onConfirm(); - await nextTick(); + modal.vm.$emit('primary'); + await waitForPromises(); expect(input.attributes('disabled')).toBe(undefined); expect(openModalBtn.props('disabled')).toBe(true); @@ -136,7 +143,8 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }]; }); - await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + await clickModalWithErrorResponse(); + expect(input.attributes('disabled')).toBe(undefined); expect(openModalBtn.props('disabled')).toBe(false); expect(openModalBtn.props('loading')).toBe(false); @@ -147,7 +155,7 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }]; }); - await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + await clickModalWithErrorResponse(); expect(createAlert).toHaveBeenCalledWith({ message: 'Invalid username', @@ -159,7 +167,7 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_BAD_REQUEST]; }); - await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + await clickModalWithErrorResponse(); expect(createAlert).toHaveBeenCalledWith({ message: 'An error occurred while updating your username, please try again.', diff --git a/spec/frontend/profile/components/activity_calendar_spec.js b/spec/frontend/profile/components/activity_calendar_spec.js new file mode 100644 index 00000000000..fb9dc7b22f7 --- /dev/null +++ b/spec/frontend/profile/components/activity_calendar_spec.js @@ -0,0 +1,120 @@ +import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import * as GitLabUIUtils from '@gitlab/ui/dist/utils'; + +import ActivityCalendar from '~/profile/components/activity_calendar.vue'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { useFakeDate } from 'helpers/fake_date'; +import { userCalendarResponse } from '../mock_data'; + +jest.mock('~/lib/utils/ajax_cache'); +jest.mock('@gitlab/ui/dist/utils'); + +describe('ActivityCalendar', () => { + // Feb 21st, 2023 + useFakeDate(2023, 1, 21); + + let wrapper; + + const defaultProvide = { + userCalendarPath: '/users/root/calendar.json', + utcOffset: '0', + }; + + const createComponent = () => { + wrapper = mountExtended(ActivityCalendar, { provide: defaultProvide }); + }; + + const mockSuccessfulApiRequest = () => + AjaxCache.retrieve.mockResolvedValueOnce(userCalendarResponse); + const mockUnsuccessfulApiRequest = () => AjaxCache.retrieve.mockRejectedValueOnce(); + + const findCalendar = () => wrapper.findByTestId('contrib-calendar'); + + describe('when API request is loading', () => { + beforeEach(() => { + AjaxCache.retrieve.mockReturnValueOnce(new Promise(() => {})); + }); + + it('renders loading icon', () => { + createComponent(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('when API request is successful', () => { + beforeEach(() => { + mockSuccessfulApiRequest(); + }); + + it('renders the calendar', async () => { + createComponent(); + + await waitForPromises(); + + expect(findCalendar().exists()).toBe(true); + expect(wrapper.findByText(ActivityCalendar.i18n.calendarHint).exists()).toBe(true); + }); + + describe('when window is resized', () => { + it('re-renders the calendar', async () => { + createComponent(); + + await waitForPromises(); + + mockSuccessfulApiRequest(); + window.innerWidth = 1200; + window.dispatchEvent(new Event('resize')); + + await waitForPromises(); + + expect(findCalendar().exists()).toBe(true); + expect(AjaxCache.retrieve).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('when API request is not successful', () => { + beforeEach(() => { + mockUnsuccessfulApiRequest(); + }); + + it('renders error', async () => { + createComponent(); + + await waitForPromises(); + + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); + }); + + describe('when retry button is clicked', () => { + it('retries API request', async () => { + createComponent(); + + await waitForPromises(); + + mockSuccessfulApiRequest(); + + await wrapper.findByRole('button', { name: ActivityCalendar.i18n.retry }).trigger('click'); + + await waitForPromises(); + + expect(findCalendar().exists()).toBe(true); + }); + }); + }); + + describe('when screen is extra small', () => { + beforeEach(() => { + GitLabUIUtils.GlBreakpointInstance.getBreakpointSize.mockReturnValueOnce('xs'); + }); + + it('does not render the calendar', () => { + createComponent(); + + expect(findCalendar().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js index 4af428c4e0c..9cc5bdea9be 100644 --- a/spec/frontend/profile/components/followers_tab_spec.js +++ b/spec/frontend/profile/components/followers_tab_spec.js @@ -1,4 +1,4 @@ -import { GlTab } from '@gitlab/ui'; +import { GlBadge, GlTab } from '@gitlab/ui'; import { s__ } from '~/locale'; import FollowersTab from '~/profile/components/followers_tab.vue'; @@ -8,12 +8,25 @@ describe('FollowersTab', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(FollowersTab); + wrapper = shallowMountExtended(FollowersTab, { + provide: { + followers: 2, + }, + }); }; - it('renders `GlTab` and sets `title` prop', () => { + it('renders `GlTab` and sets title', () => { createComponent(); - expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers')); + expect(wrapper.findComponent(GlTab).element.textContent).toContain( + s__('UserProfile|Followers'), + ); + }); + + it('renders `GlBadge`, sets size and content', () => { + createComponent(); + + expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm'); + expect(wrapper.findComponent(GlBadge).element.textContent).toBe('2'); }); }); diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js index 75123274ccb..c9d56360c3e 100644 --- a/spec/frontend/profile/components/following_tab_spec.js +++ b/spec/frontend/profile/components/following_tab_spec.js @@ -1,4 +1,4 @@ -import { GlTab } from '@gitlab/ui'; +import { GlBadge, GlTab } from '@gitlab/ui'; import { s__ } from '~/locale'; import FollowingTab from '~/profile/components/following_tab.vue'; @@ -8,12 +8,25 @@ describe('FollowingTab', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(FollowingTab); + wrapper = shallowMountExtended(FollowingTab, { + provide: { + followees: 3, + }, + }); }; - it('renders `GlTab` and sets `title` prop', () => { + it('renders `GlTab` and sets title', () => { createComponent(); - expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following')); + expect(wrapper.findComponent(GlTab).element.textContent).toContain( + s__('UserProfile|Following'), + ); + }); + + it('renders `GlBadge`, sets size and content', () => { + createComponent(); + + expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm'); + expect(wrapper.findComponent(GlBadge).element.textContent).toBe('3'); }); }); diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js index eb27515bca3..d4cb1dfd15d 100644 --- a/spec/frontend/profile/components/overview_tab_spec.js +++ b/spec/frontend/profile/components/overview_tab_spec.js @@ -3,6 +3,7 @@ import { GlTab } from '@gitlab/ui'; import { s__ } from '~/locale'; import OverviewTab from '~/profile/components/overview_tab.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ActivityCalendar from '~/profile/components/activity_calendar.vue'; describe('OverviewTab', () => { let wrapper; @@ -16,4 +17,10 @@ describe('OverviewTab', () => { expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview')); }); + + it('renders `ActivityCalendar` component', () => { + createComponent(); + + expect(wrapper.findComponent(ActivityCalendar).exists()).toBe(true); + }); }); diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js new file mode 100644 index 00000000000..5b584eff362 --- /dev/null +++ b/spec/frontend/profile/components/user_achievements_spec.js @@ -0,0 +1,102 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json'; +import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json'; +import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json'; +import getUserAchievementsNoAvatarResponse from 'test_fixtures/graphql/get_user_achievements_without_avatar_or_description_response.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import UserAchievements from '~/profile/components/user_achievements.vue'; +import getUserAchievements from '~/profile/components//graphql/get_user_achievements.query.graphql'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +const USER_ID = 123; +const ROOT_URL = 'https://gitlab.com/'; +const PLACEHOLDER_URL = 'https://gitlab.com/assets/gitlab_logo.png'; +const userAchievement1 = getUserAchievementsResponse.data.user.userAchievements.nodes[0]; + +Vue.use(VueApollo); + +describe('UserAchievements', () => { + let wrapper; + + const getUserAchievementsQueryHandler = jest.fn().mockResolvedValue(getUserAchievementsResponse); + const achievement = () => wrapper.findByTestId('user-achievement'); + + const createComponent = ({ queryHandler = getUserAchievementsQueryHandler } = {}) => { + const fakeApollo = createMockApollo([[getUserAchievements, queryHandler]]); + + wrapper = mountExtended(UserAchievements, { + apolloProvider: fakeApollo, + provide: { + rootUrl: ROOT_URL, + userId: USER_ID, + }, + }); + }; + + it('renders no achievements on reject', async () => { + createComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('user-achievement').length).toBe(0); + }); + + it('renders no achievements when none are present', async () => { + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsEmptyResponse), + }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('user-achievement').length).toBe(0); + }); + + it('only renders 3 achievements when more are present', async () => { + createComponent({ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsLongResponse) }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('user-achievement').length).toBe(3); + }); + + it('renders achievement correctly', async () => { + createComponent(); + + await waitForPromises(); + + expect(achievement().text()).toContain(userAchievement1.achievement.name); + expect(achievement().text()).toContain( + `Awarded ${timeagoMixin.methods.timeFormatted(userAchievement1.createdAt)} by`, + ); + expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath); + expect(achievement().text()).toContain(userAchievement1.achievement.description); + expect(achievement().find('img').attributes('src')).toBe( + userAchievement1.achievement.avatarUrl, + ); + }); + + it('renders a placeholder when no avatar is present', async () => { + gon.gitlab_logo = PLACEHOLDER_URL; + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse), + }); + + await waitForPromises(); + + expect(achievement().find('img').attributes('src')).toBe(PLACEHOLDER_URL); + }); + + it('does not render a description when none is present', async () => { + gon.gitlab_logo = PLACEHOLDER_URL; + createComponent({ + queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse), + }); + + await waitForPromises(); + + expect(wrapper.findAllByTestId('achievement-description').length).toBe(0); + }); +}); diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js new file mode 100644 index 00000000000..7106ea84619 --- /dev/null +++ b/spec/frontend/profile/mock_data.js @@ -0,0 +1,22 @@ +export const userCalendarResponse = { + '2022-11-18': 13, + '2022-11-19': 21, + '2022-11-20': 14, + '2022-11-21': 15, + '2022-11-22': 20, + '2022-11-23': 21, + '2022-11-24': 15, + '2022-11-25': 14, + '2022-11-26': 16, + '2022-11-27': 13, + '2022-11-28': 4, + '2022-11-29': 1, + '2022-11-30': 1, + '2022-12-13': 1, + '2023-01-10': 3, + '2023-01-11': 2, + '2023-01-20': 1, + '2023-02-02': 1, + '2023-02-06': 2, + '2023-02-07': 2, +}; diff --git a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js index e60602ab336..e69bfad765a 100644 --- a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js +++ b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js @@ -12,11 +12,6 @@ describe('DiffsColorsPreview component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders diff colors preview', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/profile/preferences/components/diffs_colors_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_spec.js index 02f501a0b06..e80851d0629 100644 --- a/spec/frontend/profile/preferences/components/diffs_colors_spec.js +++ b/spec/frontend/profile/preferences/components/diffs_colors_spec.js @@ -29,11 +29,6 @@ describe('DiffsColors component', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('mounts', () => { createComponent(); diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js index f650bee7fda..b809f2f4aed 100644 --- a/spec/frontend/profile/preferences/components/integration_view_spec.js +++ b/spec/frontend/profile/preferences/components/integration_view_spec.js @@ -38,11 +38,6 @@ describe('IntegrationView component', () => { const findHiddenField = () => wrapper.findByTestId('profile-preferences-integration-hidden-field'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render the form group legend correctly', () => { wrapper = createComponent(); diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js index 91cd868daac..21167dccda9 100644 --- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js +++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/alert'; import IntegrationView from '~/profile/preferences/components/integration_view.vue'; import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue'; import { i18n } from '~/profile/preferences/constants'; @@ -17,7 +17,7 @@ import { lightModeThemeId2, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const expectedUrl = '/foo'; useMockLocationHelper(); @@ -83,11 +83,6 @@ describe('ProfilePreferences component', () => { document.body.classList.add('content-wrapper'); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should not render Integrations section', () => { wrapper = createComponent(); const views = wrapper.findAllComponents(IntegrationView); diff --git a/spec/frontend/profile/utils_spec.js b/spec/frontend/profile/utils_spec.js new file mode 100644 index 00000000000..43537afe169 --- /dev/null +++ b/spec/frontend/profile/utils_spec.js @@ -0,0 +1,15 @@ +import { getVisibleCalendarPeriod } from '~/profile/utils'; +import { CALENDAR_PERIOD_12_MONTHS, CALENDAR_PERIOD_6_MONTHS } from '~/profile/constants'; + +describe('getVisibleCalendarPeriod', () => { + it.each` + width | expected + ${1000} | ${CALENDAR_PERIOD_12_MONTHS} + ${900} | ${CALENDAR_PERIOD_6_MONTHS} + `('returns $expected when container width is $width', ({ width, expected }) => { + const container = document.createElement('div'); + jest.spyOn(container, 'getBoundingClientRect').mockReturnValueOnce({ width }); + + expect(getVisibleCalendarPeriod(container)).toBe(expected); + }); +}); diff --git a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js index d230b96ad82..68ea3a4dc4d 100644 --- a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js +++ b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js @@ -26,10 +26,6 @@ describe('ClustersDeprecationAlert', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('should render a non-dismissible warning alert', () => { expect(findAlert().props()).toMatchObject({ diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js index 6aa5a9a5a3a..0e68bd21cd4 100644 --- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js +++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js @@ -12,7 +12,7 @@ describe('BranchesDropdown', () => { let store; const spyFetchBranches = jest.fn(); - const createComponent = (props, state = { isFetching: false }) => { + const createComponent = (props, state = { isFetching: false, branch: '_main_' }) => { store = new Vuex.Store({ getters: { joinedBranches: () => ['_main_', '_branch_1_', '_branch_2_'], @@ -41,8 +41,6 @@ describe('BranchesDropdown', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; spyFetchBranches.mockReset(); }); diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js index c59cf700e0d..84cb30953c3 100644 --- a/spec/frontend/projects/commit/components/form_modal_spec.js +++ b/spec/frontend/projects/commit/components/form_modal_spec.js @@ -55,7 +55,6 @@ describe('CommitFormModal', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); @@ -166,7 +165,7 @@ describe('CommitFormModal', () => { it('Changes the target_project_id input value', async () => { createComponent(shallowMount, {}, {}, { isCherryPick: true }); - findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_'); + findProjectsDropdown().vm.$emit('input', '_changed_project_value_'); await nextTick(); diff --git a/spec/frontend/projects/commit/components/projects_dropdown_spec.js b/spec/frontend/projects/commit/components/projects_dropdown_spec.js index 0e213ff388a..baf2ea2656f 100644 --- a/spec/frontend/projects/commit/components/projects_dropdown_spec.js +++ b/spec/frontend/projects/commit/components/projects_dropdown_spec.js @@ -1,6 +1,6 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import ProjectsDropdown from '~/projects/commit/components/projects_dropdown.vue'; @@ -38,7 +38,6 @@ describe('ProjectsDropdown', () => { const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); afterEach(() => { - wrapper.destroy(); spyFetchProjects.mockReset(); }); @@ -48,20 +47,24 @@ describe('ProjectsDropdown', () => { }); describe('Custom events', () => { - it('should emit selectProject if a project is clicked', () => { + it('should emit input if a project is clicked', () => { findDropdown().vm.$emit('select', '1'); - expect(wrapper.emitted('selectProject')).toEqual([['1']]); + expect(wrapper.emitted('input')).toEqual([['1']]); }); }); }); describe('Case insensitive for search term', () => { beforeEach(() => { - createComponent('_PrOjEcT_1_'); + createComponent('_PrOjEcT_1_', { targetProjectId: '1' }); }); - it('renders only the project searched for', () => { + it('renders only the project searched for', async () => { + findDropdown().vm.$emit('search', '_project_1_'); + + await nextTick(); + expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]); }); }); diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js index 008710984b9..d48f9fd6fc0 100644 --- a/spec/frontend/projects/commit/store/actions_spec.js +++ b/spec/frontend/projects/commit/store/actions_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants'; import * as actions from '~/projects/commit/store/actions'; @@ -8,7 +8,7 @@ import * as types from '~/projects/commit/store/mutation_types'; import getInitialState from '~/projects/commit/store/state'; import mockData from '../mock_data'; -jest.mock('~/flash.js'); +jest.mock('~/alert'); describe('Commit form modal store actions', () => { let axiosMock; @@ -63,7 +63,7 @@ describe('Commit form modal store actions', () => { ); }); - it('should show flash error and set error in state on fetchBranches failure', async () => { + it('should show alert error and set error in state on fetchBranches failure', async () => { jest.spyOn(axios, 'get').mockRejectedValue(); await testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }]); diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js index 907e0e226b6..ff1d860fd53 100644 --- a/spec/frontend/projects/commits/components/author_select_spec.js +++ b/spec/frontend/projects/commits/components/author_select_spec.js @@ -54,7 +54,6 @@ describe('Author Select', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js index bae9c48fc1e..f5184e59420 100644 --- a/spec/frontend/projects/commits/store/actions_spec.js +++ b/spec/frontend/projects/commits/store/actions_spec.js @@ -1,13 +1,13 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import actions from '~/projects/commits/store/actions'; import * as types from '~/projects/commits/store/mutation_types'; import createState from '~/projects/commits/store/state'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Project commits actions', () => { let state; @@ -34,8 +34,8 @@ describe('Project commits actions', () => { ])); }); - describe('shows a flash message when there is an error', () => { - it('creates a flash', () => { + describe('shows an alert message when there is an error', () => { + it('creates an alert', () => { const mockDispatchContext = { dispatch: () => {}, commit: () => {}, state }; actions.receiveAuthorsError(mockDispatchContext); diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js index 9b052a17caa..ee96f46ea0c 100644 --- a/spec/frontend/projects/compare/components/app_spec.js +++ b/spec/frontend/projects/compare/components/app_spec.js @@ -21,11 +21,6 @@ describe('CompareApp component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - beforeEach(() => { createComponent(); }); diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js index 21cca857c6a..0b1085470b8 100644 --- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js @@ -16,11 +16,6 @@ describe('RepoDropdown component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findGlDropdown = () => wrapper.findComponent(GlDropdown); const findHiddenInput = () => wrapper.find('input[type="hidden"]'); diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js index b23bd91ceda..3c9c61c8903 100644 --- a/spec/frontend/projects/compare/components/revision_card_spec.js +++ b/spec/frontend/projects/compare/components/revision_card_spec.js @@ -16,11 +16,6 @@ describe('RepoDropdown component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - beforeEach(() => { createComponent(); }); diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js index 53763bd7d8f..645d0483a5f 100644 --- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js +++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js @@ -1,8 +1,8 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import { createAlert } from '~/flash'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue'; @@ -14,7 +14,7 @@ const defaultProps = { paramsBranch: 'main', }; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('RevisionDropdown component', () => { let wrapper; @@ -35,11 +35,14 @@ describe('RevisionDropdown component', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); const findGlDropdown = () => wrapper.findComponent(GlDropdown); + const findBranchesDropdownItem = () => + wrapper.findAllComponents('[data-testid="branches-dropdown-item"]'); + const findTagsDropdownItem = () => + wrapper.findAllComponents('[data-testid="tags-dropdown-item"]'); it('sets hidden input', () => { expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe( @@ -58,10 +61,21 @@ describe('RevisionDropdown component', () => { createComponent(); - await axios.waitForAll(); + expect(findBranchesDropdownItem()).toHaveLength(0); + expect(findTagsDropdownItem()).toHaveLength(0); - expect(wrapper.vm.branches).toEqual(Branches); - expect(wrapper.vm.tags).toEqual(Tags); + await waitForPromises(); + + Branches.forEach((branch, index) => { + expect(findBranchesDropdownItem().at(index).text()).toBe(branch); + }); + + Tags.forEach((tag, index) => { + expect(findTagsDropdownItem().at(index).text()).toBe(tag); + }); + + expect(findBranchesDropdownItem()).toHaveLength(Branches.length); + expect(findTagsDropdownItem()).toHaveLength(Tags.length); }); it('sets branches and tags to be an empty array when no tags or branches are given', async () => { @@ -70,16 +84,17 @@ describe('RevisionDropdown component', () => { Tags: undefined, }); - await axios.waitForAll(); + await waitForPromises(); - expect(wrapper.vm.branches).toEqual([]); - expect(wrapper.vm.tags).toEqual([]); + expect(findBranchesDropdownItem()).toHaveLength(0); + expect(findTagsDropdownItem()).toHaveLength(0); }); - it('shows flash message on error', async () => { + it('shows alert message on error', async () => { axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND); - await wrapper.vm.fetchBranchesAndTags(); + await waitForPromises(); + expect(createAlert).toHaveBeenCalled(); }); @@ -102,17 +117,19 @@ describe('RevisionDropdown component', () => { it('emits a "selectRevision" event when a revision is selected', async () => { const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstGlDropdownItem = () => findGlDropdownItems().at(0); + const branchName = 'some-branch'; - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ branches: ['some-branch'] }); + axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, { + Branches: [branchName], + }); - await nextTick(); + createComponent(); + await waitForPromises(); findFirstGlDropdownItem().vm.$emit('click'); expect(wrapper.emitted()).toEqual({ - selectRevision: [[{ direction: 'from', revision: 'some-branch' }]], + selectRevision: [[{ direction: 'from', revision: branchName }]], }); }); }); diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js index db4a1158996..3a256682549 100644 --- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js @@ -2,13 +2,14 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; -import { createAlert } from '~/flash'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; import { revisionDropdownDefaultProps as defaultProps } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('RevisionDropdown component', () => { let wrapper; @@ -32,12 +33,15 @@ describe('RevisionDropdown component', () => { }); afterEach(() => { - wrapper.destroy(); axiosMock.restore(); }); const findGlDropdown = () => wrapper.findComponent(GlDropdown); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findBranchesDropdownItem = () => + wrapper.findAllComponents('[data-testid="branches-dropdown-item"]'); + const findTagsDropdownItem = () => + wrapper.findAllComponents('[data-testid="tags-dropdown-item"]'); it('sets hidden input', () => { createComponent(); @@ -57,17 +61,29 @@ describe('RevisionDropdown component', () => { createComponent(); - await axios.waitForAll(); - expect(wrapper.vm.branches).toEqual(Branches); - expect(wrapper.vm.tags).toEqual(Tags); + expect(findBranchesDropdownItem()).toHaveLength(0); + expect(findTagsDropdownItem()).toHaveLength(0); + + await waitForPromises(); + + expect(findBranchesDropdownItem()).toHaveLength(Branches.length); + expect(findTagsDropdownItem()).toHaveLength(Tags.length); + + Branches.forEach((branch, index) => { + expect(findBranchesDropdownItem().at(index).text()).toBe(branch); + }); + + Tags.forEach((tag, index) => { + expect(findTagsDropdownItem().at(index).text()).toBe(tag); + }); }); - it('shows flash message on error', async () => { + it('shows alert message on error', async () => { axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND); createComponent(); + await waitForPromises(); - await wrapper.vm.fetchBranchesAndTags(); expect(createAlert).toHaveBeenCalled(); }); @@ -83,17 +99,17 @@ describe('RevisionDropdown component', () => { refsProjectPath: newRefsProjectPath, }); - await axios.waitForAll(); + await waitForPromises(); expect(axios.get).toHaveBeenLastCalledWith(newRefsProjectPath); }); describe('search', () => { - it('shows flash message on error', async () => { + it('shows alert message on error', async () => { axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND); createComponent(); + await waitForPromises(); - await wrapper.vm.searchBranchesAndTags(); expect(createAlert).toHaveBeenCalled(); }); @@ -108,7 +124,7 @@ describe('RevisionDropdown component', () => { const mockSearchTerm = 'foobar'; createComponent(); findSearchBox().vm.$emit('input', mockSearchTerm); - await axios.waitForAll(); + await waitForPromises(); expect(axios.get).toHaveBeenCalledWith( defaultProps.refsProjectPath, @@ -141,8 +157,14 @@ describe('RevisionDropdown component', () => { }); it('emits `selectRevision` event when another revision is selected', async () => { + jest.spyOn(axios, 'get').mockResolvedValue({ + data: { + Branches: ['some-branch'], + Tags: [], + }, + }); + createComponent(); - wrapper.vm.branches = ['some-branch']; await nextTick(); findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click'); diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js index 49e3218e5bc..bae76e7eeb6 100644 --- a/spec/frontend/projects/components/project_delete_button_spec.js +++ b/spec/frontend/projects/components/project_delete_button_spec.js @@ -33,11 +33,6 @@ describe('Project remove modal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('initialized', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js index 097b18025a3..364a29d0e41 100644 --- a/spec/frontend/projects/components/shared/delete_button_spec.js +++ b/spec/frontend/projects/components/shared/delete_button_spec.js @@ -45,11 +45,6 @@ describe('Project remove modal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('intialized', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js index 50638755260..e9b11ce544a 100644 --- a/spec/frontend/projects/details/upload_button_spec.js +++ b/spec/frontend/projects/details/upload_button_spec.js @@ -27,10 +27,6 @@ describe('UploadButton', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays an upload button', () => { expect(wrapper.findComponent(GlButton).exists()).toBe(true); }); diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js index f6edbab3cca..5b2dc25077e 100644 --- a/spec/frontend/projects/new/components/app_spec.js +++ b/spec/frontend/projects/new/components/app_spec.js @@ -6,12 +6,12 @@ describe('Experimental new project creation app', () => { let wrapper; const createComponent = (propsData) => { - wrapper = shallowMount(App, { propsData }); + wrapper = shallowMount(App, { + propsData: { projectsUrl: '/dashboard/projects', ...propsData }, + }); }; - afterEach(() => { - wrapper.destroy(); - }); + const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage); it('passes custom new project guideline text to underlying component', () => { const DEMO_GUIDELINES = 'Demo guidelines'; @@ -34,11 +34,28 @@ describe('Experimental new project creation app', () => { expect( Boolean( - wrapper - .findComponent(NewNamespacePage) + findNewNamespacePage() .props() .panels.find((p) => p.name === 'cicd_for_external_repo'), ), ).toBe(isCiCdAvailable); }); + + it('creates correct breadcrumbs for top-level projects', () => { + createComponent(); + + expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([ + { href: '/dashboard/projects', text: 'Projects' }, + { href: '#', text: 'New project' }, + ]); + }); + + it('creates correct breadcrumbs for projects within groups', () => { + createComponent({ parentGroupUrl: '/parent-group', parentGroupName: 'Parent Group' }); + + expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([ + { href: '/parent-group', text: 'Parent Group' }, + { href: '#', text: 'New project' }, + ]); + }); }); diff --git a/spec/frontend/projects/new/components/deployment_target_select_spec.js b/spec/frontend/projects/new/components/deployment_target_select_spec.js index f3b22d4a1b9..bec738f7765 100644 --- a/spec/frontend/projects/new/components/deployment_target_select_spec.js +++ b/spec/frontend/projects/new/components/deployment_target_select_spec.js @@ -47,7 +47,6 @@ describe('Deployment target select', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js index 16b4493c622..1a43dcb682b 100644 --- a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js +++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js @@ -37,7 +37,6 @@ describe('New project push tip popover', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js index 67532cea61e..fa720f4487c 100644 --- a/spec/frontend/projects/new/components/new_project_url_select_spec.js +++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js @@ -3,8 +3,8 @@ import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, - GlSearchBoxByType, GlTruncate, + GlSearchBoxByType, } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; @@ -12,6 +12,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { stubComponent } from 'helpers/stub_component'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import eventHub from '~/projects/new/event_hub'; import NewProjectUrlSelect from '~/projects/new/components/new_project_url_select.vue'; @@ -68,6 +69,7 @@ describe('NewProjectUrlSelect component', () => { }; let mockQueryResponse; + let focusInputSpy; const mountComponent = ({ search = '', @@ -78,6 +80,7 @@ describe('NewProjectUrlSelect component', () => { mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse }); const requestHandlers = [[searchQuery, mockQueryResponse]]; const apolloProvider = createMockApollo(requestHandlers); + focusInputSpy = jest.fn(); return mountFn(NewProjectUrlSelect, { apolloProvider, @@ -87,13 +90,17 @@ describe('NewProjectUrlSelect component', () => { search, }; }, + stubs: { + GlSearchBoxByType: stubComponent(GlSearchBoxByType, { + methods: { focusInput: focusInputSpy }, + }), + }, }); }; const findButtonLabel = () => wrapper.findComponent(GlButton); const findDropdown = () => wrapper.findComponent(GlDropdown); const findSelectedPath = () => wrapper.findComponent(GlTruncate); - const findInput = () => wrapper.findComponent(GlSearchBoxByType); const findHiddenNamespaceInput = () => wrapper.find(`[name="${defaultProvide.inputName}`); const findHiddenSelectedNamespaceInput = () => @@ -111,10 +118,6 @@ describe('NewProjectUrlSelect component', () => { await waitForPromises(); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders the root url as a label', () => { wrapper = mountComponent(); @@ -177,13 +180,11 @@ describe('NewProjectUrlSelect component', () => { }); it('focuses on the input when the dropdown is opened', async () => { - wrapper = mountComponent({ mountFn: mount }); - - const spy = jest.spyOn(findInput().vm, 'focusInput'); + wrapper = mountComponent(); await showDropdown(); - expect(spy).toHaveBeenCalledTimes(1); + expect(focusInputSpy).toHaveBeenCalledTimes(1); }); it('renders expected dropdown items', async () => { diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap index fc51825f15b..1545c52d7cb 100644 --- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap +++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap @@ -8,20 +8,19 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = ` Some title </p> - <div> - <glareachart-stub - annotations="" - data="[object Object],[object Object]" - height="300" - legendaveragetext="Avg" - legendcurrenttext="Current" - legendlayout="inline" - legendmaxtext="Max" - legendmintext="Min" - option="[object Object]" - thresholds="" - width="0" - /> - </div> + <glareachart-stub + annotations="" + data="[object Object],[object Object]" + height="300" + legendaveragetext="Avg" + legendcurrenttext="Current" + legendlayout="inline" + legendmaxtext="Max" + legendmintext="Min" + option="[object Object]" + responsive="" + thresholds="" + width="auto" + /> </div> `; diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js index d8876349c5e..94f421239da 100644 --- a/spec/frontend/projects/pipelines/charts/components/app_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js @@ -49,10 +49,6 @@ describe('ProjectsPipelinesChartsApp', () => { ); } - afterEach(() => { - wrapper.destroy(); - }); - const findGlTabs = () => wrapper.findComponent(GlTabs); const findAllGlTabs = () => wrapper.findAllComponents(GlTab); const findGlTabAtIndex = (index) => findAllGlTabs().at(index); diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js index 2b523467379..5fc121b5c9f 100644 --- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js @@ -28,11 +28,6 @@ describe('CiCdAnalyticsAreaChart', () => { }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('matches the snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js index 8fb59f38ee1..ab2a12219e5 100644 --- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js @@ -37,11 +37,6 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => { await waitForPromises(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('overall statistics', () => { it('displays the statistics list', () => { const list = wrapper.findComponent(StatisticsList); diff --git a/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js b/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js index 57a864cb2c4..24dbc628ce6 100644 --- a/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js @@ -21,10 +21,6 @@ describe('StatisticsList', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays the counts data with labels', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/projects/prune_unreachable_objects_button_spec.js b/spec/frontend/projects/prune_unreachable_objects_button_spec.js index b345f264ca7..012b19ea3d3 100644 --- a/spec/frontend/projects/prune_unreachable_objects_button_spec.js +++ b/spec/frontend/projects/prune_unreachable_objects_button_spec.js @@ -22,16 +22,11 @@ describe('Project remove modal', () => { wrapper = shallowMountExtended(PruneObjectsButton, { propsData: defaultProps, directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-modal'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('intialized', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js index 11f219c1f90..6d3317a5f78 100644 --- a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js @@ -8,10 +8,10 @@ import BranchDropdown, { import createMockApollo from 'helpers/mock_apollo_helper'; import branchesQuery from '~/projects/settings/branch_rules/queries/branches.query.graphql'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Branch dropdown', () => { let wrapper; @@ -46,10 +46,6 @@ describe('Branch dropdown', () => { beforeEach(() => createComponent()); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a GlDropdown component with the correct props', () => { expect(findGlDropdown().props()).toMatchObject({ text: value }); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js index 21e63fdb24d..e9982872e03 100644 --- a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js @@ -24,10 +24,6 @@ describe('Edit branch rule', () => { beforeEach(() => createComponent()); - afterEach(() => { - wrapper.destroy(); - }); - it('gets the branch param from url', () => { expect(getParameterByName).toHaveBeenCalledWith('branch'); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js index ee90ff8318f..14edaf31a1f 100644 --- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js @@ -26,10 +26,6 @@ describe('Branch Protections', () => { beforeEach(() => createComponent()); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a heading', () => { expect(findHeading().text()).toBe(i18n.protections); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js index b5fdc46d600..ca561ef87ec 100644 --- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js @@ -24,10 +24,6 @@ describe('Merge Protections', () => { beforeEach(() => createComponent()); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a form group with the correct label', () => { expect(findFormGroup().text()).toContain(i18n.allowedToMerge); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js index 60bb7a51dcb..82998640f17 100644 --- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js @@ -24,10 +24,6 @@ describe('Push Protections', () => { beforeEach(() => createComponent()); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a form group with the correct label', () => { expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index 714e0df596e..077995ab6e4 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -9,6 +9,10 @@ import Protection from '~/projects/settings/branch_rules/components/view/protect import { I18N, ALL_BRANCHES_WILDCARD, + REQUIRED_ICON, + NOT_REQUIRED_ICON, + REQUIRED_ICON_CLASS, + NOT_REQUIRED_ICON_CLASS, } from '~/projects/settings/branch_rules/components/view/constants'; import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'; import { sprintf } from '~/locale'; @@ -19,7 +23,7 @@ import { jest.mock('~/lib/utils/url_utility', () => ({ getParameterByName: jest.fn().mockReturnValue('main'), - mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=main'), + mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=%5Emain%24'), joinPaths: jest.fn(), })); @@ -39,12 +43,13 @@ describe('View branch rules', () => { let fakeApollo; const projectPath = 'test/testing'; const protectedBranchesPath = 'protected/branches'; - const branchProtectionsMockRequestHandler = jest - .fn() - .mockResolvedValue(branchProtectionsMockResponse); + const branchProtectionsMockRequestHandler = (response = branchProtectionsMockResponse) => + jest.fn().mockResolvedValue(response); - const createComponent = async () => { - fakeApollo = createMockApollo([[branchRulesQuery, branchProtectionsMockRequestHandler]]); + const createComponent = async (mockResponse) => { + fakeApollo = createMockApollo([ + [branchRulesQuery, branchProtectionsMockRequestHandler(mockResponse)], + ]); wrapper = shallowMountExtended(RuleView, { apolloProvider: fakeApollo, @@ -57,13 +62,13 @@ describe('View branch rules', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - const findBranchName = () => wrapper.findByTestId('branch'); const findBranchTitle = () => wrapper.findByTestId('branch-title'); const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle); const findBranchProtections = () => wrapper.findAllComponents(Protection); - const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription); + const findForcePushIcon = () => wrapper.findByTestId('force-push-icon'); + const findForcePushTitle = (title) => wrapper.findByText(title); + const findForcePushDescription = () => wrapper.findByText(I18N.forcePushDescription); const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle); const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle); const findMatchingBranchesLink = () => @@ -94,9 +99,12 @@ describe('View branch rules', () => { }); it('renders matching branches link', () => { + const mergeUrlParams = jest.spyOn(util, 'mergeUrlParams'); const matchingBranchesLink = findMatchingBranchesLink(); + + expect(mergeUrlParams).toHaveBeenCalledWith({ state: 'all', search: `^main$` }, ''); expect(matchingBranchesLink.exists()).toBe(true); - expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=main'); + expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=%5Emain%24'); }); it('renders a branch protection title', () => { @@ -123,9 +131,23 @@ describe('View branch rules', () => { }); }); - it('renders force push protection', () => { - expect(findForcePushTitle().exists()).toBe(true); - }); + it.each` + allowForcePush | iconName | iconClass | title + ${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.allowForcePushTitle} + ${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotAllowForcePushTitle} + `( + 'renders force push section with the correct icon, title and description', + async ({ allowForcePush, iconName, iconClass, title }) => { + const mockResponse = branchProtectionsMockResponse; + mockResponse.data.project.branchRules.nodes[0].branchProtection.allowForcePush = allowForcePush; + await createComponent(mockResponse); + + expect(findForcePushIcon().props('name')).toBe(iconName); + expect(findForcePushIcon().attributes('class')).toBe(iconClass); + expect(findForcePushTitle(title).exists()).toBe(true); + expect(findForcePushDescription().exists()).toBe(true); + }, + ); it('renders a branch protection component for merge rules', () => { expect(findBranchProtections().at(1).props()).toMatchObject({ diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js index a98b156f94e..1bfd04e10a1 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js @@ -18,8 +18,6 @@ describe('Branch rule protection row', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - const findTitle = () => wrapper.findByText(protectionRowPropsMock.title); const findAvatarsInline = () => wrapper.findComponent(GlAvatarsInline); const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js index caf967b4257..f10d8d6d770 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js @@ -16,8 +16,6 @@ describe('Branch rule protection', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - const findCard = () => wrapper.findComponent(GlCard); const findHeader = () => wrapper.findByText(protectionPropsMock.header); const findLink = () => wrapper.findComponent(GlLink); diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js index ca9a72663d2..c1412d01b53 100644 --- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js +++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js @@ -19,10 +19,6 @@ describe('projects/settings/components/default_branch_selector', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { buildWrapper(); }); diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js index 26297d0c3ff..f3e536de703 100644 --- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js +++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js @@ -89,15 +89,12 @@ describe('Access Level Dropdown', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownToggleLabel = () => findDropdown().props('text'); const findAllDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem); const findAllDropdownHeaders = () => findDropdown().findAllComponents(GlDropdownSectionHeader); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findDeployKeyDropdownItem = () => wrapper.findByTestId('deploy_key-dropdown-item'); const findDropdownItemWithText = (items, text) => items.filter((item) => item.text().includes(text)).at(0); @@ -142,6 +139,21 @@ describe('Access Level Dropdown', () => { it('renders dropdown item for each access level type', () => { expect(findAllDropdownItems()).toHaveLength(12); }); + + it.each` + accessLevel | shouldRenderDeployKeyItems + ${ACCESS_LEVELS.PUSH} | ${true} + ${ACCESS_LEVELS.CREATE} | ${true} + ${ACCESS_LEVELS.MERGE} | ${false} + `( + 'conditionally renders deploy keys based on access levels', + async ({ accessLevel, shouldRenderDeployKeyItems }) => { + createComponent({ accessLevel }); + await waitForPromises(); + + expect(findDeployKeyDropdownItem().exists()).toBe(shouldRenderDeployKeyItems); + }, + ); }); describe('toggleLabel', () => { diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js index f82ad80135e..0ec0e981d65 100644 --- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js +++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js @@ -9,7 +9,7 @@ import SharedRunnersToggleComponent from '~/projects/settings/components/shared_ const TEST_UPDATE_PATH = '/test/update_shared_runners'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('projects/settings/components/shared_runners', () => { let wrapper; @@ -41,8 +41,6 @@ describe('projects/settings/components/shared_runners', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; mockAxios.restore(); }); 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 e091f3e25c3..d8c2cf83f38 100644 --- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js +++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js @@ -31,10 +31,6 @@ describe('Transfer project form', () => { const findTransferLocations = () => wrapper.findComponent(TransferLocations); const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => { createComponent(); diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js index 56b39f04580..dd534bec25d 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js @@ -7,7 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import BranchRules from '~/projects/settings/repository/branch_rules/app.vue'; import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { branchRulesMockResponse, appProvideMock, @@ -22,7 +22,7 @@ import { expandSection } from '~/settings_panels'; import { scrollToElement } from '~/lib/utils/common_utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/settings_panels'); jest.mock('~/lib/utils/common_utils'); @@ -41,7 +41,7 @@ describe('Branch rules app', () => { apolloProvider: fakeApollo, provide: appProvideMock, stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) }, - directives: { GlModal: createMockDirective() }, + directives: { GlModal: createMockDirective('gl-modal') }, }); await waitForPromises(); diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js index 8d0fd390e35..8bea84f4429 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js @@ -71,8 +71,10 @@ describe('Branch rule', () => { }); it('renders a detail button with the correct href', () => { + const encodedBranchName = encodeURIComponent(branchRulePropsMock.name); + expect(findDetailsButton().attributes('href')).toBe( - `${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`, + `${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`, ); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js index de7f6c8b88d..d169397241d 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js +++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js @@ -74,7 +74,7 @@ export const branchRuleProvideMock = { }; export const branchRulePropsMock = { - name: 'main', + name: 'branch-with-$speci@l-#-chars', isDefault: true, matchingBranchesCount: 1, branchProtection: { diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js index 8b8e7d1454d..4b94c179f74 100644 --- a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js +++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js @@ -64,7 +64,6 @@ describe('TopicsTokenSelector', () => { }); afterEach(() => { - wrapper.destroy(); div.remove(); input.remove(); }); diff --git a/spec/frontend/projects/settings/utils_spec.js b/spec/frontend/projects/settings/utils_spec.js index 319aa4000b5..d85f43778b1 100644 --- a/spec/frontend/projects/settings/utils_spec.js +++ b/spec/frontend/projects/settings/utils_spec.js @@ -1,4 +1,5 @@ -import { getAccessLevels } from '~/projects/settings/utils'; +import { getAccessLevels, generateRefDestinationPath } from '~/projects/settings/utils'; +import setWindowLocation from 'helpers/set_window_location_helper'; import { pushAccessLevelsMockResponse, pushAccessLevelsMockResult } from './mock_data'; describe('Utils', () => { @@ -8,4 +9,25 @@ describe('Utils', () => { expect(pushAccessLevels).toEqual(pushAccessLevelsMockResult); }); }); + + describe('generateRefDestinationPath', () => { + const projectRootPath = 'http://test.host/root/Project1'; + const settingsCi = '-/settings/ci_cd'; + + it.each` + currentPath | selectedRef | result + ${`${projectRootPath}`} | ${undefined} | ${`${projectRootPath}`} + ${`${projectRootPath}`} | ${'test'} | ${`${projectRootPath}`} + ${`${projectRootPath}/${settingsCi}`} | ${'test'} | ${`${projectRootPath}/${settingsCi}?ref=test`} + ${`${projectRootPath}/${settingsCi}`} | ${'branch-hyphen'} | ${`${projectRootPath}/${settingsCi}?ref=branch-hyphen`} + ${`${projectRootPath}/${settingsCi}`} | ${'test/branch'} | ${`${projectRootPath}/${settingsCi}?ref=test%2Fbranch`} + ${`${projectRootPath}/${settingsCi}`} | ${'test/branch-hyphen'} | ${`${projectRootPath}/${settingsCi}?ref=test%2Fbranch-hyphen`} + `( + 'generates the correct destination path for the `$selectedRef` ref and current url $currentPath by outputting $result', + ({ currentPath, selectedRef, result }) => { + setWindowLocation(currentPath); + expect(generateRefDestinationPath(selectedRef)).toBe(result); + }, + ); + }); }); diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js index 5fc9f9ba629..4d0d2191176 100644 --- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js @@ -41,7 +41,6 @@ describe('ServiceDeskRoot', () => { afterEach(() => { axiosMock.restore(); - wrapper.destroy(); if (spy) { spy.mockRestore(); } diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js index 6576ce70d60..1d0faebbcb2 100644 --- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js +++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js @@ -41,10 +41,6 @@ describe('TerraformNotificationBanner', () => { trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when user has already dismissed the banner', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js index b4029d94980..4141d000a1c 100644 --- a/spec/frontend/protected_branches/protected_branch_edit_spec.js +++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js @@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit'; -jest.mock('~/flash'); +jest.mock('~/alert'); const TEST_URL = `${TEST_HOST}/url`; const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle'; @@ -149,7 +149,7 @@ describe('ProtectedBranchEdit', () => { toggle.click(); }); - it('flashes error', async () => { + it('alerts error', async () => { await axios.waitForAll(); expect(createAlert).toHaveBeenCalled(); diff --git a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap b/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap deleted file mode 100644 index 5053778369e..00000000000 --- a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Ref selector component footer slot passes the expected slot props 1`] = ` -Object { - "isLoading": false, - "matches": Object { - "branches": Object { - "error": null, - "list": Array [ - Object { - "default": false, - "name": "add_images_and_changes", - "value": undefined, - }, - Object { - "default": false, - "name": "conflict-contains-conflict-markers", - "value": undefined, - }, - Object { - "default": false, - "name": "deleted-image-test", - "value": undefined, - }, - Object { - "default": false, - "name": "diff-files-image-to-symlink", - "value": undefined, - }, - Object { - "default": false, - "name": "diff-files-symlink-to-image", - "value": undefined, - }, - Object { - "default": false, - "name": "markdown", - "value": undefined, - }, - Object { - "default": true, - "name": "master", - "value": undefined, - }, - ], - "totalCount": 123, - }, - "commits": Object { - "error": null, - "list": Array [ - Object { - "name": "b83d6e39", - "subtitle": "Merge branch 'branch-merged' into 'master'", - "value": "b83d6e391c22777fca1ed3012fce84f633d7fed0", - }, - ], - "totalCount": 1, - }, - "tags": Object { - "error": null, - "list": Array [ - Object { - "name": "v1.1.1", - "value": undefined, - }, - Object { - "name": "v1.1.0", - "value": undefined, - }, - Object { - "name": "v1.0.0", - "value": undefined, - }, - ], - "totalCount": 456, - }, - }, - "query": "abcd1234", -} -`; diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index 40d3a291074..6b90827f9c2 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -4,9 +4,9 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { merge, last } from 'lodash'; import Vuex from 'vuex'; +import tags from 'test_fixtures/api/tags/tags.json'; import commit from 'test_fixtures/api/commits/commit.json'; import branches from 'test_fixtures/api/branches/branches.json'; -import tags from 'test_fixtures/api/tags/tags.json'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { trimText } from 'helpers/text_helper'; import { @@ -33,6 +33,8 @@ describe('Ref selector component', () => { const fixtures = { branches, tags, commit }; const projectId = '8'; + const totalBranchesCount = 123; + const totalTagsCount = 456; let wrapper; let branchesApiCallSpy; @@ -69,10 +71,14 @@ describe('Ref selector component', () => { branchesApiCallSpy = jest .fn() - .mockReturnValue([HTTP_STATUS_OK, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]); + .mockReturnValue([ + HTTP_STATUS_OK, + fixtures.branches, + { [X_TOTAL_HEADER]: totalBranchesCount }, + ]); tagsApiCallSpy = jest .fn() - .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]); + .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: totalTagsCount }]); commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, fixtures.commit]); requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy }; @@ -690,7 +696,46 @@ describe('Ref selector component', () => { // is updated. For the sake of this test, we'll just test the last call, which // represents the final state of the slot props. const lastCallProps = last(createFooter.mock.calls)[0]; - expect(lastCallProps).toMatchSnapshot(); + expect(lastCallProps.isLoading).toBe(false); + expect(lastCallProps.query).toBe('abcd1234'); + + const branchesList = fixtures.branches.map((branch) => { + return { + default: branch.default, + name: branch.name, + }; + }); + + const commitsList = [ + { + name: fixtures.commit.short_id, + subtitle: fixtures.commit.title, + value: fixtures.commit.id, + }, + ]; + + const tagsList = fixtures.tags.map((tag) => { + return { + name: tag.name, + }; + }); + + const expectedMatches = { + branches: { + list: branchesList, + totalCount: totalBranchesCount, + }, + commits: { + list: commitsList, + totalCount: 1, + }, + tags: { + list: tagsList, + totalCount: totalTagsCount, + }, + }; + + expect(lastCallProps.matches).toMatchObject(expectedMatches); }); }); }); diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index bd61e4537f9..dcb6a3293a6 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -16,6 +16,7 @@ import AssetLinksForm from '~/releases/components/asset_links_form.vue'; import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { ValidationResult } from '~/lib/utils/ref_validator'; const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release; const originalMilestones = originalRelease.milestones; @@ -58,6 +59,7 @@ describe('Release edit/new component', () => { assets: { links: [], }, + tagNameValidation: new ValidationResult(), }), formattedReleaseNotes: () => 'these notes are formatted', }; @@ -101,11 +103,6 @@ describe('Release edit/new component', () => { release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findSubmitButton = () => wrapper.find('button[type=submit]'); const findForm = () => wrapper.find('form'); diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js index ef3bd5ca873..7a0e9fb7326 100644 --- a/spec/frontend/releases/components/app_index_spec.js +++ b/spec/frontend/releases/components/app_index_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { historyPushState } from '~/lib/utils/common_utils'; import { sprintf, __ } from '~/locale'; import ReleasesIndexApp from '~/releases/components/app_index.vue'; @@ -20,7 +20,7 @@ import { deleteReleaseSessionKey } from '~/releases/util'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); let mockQueryParams; jest.mock('~/lib/utils/common_utils', () => ({ @@ -114,7 +114,7 @@ describe('app_index.vue', () => { const toDescription = (bool) => (bool ? 'does' : 'does not'); describe.each` - description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination + description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | alertMessage | releaseCount | pagination ${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false} ${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false} ${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true} @@ -134,7 +134,7 @@ describe('app_index.vue', () => { fullResponseFn, loadingIndicator, emptyState, - flashMessage, + alertMessage, releaseCount, pagination, }) => { @@ -154,9 +154,9 @@ describe('app_index.vue', () => { expect(findEmptyState().exists()).toBe(emptyState); }); - it(`${toDescription(flashMessage)} show a flash message`, async () => { + it(`${toDescription(alertMessage)} show a flash message`, async () => { await waitForPromises(); - if (flashMessage) { + if (alertMessage) { expect(createAlert).toHaveBeenCalledWith({ message: ReleasesIndexApp.i18n.errorMessage, captureError: true, diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index efe72e8000a..942280cb6a2 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo'; import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { popCreateReleaseNotification } from '~/releases/release_notification_service'; import ReleaseShowApp from '~/releases/components/app_show.vue'; import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue'; import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/releases/release_notification_service'); Vue.use(VueApollo); @@ -33,11 +33,6 @@ describe('Release show component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findLoadingSkeleton = () => wrapper.findComponent(ReleaseSkeletonLoader); const findReleaseBlock = () => wrapper.findComponent(ReleaseBlock); @@ -54,13 +49,13 @@ describe('Release show component', () => { }; const expectNoFlash = () => { - it('does not show a flash message', () => { + it('does not show an alert message', () => { expect(createAlert).not.toHaveBeenCalled(); }); }; const expectFlashWithMessage = (message) => { - it(`shows a flash message that reads "${message}"`, () => { + it(`shows an alert message that reads "${message}"`, () => { expect(createAlert).toHaveBeenCalledWith({ message, captureError: true, @@ -152,7 +147,7 @@ describe('Release show component', () => { beforeEach(async () => { // As we return a release as `null`, Apollo also throws an error to the console // about the missing field. We need to suppress console.error in order to check - // that flash message was called + // that alert message was called // eslint-disable-next-line no-console console.error = jest.fn(); diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js index b1e9d8d1256..8eee9acd808 100644 --- a/spec/frontend/releases/components/asset_links_form_spec.js +++ b/spec/frontend/releases/components/asset_links_form_spec.js @@ -60,11 +60,6 @@ describe('Release edit component', () => { release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with a basic store state', () => { beforeEach(() => { factory(); diff --git a/spec/frontend/releases/components/confirm_delete_modal_spec.js b/spec/frontend/releases/components/confirm_delete_modal_spec.js index f7c526c1ced..b4699302779 100644 --- a/spec/frontend/releases/components/confirm_delete_modal_spec.js +++ b/spec/frontend/releases/components/confirm_delete_modal_spec.js @@ -42,10 +42,6 @@ describe('~/releases/components/confirm_delete_modal.vue', () => { factory(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('button', () => { it('should open the modal on click', async () => { await wrapper.findByRole('button', { name: 'Delete' }).trigger('click'); diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js index 69443cb7a11..42eac31e5ac 100644 --- a/spec/frontend/releases/components/evidence_block_spec.js +++ b/spec/frontend/releases/components/evidence_block_spec.js @@ -27,10 +27,6 @@ describe('Evidence Block', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the evidence icon', () => { expect(wrapper.findComponent(GlIcon).props('name')).toBe('review-list'); }); diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js index 3ac75e138ee..c8cdf9cb951 100644 --- a/spec/frontend/releases/components/issuable_stats_spec.js +++ b/spec/frontend/releases/components/issuable_stats_spec.js @@ -34,11 +34,6 @@ describe('~/releases/components/issuable_stats.vue', () => { }; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('matches snapshot', () => { createComponent(); diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js index 19b41d05a44..12e3807c9fa 100644 --- a/spec/frontend/releases/components/release_block_footer_spec.js +++ b/spec/frontend/releases/components/release_block_footer_spec.js @@ -33,11 +33,6 @@ describe('Release block footer', () => { release = cloneDeep(originalRelease); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const commitInfoSection = () => wrapper.find('.js-commit-info'); const commitInfoSectionLink = () => commitInfoSection().findComponent(GlLink); const tagInfoSection = () => wrapper.find('.js-tag-info'); diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js index fc421776d60..dd39a1bce53 100644 --- a/spec/frontend/releases/components/release_block_header_spec.js +++ b/spec/frontend/releases/components/release_block_header_spec.js @@ -25,10 +25,6 @@ describe('Release block header', () => { release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); - afterEach(() => { - wrapper.destroy(); - }); - const findHeader = () => wrapper.find('h2'); const findHeaderLink = () => findHeader().findComponent(GlLink); const findEditButton = () => wrapper.find('.js-edit-button'); diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js index 541d487091c..b8030ae1fd2 100644 --- a/spec/frontend/releases/components/release_block_milestone_info_spec.js +++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js @@ -25,11 +25,6 @@ describe('Release block milestone info', () => { milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const milestoneProgressBarContainer = () => wrapper.find('.js-milestone-progress-bar-container'); const milestoneListContainer = () => wrapper.find('.js-milestone-list-container'); const issuesContainer = () => wrapper.find('[data-testid="issue-stats"]'); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index f1b8554fbc3..3355b5ab2c3 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -39,10 +39,6 @@ describe('Release block', () => { release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('with default props', () => { beforeEach(() => factory(release)); diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js index 59be808c802..923d84ae2b3 100644 --- a/spec/frontend/releases/components/releases_pagination_spec.js +++ b/spec/frontend/releases/components/releases_pagination_spec.js @@ -29,10 +29,6 @@ describe('releases_pagination.vue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const singlePageInfo = { hasPreviousPage: false, hasNextPage: false, diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js index c6e1846d252..92199896ab4 100644 --- a/spec/frontend/releases/components/releases_sort_spec.js +++ b/spec/frontend/releases/components/releases_sort_spec.js @@ -17,10 +17,6 @@ describe('releases_sort.vue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSorting = () => wrapper.findComponent(GlSorting); const findSortingItems = () => wrapper.findAllComponents(GlSortingItem); const findReleasedDateItem = () => diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js index 8105aa4f6f2..0e896eb645c 100644 --- a/spec/frontend/releases/components/tag_field_exsting_spec.js +++ b/spec/frontend/releases/components/tag_field_exsting_spec.js @@ -37,11 +37,6 @@ describe('releases/components/tag_field_existing', () => { }; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('default', () => { it('shows the tag name', () => { createComponent(); diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js index fcba0da3462..2508495429c 100644 --- a/spec/frontend/releases/components/tag_field_new_spec.js +++ b/spec/frontend/releases/components/tag_field_new_spec.js @@ -9,6 +9,7 @@ import { __ } from '~/locale'; import TagFieldNew from '~/releases/components/tag_field_new.vue'; import createStore from '~/releases/stores'; import createEditNewModule from '~/releases/stores/modules/edit_new'; +import { i18n } from '~/releases/constants'; const TEST_TAG_NAME = 'test-tag-name'; const TEST_TAG_MESSAGE = 'Test tag message'; @@ -81,7 +82,6 @@ describe('releases/components/tag_field_new', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); @@ -210,9 +210,7 @@ describe('releases/components/tag_field_new', () => { store.state.editNew.existingRelease = {}; await expectValidationMessageToBe('shown'); - expect(findTagNameFormGroup().text()).toContain( - __('Selected tag is already in use. Choose another option.'), - ); + expect(findTagNameFormGroup().text()).toContain(i18n.tagIsAlredyInUseMessage); }); }); @@ -222,7 +220,7 @@ describe('releases/components/tag_field_new', () => { findTagNameDropdown().vm.$emit('hide'); await expectValidationMessageToBe('shown'); - expect(findTagNameFormGroup().text()).toContain(__('Tag name is required.')); + expect(findTagNameFormGroup().text()).toContain(i18n.tagNameIsRequiredMessage); }); }); }); diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js index 85a40f02c53..8509c347291 100644 --- a/spec/frontend/releases/components/tag_field_spec.js +++ b/spec/frontend/releases/components/tag_field_spec.js @@ -24,11 +24,6 @@ describe('releases/components/tag_field', () => { const findTagFieldNew = () => wrapper.findComponent(TagFieldNew); const findTagFieldExisting = () => wrapper.findComponent(TagFieldExisting); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when an existing release is being edited', () => { beforeEach(() => { createComponent({ isExistingRelease: true }); diff --git a/spec/frontend/releases/release_notification_service_spec.js b/spec/frontend/releases/release_notification_service_spec.js index 2344d4b929a..a90bfa3dcbd 100644 --- a/spec/frontend/releases/release_notification_service_spec.js +++ b/spec/frontend/releases/release_notification_service_spec.js @@ -2,9 +2,9 @@ import { popCreateReleaseNotification, putCreateReleaseNotification, } from '~/releases/release_notification_service'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('~/releases/release_notification_service', () => { const projectPath = 'test-project-path'; @@ -35,7 +35,7 @@ describe('~/releases/release_notification_service', () => { expect(item).toBe(null); }); - it('should create a flash message', () => { + it('should create an alert message', () => { expect(createAlert).toHaveBeenCalledTimes(1); expect(createAlert).toHaveBeenCalledWith({ message: `Release ${releaseName} has been successfully created.`, @@ -49,7 +49,7 @@ describe('~/releases/release_notification_service', () => { popCreateReleaseNotification(projectPath); }); - it('should not create a flash message', () => { + it('should not create an alert message', () => { expect(createAlert).toHaveBeenCalledTimes(0); }); }); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index ca3b2d5f734..2fca3396a1f 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'; import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json'; import testAction from 'helpers/vuex_action_helper'; import { getTag } from '~/api/tags_api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import { ASSET_LINK_TYPE } from '~/releases/constants'; @@ -21,7 +21,7 @@ import { jest.mock('~/api/tags_api'); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/releases/release_notification_service'); @@ -154,7 +154,7 @@ describe('Release edit/new actions', () => { ]); }); - it(`shows a flash message`, () => { + it(`shows an alert message`, () => { return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => { expect(createAlert).toHaveBeenCalledTimes(1); expect(createAlert).toHaveBeenCalledWith({ @@ -380,7 +380,7 @@ describe('Release edit/new actions', () => { ]); }); - it(`shows a flash message`, () => { + it(`shows an alert message`, () => { return actions .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} }) .then(() => { @@ -406,7 +406,7 @@ describe('Release edit/new actions', () => { ]); }); - it(`shows a flash message`, () => { + it(`shows an alert message`, () => { return actions .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} }) .then(() => { @@ -538,7 +538,7 @@ describe('Release edit/new actions', () => { expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]); }); - it('shows a flash message', async () => { + it('shows an alert message', async () => { await actions.updateRelease({ commit, dispatch, state, getters }); expect(createAlert).toHaveBeenCalledTimes(1); @@ -558,7 +558,7 @@ describe('Release edit/new actions', () => { ]); }); - it('shows a flash message', async () => { + it('shows an alert message', async () => { await actions.updateRelease({ commit, dispatch, state, getters }); expect(createAlert).toHaveBeenCalledTimes(1); @@ -711,7 +711,7 @@ describe('Release edit/new actions', () => { expect(commit.mock.calls).toContainEqual([types.RECEIVE_SAVE_RELEASE_ERROR, error]); }); - it('shows a flash message', async () => { + it('shows an alert message', async () => { await actions.deleteRelease({ commit, dispatch, state, getters }); expect(createAlert).toHaveBeenCalledTimes(1); @@ -747,7 +747,7 @@ describe('Release edit/new actions', () => { ]); }); - it('shows a flash message', async () => { + it('shows an alert message', async () => { await actions.deleteRelease({ commit, dispatch, state, getters }); expect(createAlert).toHaveBeenCalledTimes(1); @@ -778,7 +778,7 @@ describe('Release edit/new actions', () => { expect(getTag).toHaveBeenCalledWith(state.projectId, tagName); }); - it('creates a flash on error', async () => { + it('creates an alert on error', async () => { error = new Error(); getTag.mockRejectedValue(error); diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js index f8b87ec71dc..649e772f956 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -1,5 +1,16 @@ import { s__ } from '~/locale'; import * as getters from '~/releases/stores/modules/edit_new/getters'; +import { i18n } from '~/releases/constants'; +import { validateTag, ValidationResult } from '~/lib/utils/ref_validator'; + +jest.mock('~/lib/utils/ref_validator', () => { + const original = jest.requireActual('~/lib/utils/ref_validator'); + return { + __esModule: true, + ValidationResult: original.ValidationResult, + validateTag: jest.fn(() => new original.ValidationResult()), + }; +}); describe('Release edit/new getters', () => { describe('releaseLinksToCreate', () => { @@ -59,23 +70,23 @@ describe('Release edit/new getters', () => { }); describe('validationErrors', () => { + const validState = { + release: { + tagName: 'test-tag-name', + assets: { + links: [ + { id: 1, url: 'https://example.com/valid', name: 'Link 1' }, + { id: 2, url: '', name: '' }, + { id: 3, url: '', name: ' ' }, + { id: 4, url: ' ', name: '' }, + { id: 5, url: ' ', name: ' ' }, + ], + }, + }, + }; describe('when the form is valid', () => { + const state = validState; it('returns no validation errors', () => { - const state = { - release: { - tagName: 'test-tag-name', - assets: { - links: [ - { id: 1, url: 'https://example.com/valid', name: 'Link 1' }, - { id: 2, url: '', name: '' }, - { id: 3, url: '', name: ' ' }, - { id: 4, url: ' ', name: '' }, - { id: 5, url: ' ', name: ' ' }, - ], - }, - }, - }; - const expectedErrors = { assets: { links: { @@ -88,7 +99,27 @@ describe('Release edit/new getters', () => { }, }; - expect(getters.validationErrors(state)).toEqual(expectedErrors); + expect(getters.validationErrors(state).assets).toEqual(expectedErrors.assets); + expect(getters.validationErrors(state).tagNameValidation.isValid).toBe(true); + }); + }); + + describe('when validating tag', () => { + const state = validState; + it('validateTag is called with right parameters', () => { + getters.validationErrors(state); + expect(validateTag).toHaveBeenCalledWith(state.release.tagName); + }); + + it('validation error is correctly returned', () => { + const validationError = new ValidationResult(); + const errorText = 'Tag format validation error'; + validationError.addValidationError(errorText); + validateTag.mockReturnValue(validationError); + + const result = getters.validationErrors(state); + expect(validateTag).toHaveBeenCalledWith(state.release.tagName); + expect(result.tagNameValidation.validationErrors).toContain(errorText); }); }); @@ -140,19 +171,17 @@ describe('Release edit/new getters', () => { }); it('returns a validation error if the tag name is empty', () => { - const expectedErrors = { - isTagNameEmpty: true, - }; - - expect(actualErrors).toMatchObject(expectedErrors); + expect(actualErrors.tagNameValidation.isValid).toBe(false); + expect(actualErrors.tagNameValidation.validationErrors).toContain( + i18n.tagNameIsRequiredMessage, + ); }); it('returns a validation error if the tag has an existing release', () => { - const expectedErrors = { - existingRelease: true, - }; - - expect(actualErrors).toMatchObject(expectedErrors); + expect(actualErrors.tagNameValidation.isValid).toBe(false); + expect(actualErrors.tagNameValidation.validationErrors).toContain( + i18n.tagIsAlredyInUseMessage, + ); }); it('returns a validation error if links share a URL', () => { diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js index e56975d021a..22ef552c2f9 100644 --- a/spec/frontend/repository/commits_service_spec.js +++ b/spec/frontend/repository/commits_service_spec.js @@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants'; import { refWithSpecialCharMock } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('commits service', () => { let mock; diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js index 33a85c04fcf..96dedd54126 100644 --- a/spec/frontend/repository/components/blob_button_group_spec.js +++ b/spec/frontend/repository/components/blob_button_group_spec.js @@ -38,10 +38,6 @@ describe('BlobButtonGroup component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); const findDeleteButton = () => wrapper.findByTestId('delete'); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index 03a8ee6ac5d..a588251c4bd 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -175,7 +175,6 @@ describe('Blob content viewer component', () => { }); afterEach(() => { - wrapper.destroy(); mockAxios.reset(); }); @@ -482,11 +481,6 @@ describe('Blob content viewer component', () => { repository: { empty }, } = projectMock; - afterEach(() => { - delete gon.current_user_id; - delete gon.current_username; - }); - it('renders component', async () => { window.gon.current_user_id = 1; window.gon.current_username = 'root'; diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js index 0d52542397f..3ced5f6c4d2 100644 --- a/spec/frontend/repository/components/blob_controls_spec.js +++ b/spec/frontend/repository/components/blob_controls_spec.js @@ -50,8 +50,6 @@ describe('Blob controls component', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - it('renders a find button with the correct href', () => { expect(findFindButton().attributes('href')).toBe('find/file.js'); }); diff --git a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js index 599443bf862..b4f4b0058de 100644 --- a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js +++ b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js @@ -21,8 +21,6 @@ describe('LFS Viewer', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - it('renders the correct text', () => { expect(wrapper.text()).toBe( 'This content could not be displayed because it is stored in LFS. You can download it instead.', diff --git a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js index 51f3d31ec72..5d37692bf90 100644 --- a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js +++ b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js @@ -1,7 +1,6 @@ -import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NotebookViewer from '~/repository/components/blob_viewers/notebook_viewer.vue'; -import notebookLoader from '~/blob/notebook'; +import Notebook from '~/blob/notebook/notebook_viewer.vue'; jest.mock('~/blob/notebook'); @@ -17,24 +16,11 @@ describe('Notebook Viewer', () => { }); }; - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findNotebookWrapper = () => wrapper.findByTestId('notebook'); + const findNotebook = () => wrapper.findComponent(Notebook); beforeEach(() => createComponent()); - it('calls the notebook loader', () => { - expect(notebookLoader).toHaveBeenCalledWith({ - el: wrapper.vm.$refs.viewer, - relativeRawPath: ROOT_RELATIVE_PATH, - }); - }); - - it('renders a loading icon component', () => { - expect(findLoadingIcon().props('size')).toBe('lg'); - }); - - it('renders the notebook wrapper', () => { - expect(findNotebookWrapper().exists()).toBe(true); - expect(findNotebookWrapper().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath); + it('renders a Notebook component', () => { + expect(findNotebook().props('endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath); }); }); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index c2f34f79f89..8b7a7d91125 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -42,10 +42,6 @@ describe('Repository breadcrumbs component', () => { const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal); - afterEach(() => { - wrapper.destroy(); - }); - it.each` path | linkCount ${'/'} | ${1} diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js index b5996816ad8..9ca45bfb655 100644 --- a/spec/frontend/repository/components/delete_blob_modal_spec.js +++ b/spec/frontend/repository/components/delete_blob_modal_spec.js @@ -49,10 +49,6 @@ describe('DeleteBlobModal', () => { await findCommitTextarea().vm.$emit('input', commitText); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders Modal component', () => { createComponent(); @@ -187,7 +183,7 @@ describe('DeleteBlobModal', () => { }); it('disables submit button', async () => { - expect(findModal().props('actionPrimary').attributes[0]).toEqual( + expect(findModal().props('actionPrimary').attributes).toEqual( expect.objectContaining({ disabled: true }), ); }); @@ -207,7 +203,7 @@ describe('DeleteBlobModal', () => { }); it('enables submit button', async () => { - expect(findModal().props('actionPrimary').attributes[0]).toEqual( + expect(findModal().props('actionPrimary').attributes).toEqual( expect.objectContaining({ disabled: false }), ); }); diff --git a/spec/frontend/repository/components/directory_download_links_spec.js b/spec/frontend/repository/components/directory_download_links_spec.js index 72c4165c2e9..3739829c759 100644 --- a/spec/frontend/repository/components/directory_download_links_spec.js +++ b/spec/frontend/repository/components/directory_download_links_spec.js @@ -16,10 +16,6 @@ function factory(currentPath) { } describe('Repository directory download links component', () => { - afterEach(() => { - vm.destroy(); - }); - it.each` path ${'app'} diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js index f327a8cfae7..7a2b03a8d8f 100644 --- a/spec/frontend/repository/components/fork_info_spec.js +++ b/spec/frontend/repository/components/fork_info_spec.js @@ -1,42 +1,75 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf, GlButton, GlLoadingIcon } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import ForkInfo, { i18n } from '~/repository/components/fork_info.vue'; +import ConflictsModal from '~/repository/components/fork_sync_conflicts_modal.vue'; import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql'; +import syncForkMutation from '~/repository/mutations/sync_fork.mutation.graphql'; import { propsForkInfo } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('ForkInfo component', () => { let wrapper; let mockResolver; const forkInfoError = new Error('Something went wrong'); const projectId = 'gid://gitlab/Project/1'; + const showMock = jest.fn(); + const synchronizeFork = true; Vue.use(VueApollo); - const createCommitData = ({ ahead = 3, behind = 7 }) => { + const createForkDetailsData = ( + forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false }, + ) => { return { data: { - project: { id: projectId, forkDetails: { ahead, behind, __typename: 'ForkDetails' } }, + project: { id: projectId, forkDetails }, }, }; }; - const createComponent = (props = {}, data = {}, isRequestFailed = false) => { + const createSyncForkDetailsData = ( + forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false }, + ) => { + return { + data: { + projectSyncFork: { details: forkDetails, errors: [] }, + }, + }; + }; + + const createComponent = (props = {}, data = {}, mutationData = {}, isRequestFailed = false) => { mockResolver = isRequestFailed ? jest.fn().mockRejectedValue(forkInfoError) - : jest.fn().mockResolvedValue(createCommitData(data)); + : jest.fn().mockResolvedValue(createForkDetailsData(data)); wrapper = shallowMountExtended(ForkInfo, { - apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]), + apolloProvider: createMockApollo([ + [forkDetailsQuery, mockResolver], + [syncForkMutation, jest.fn().mockResolvedValue(createSyncForkDetailsData(mutationData))], + ]), propsData: { ...propsForkInfo, ...props }, - stubs: { GlSprintf }, + stubs: { + GlSprintf, + GlButton, + ConflictsModal: stubComponent(ConflictsModal, { + template: + '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>', + methods: { show: showMock }, + }), + }, + provide: { + glFeatures: { + synchronizeFork, + }, + }, }); return waitForPromises(); }; @@ -44,6 +77,8 @@ describe('ForkInfo component', () => { const findLink = () => wrapper.findComponent(GlLink); const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); const findIcon = () => wrapper.findComponent(GlIcon); + const findUpdateForkButton = () => wrapper.findComponent(GlButton); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findDivergenceMessage = () => wrapper.findByTestId('divergence-message'); const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project'); const findCompareLinks = () => findDivergenceMessage().findAllComponents(GlLink); @@ -87,14 +122,50 @@ describe('ForkInfo component', () => { expect(link.attributes('href')).toBe(propsForkInfo.sourcePath); }); - it('renders unknown divergence message when divergence is unknown', async () => { - await createComponent({}, { ahead: null, behind: null }); - expect(findDivergenceMessage().text()).toBe(i18n.unknown); + describe('Unknown divergence', () => { + beforeEach(async () => { + await createComponent( + {}, + { ahead: null, behind: null, isSyncing: false, hasConflicts: false }, + ); + }); + + it('renders unknown divergence message when divergence is unknown', async () => { + expect(findDivergenceMessage().text()).toBe(i18n.unknown); + }); + + it('renders Update Fork button', async () => { + expect(findUpdateForkButton().exists()).toBe(true); + expect(findUpdateForkButton().text()).toBe(i18n.sync); + }); + }); + + describe('Up to date divergence', () => { + beforeEach(async () => { + await createComponent({}, { ahead: 0, behind: 0, isSyncing: false, hasConflicts: false }); + }); + + it('renders up to date message when fork is up to date', async () => { + expect(findDivergenceMessage().text()).toBe(i18n.upToDate); + }); + + it('does not render Update Fork button', async () => { + expect(findUpdateForkButton().exists()).toBe(false); + }); }); - it('renders up to date message when divergence is unknown', async () => { - await createComponent({}, { ahead: 0, behind: 0 }); - expect(findDivergenceMessage().text()).toBe(i18n.upToDate); + describe('Limited visibility project', () => { + beforeEach(async () => { + await createComponent({}, null); + }); + + it('renders limited visibility messsage when forkDetails are empty', async () => { + expect(findDivergenceMessage().text()).toBe(i18n.limitedVisibility); + }); + + it('does not render Update Fork button', async () => { + expect(findUpdateForkButton().exists()).toBe(false); + }); }); describe.each([ @@ -104,6 +175,7 @@ describe('ForkInfo component', () => { message: '3 commits behind, 7 commits ahead of the upstream repository.', firstLink: propsForkInfo.behindComparePath, secondLink: propsForkInfo.aheadComparePath, + hasButton: true, }, { ahead: 7, @@ -111,6 +183,7 @@ describe('ForkInfo component', () => { message: '7 commits ahead of the upstream repository.', firstLink: propsForkInfo.aheadComparePath, secondLink: '', + hasButton: false, }, { ahead: 0, @@ -118,12 +191,13 @@ describe('ForkInfo component', () => { message: '3 commits behind the upstream repository.', firstLink: propsForkInfo.behindComparePath, secondLink: '', + hasButton: true, }, ])( 'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits', - ({ ahead, behind, message, firstLink, secondLink }) => { + ({ ahead, behind, message, firstLink, secondLink, hasButton }) => { beforeEach(async () => { - await createComponent({}, { ahead, behind }); + await createComponent({}, { ahead, behind, isSyncing: false, hasConflicts: false }); }); it('displays correct text', () => { @@ -138,9 +212,38 @@ describe('ForkInfo component', () => { expect(links.at(1).attributes('href')).toBe(secondLink); } }); + + it('renders Update Fork button when fork is behind', () => { + expect(findUpdateForkButton().exists()).toBe(hasButton); + if (hasButton) { + expect(findUpdateForkButton().text()).toBe(i18n.sync); + } + }); }, ); + describe('when sync is not possible due to conflicts', () => { + it('opens Conflicts Modal', async () => { + await createComponent({}, { ahead: 7, behind: 3, isSyncing: false, hasConflicts: true }); + findUpdateForkButton().vm.$emit('click'); + expect(showMock).toHaveBeenCalled(); + }); + }); + + describe('projectSyncFork mutation', () => { + it('changes button to have loading state', async () => { + await createComponent( + {}, + { ahead: 0, behind: 3, isSyncing: false, hasConflicts: false }, + { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false }, + ); + expect(findLoadingIcon().exists()).toBe(false); + findUpdateForkButton().vm.$emit('click'); + await waitForPromises(); + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + it('renders alert with error message when request fails', async () => { await createComponent({}, {}, true); expect(createAlert).toHaveBeenCalledWith({ diff --git a/spec/frontend/repository/components/fork_suggestion_spec.js b/spec/frontend/repository/components/fork_suggestion_spec.js index 36a48a3fdb8..a9e5c18c0a9 100644 --- a/spec/frontend/repository/components/fork_suggestion_spec.js +++ b/spec/frontend/repository/components/fork_suggestion_spec.js @@ -14,8 +14,6 @@ describe('ForkSuggestion component', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - const { i18n } = ForkSuggestion; const findMessage = () => wrapper.findByTestId('message'); const findForkButton = () => wrapper.findByTestId('fork'); diff --git a/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js new file mode 100644 index 00000000000..f97c970275b --- /dev/null +++ b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js @@ -0,0 +1,42 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import ConflictsModal, { i18n } from '~/repository/components/fork_sync_conflicts_modal.vue'; +import { propsConflictsModal } from '../mock_data'; + +describe('ConflictsModal', () => { + let wrapper; + + function createComponent({ props = {} } = {}) { + wrapper = shallowMount(ConflictsModal, { + propsData: props, + stubs: { GlModal }, + }); + } + + beforeEach(() => { + createComponent({ props: propsConflictsModal }); + }); + + const findModal = () => wrapper.findComponent(GlModal); + const findInstructions = () => wrapper.findAll('[ data-testid="resolve-conflict-instructions"]'); + + it('renders a modal', () => { + expect(findModal().exists()).toBe(true); + }); + + it('passes title as a prop to a gl-modal component', () => { + expect(findModal().props().title).toBe(i18n.modalTitle); + }); + + it('renders a selection of markdown fields', () => { + expect(findInstructions().length).toBe(3); + }); + + it('renders a source url in a first intruction', () => { + expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourcePath); + }); + + it('renders default branch name in a first intruction', () => { + expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourceDefaultBranch); + }); +}); diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js index 7226e7baa36..f16edcb0b7c 100644 --- a/spec/frontend/repository/components/last_commit_spec.js +++ b/spec/frontend/repository/components/last_commit_spec.js @@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import LastCommit from '~/repository/components/last_commit.vue'; +import SignatureBadge from '~/commit/components/signature_badge.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql'; import { refMock } from '../mock_data'; @@ -20,7 +21,7 @@ const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink); const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findCommitRowDescription = () => wrapper.find('.commit-row-description'); -const findStatusBox = () => wrapper.find('.signature-badge'); +const findStatusBox = () => wrapper.findComponent(SignatureBadge); const findItemTitle = () => wrapper.find('.item-title'); const defaultPipelineEdges = [ @@ -56,7 +57,7 @@ const createCommitData = ({ pipelineEdges = defaultPipelineEdges, author = defaultAuthor, descriptionHtml = '', - signatureHtml = null, + signature = null, message = defaultMessage, }) => { return { @@ -84,7 +85,7 @@ const createCommitData = ({ authorName: 'Test', authorGravatar: 'https://test.com', author, - signatureHtml, + signature, pipelines: { __typename: 'PipelineConnection', edges: pipelineEdges, @@ -110,11 +111,13 @@ const createComponent = async (data = {}) => { apolloProvider: createMockApollo([[pathLastCommitQuery, mockResolver]]), propsData: { currentPath }, mixins: [{ data: () => ({ ref: refMock }) }], + stubs: { + SignatureBadge, + }, }); }; afterEach(() => { - wrapper.destroy(); mockResolver = null; }); @@ -204,23 +207,19 @@ describe('Repository last commit component', () => { }); it('renders the signature HTML as returned by the backend', async () => { + const signatureResponse = { + __typename: 'GpgSignature', + gpgKeyPrimaryKeyid: 'xxx', + verificationStatus: 'VERIFIED', + }; createComponent({ - signatureHtml: `<a - class="btn signature-badge" - data-content="signature-content" - data-html="true" - data-placement="top" - data-title="signature-title" - data-toggle="popover" - role="button" - tabindex="0" - ><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`, + signature: { + ...signatureResponse, + }, }); await waitForPromises(); - expect(findStatusBox().html()).toBe( - `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0"><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`, - ); + expect(findStatusBox().props()).toMatchObject({ signature: signatureResponse }); }); it('sets correct CSS class if the commit message is empty', async () => { diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js index 4e5c9a685c4..c920159375f 100644 --- a/spec/frontend/repository/components/new_directory_modal_spec.js +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -4,12 +4,12 @@ import { nextTick } from 'vue'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), })); @@ -76,10 +76,6 @@ describe('NewDirectoryModal', () => { await waitForPromises(); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders modal component', () => { createComponent(); @@ -185,10 +181,10 @@ describe('NewDirectoryModal', () => { it('disables submit button', async () => { await fillForm({ dirName: '', branchName: '', commitMessage: '' }); - expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true); + expect(findModal().props('actionPrimary').attributes.disabled).toBe(true); }); - it('creates a flash error', async () => { + it('creates an alert error', async () => { mock.onPost(initialProps.path).timeout(); await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); diff --git a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap deleted file mode 100644 index 48a4feca1e5..00000000000 --- a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Repository file preview component renders file HTML 1`] = ` -<article - class="file-holder limited-width-container readme-holder" -> - <div - class="js-file-title file-title-flex-parent" - > - <div - class="file-header-content" - > - <gl-icon-stub - name="doc-text" - size="16" - /> - - <gl-link-stub - href="http://test.com" - > - <strong> - README.md - </strong> - </gl-link-stub> - </div> - </div> - - <div - class="blob-viewer" - data-qa-selector="blob_viewer_content" - itemprop="about" - > - <div> - <div - class="blob" - > - test - </div> - </div> - </div> -</article> -`; diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js index d4c746b67d6..8a88c5b9c61 100644 --- a/spec/frontend/repository/components/preview/index_spec.js +++ b/spec/frontend/repository/components/preview/index_spec.js @@ -1,77 +1,60 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { handleLocationHash } from '~/lib/utils/common_utils'; +import waitForPromises from 'helpers/wait_for_promises'; import Preview from '~/repository/components/preview/index.vue'; +const PROPS_DATA = { + blob: { + webPath: 'http://test.com', + name: 'README.md', + }, +}; + +const MOCK_README_DATA = { + __typename: 'ReadmeFile', + html: '<div class="blob">test</div>', +}; + jest.mock('~/lib/utils/common_utils'); -let vm; -let $apollo; +Vue.use(VueApollo); + +let wrapper; +let mockApollo; +let mockReadmeData; -function factory(blob, loading) { - $apollo = { - queries: { - readme: { - query: jest.fn().mockReturnValue(Promise.resolve({})), - loading, - }, - }, - }; +const mockResolvers = { + Query: { + readme: () => mockReadmeData(), + }, +}; - vm = shallowMount(Preview, { - propsData: { - blob, - }, - mocks: { - $apollo, - }, +function createComponent() { + mockApollo = createMockApollo([], mockResolvers); + + return shallowMount(Preview, { + propsData: PROPS_DATA, + apolloProvider: mockApollo, }); } describe('Repository file preview component', () => { - afterEach(() => { - vm.destroy(); - }); - - it('renders file HTML', async () => { - factory({ - webPath: 'http://test.com', - name: 'README.md', - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - vm.setData({ readme: { html: '<div class="blob">test</div>' } }); - - await nextTick(); - expect(vm.element).toMatchSnapshot(); + beforeEach(() => { + mockReadmeData = jest.fn(); + wrapper = createComponent(); + mockReadmeData.mockResolvedValue(MOCK_README_DATA); }); it('handles hash after render', async () => { - factory({ - webPath: 'http://test.com', - name: 'README.md', - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - vm.setData({ readme: { html: '<div class="blob">test</div>' } }); - - await nextTick(); + await waitForPromises(); expect(handleLocationHash).toHaveBeenCalled(); }); it('renders loading icon', async () => { - factory( - { - webPath: 'http://test.com', - name: 'README.md', - }, - true, - ); - - await nextTick(); - expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index 8b987551b33..f7be367887c 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -88,10 +88,6 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit const findTableRows = () => vm.findAllComponents(TableRow); describe('Repository table component', () => { - afterEach(() => { - vm.destroy(); - }); - it.each` path | ref ${'/'} | ${'main'} diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js index 03fb4242e40..77822a148b7 100644 --- a/spec/frontend/repository/components/table/parent_row_spec.js +++ b/spec/frontend/repository/components/table/parent_row_spec.js @@ -26,10 +26,6 @@ function factory(path, loadingPath) { } describe('Repository parent row component', () => { - afterEach(() => { - vm.destroy(); - }); - it.each` path | to ${'app'} | ${'/-/tree/main/'} diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 5d9138ab9cd..055616d6e8e 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -28,7 +28,7 @@ function factory(propsData = {}) { rowNumber: 123, }, directives: { - GlHoverLoad: createMockDirective(), + GlHoverLoad: createMockDirective('gl-hover-load'), }, mocks: { $router, @@ -47,10 +47,6 @@ describe('Repository table row component', () => { const findRouterLink = () => vm.findComponent(RouterLinkStub); const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver); - afterEach(() => { - vm.destroy(); - }); - it('renders table row', async () => { factory({ id: '1', diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js index f694c8e9166..9597d8a7b77 100644 --- a/spec/frontend/repository/components/tree_content_spec.js +++ b/spec/frontend/repository/components/tree_content_spec.js @@ -1,12 +1,11 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql'; import FilePreview from '~/repository/components/preview/index.vue'; import FileTable from '~/repository/components/table/index.vue'; import TreeContent from 'jh_else_ce/repository/components/tree_content.vue'; import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { i18n } from '~/repository/constants'; import { graphQLErrors } from '../mock_data'; @@ -15,15 +14,15 @@ jest.mock('~/repository/commits_service', () => ({ isRequested: jest.fn(), resetRequestedCommits: jest.fn(), })); -jest.mock('~/flash'); +jest.mock('~/alert'); let vm; let $apollo; const mockResponse = jest.fn().mockReturnValue(Promise.resolve({ data: {} })); -function factory(path, appoloMockResponse = mockResponse) { +function factory(path, apolloMockResponse = mockResponse) { $apollo = { - query: appoloMockResponse, + query: apolloMockResponse, }; vm = shallowMount(TreeContent, { @@ -33,22 +32,12 @@ function factory(path, appoloMockResponse = mockResponse) { mocks: { $apollo, }, - provide: { - glFeatures: { - increasePageSizeExponentially: true, - paginatedTreeGraphqlQuery: true, - }, - }, }); } describe('Repository table component', () => { const findFileTable = () => vm.findComponent(FileTable); - afterEach(() => { - vm.destroy(); - }); - it('renders file preview', async () => { factory('/'); @@ -171,37 +160,6 @@ describe('Repository table component', () => { expect(findFileTable().props('hasMore')).toBe(limitReached); }); - - it.each` - fetchCounter | pageSize - ${0} | ${10} - ${2} | ${30} - ${4} | ${50} - ${6} | ${70} - ${8} | ${90} - ${10} | ${100} - ${20} | ${100} - ${100} | ${100} - ${200} | ${100} - `('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => { - factory('/'); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - vm.setData({ fetchCounter }); - - vm.vm.fetchFiles(); - - expect($apollo.query).toHaveBeenCalledWith({ - query: paginatedTreeQuery, - variables: { - pageSize, - nextPageCursor: '', - path: '/', - projectPath: '', - ref: '', - }, - }); - }); }); describe('commit data', () => { diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js index 9de0666f27a..319321cfcb4 100644 --- a/spec/frontend/repository/components/upload_blob_modal_spec.js +++ b/spec/frontend/repository/components/upload_blob_modal_spec.js @@ -4,13 +4,13 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), joinPaths: () => '/new_upload', @@ -53,14 +53,9 @@ describe('UploadBlobModal', () => { const findBranchName = () => wrapper.findComponent(GlFormInput); const findMrToggle = () => wrapper.findComponent(GlToggle); const findUploadDropzone = () => wrapper.findComponent(UploadDropzone); - const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled; - const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled; - const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); + const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes.disabled; + const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes.disabled; + const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes.loading; describe.each` canPushCode | displayBranchName | displayForkedBranchMessage @@ -110,9 +105,7 @@ describe('UploadBlobModal', () => { if (canPushCode) { describe('when changing the branch name', () => { it('displays the MR toggle', 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({ target: 'Not main' }); + createComponent({ targetBranch: 'Not main' }); await nextTick(); @@ -123,12 +116,10 @@ describe('UploadBlobModal', () => { describe('completed form', () => { beforeEach(() => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - file: { type: 'jpg' }, - filePreviewURL: 'http://file.com?format=jpg', - }); + findUploadDropzone().vm.$emit( + 'change', + new File(['http://file.com?format=jpg'], 'file.jpg'), + ); }); it('enables the upload button when the form is completed', () => { @@ -184,7 +175,7 @@ describe('UploadBlobModal', () => { await waitForPromises(); }); - it('creates a flash error', () => { + it('creates an alert error', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'Error uploading file. Please try again.', }); @@ -199,13 +190,6 @@ describe('UploadBlobModal', () => { ); describe('blob file submission type', () => { - const submitForm = async () => { - wrapper.vm.uploadFile = jest.fn(); - wrapper.vm.replaceFile = jest.fn(); - wrapper.vm.submitForm(); - await nextTick(); - }; - const submitRequest = async () => { mock = new MockAdapter(axios); findModal().vm.$emit('primary', mockEvent); @@ -225,13 +209,6 @@ describe('UploadBlobModal', () => { expect(findModal().props('actionPrimary').text).toBe('Upload file'); }); - it('calls the default uploadFile when the form submit', async () => { - await submitForm(); - - expect(wrapper.vm.uploadFile).toHaveBeenCalled(); - expect(wrapper.vm.replaceFile).not.toHaveBeenCalled(); - }); - it('makes a POST request', async () => { await submitRequest(); @@ -261,13 +238,6 @@ describe('UploadBlobModal', () => { expect(findModal().props('actionPrimary').text).toBe(primaryBtnText); }); - it('calls the replaceFile when the form submit', async () => { - await submitForm(); - - expect(wrapper.vm.replaceFile).toHaveBeenCalled(); - expect(wrapper.vm.uploadFile).not.toHaveBeenCalled(); - }); - it('makes a PUT request', async () => { await submitRequest(); diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js index 7c48fe440d2..5f872749581 100644 --- a/spec/frontend/repository/mixins/highlight_mixin_spec.js +++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js @@ -44,8 +44,6 @@ describe('HighlightMixin', () => { beforeEach(() => createComponent()); - afterEach(() => wrapper.destroy()); - describe('initHighlightWorker', () => { const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n'); diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index 04ffe52bc3f..418a93a10cc 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -126,3 +126,9 @@ export const propsForkInfo = { aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1', behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2', }; + +export const propsConflictsModal = { + sourceDefaultBranch: 'branch-name', + sourceName: 'source-name', + sourcePath: 'path/to/project', +}; diff --git a/spec/frontend/repository/pages/blob_spec.js b/spec/frontend/repository/pages/blob_spec.js index 4fe6188370e..366523e2b8b 100644 --- a/spec/frontend/repository/pages/blob_spec.js +++ b/spec/frontend/repository/pages/blob_spec.js @@ -16,10 +16,6 @@ describe('Repository blob page component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('has a Blob Content Viewer component', () => { expect(findBlobContentViewer().exists()).toBe(true); expect(findBlobContentViewer().props('path')).toBe(path); diff --git a/spec/frontend/repository/pages/index_spec.js b/spec/frontend/repository/pages/index_spec.js index 559257d414c..e50557e7d61 100644 --- a/spec/frontend/repository/pages/index_spec.js +++ b/spec/frontend/repository/pages/index_spec.js @@ -13,8 +13,6 @@ describe('Repository index page component', () => { } afterEach(() => { - wrapper.destroy(); - updateElementsVisibility.mockClear(); }); diff --git a/spec/frontend/repository/pages/tree_spec.js b/spec/frontend/repository/pages/tree_spec.js index 36662696c91..b1529d77c7d 100644 --- a/spec/frontend/repository/pages/tree_spec.js +++ b/spec/frontend/repository/pages/tree_spec.js @@ -12,8 +12,6 @@ describe('Repository tree page component', () => { } afterEach(() => { - wrapper.destroy(); - updateElementsVisibility.mockClear(); }); diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap index 3abdfcdaf20..204afc744e7 100644 --- a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap +++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap @@ -7,9 +7,39 @@ exports[`Saved replies list item component renders list item 1`] = ` <div class="gl-display-flex gl-align-items-center" > - <strong> + <strong + data-testid="saved-reply-name" + > test </strong> + + <div + class="gl-ml-auto" + > + <gl-button-stub + aria-label="Edit" + buttontextclasses="" + category="primary" + class="gl-mr-3" + data-testid="saved-reply-edit-btn" + icon="pencil" + size="medium" + title="Edit" + to="[object Object]" + variant="default" + /> + + <gl-button-stub + aria-label="Delete" + buttontextclasses="" + category="secondary" + data-testid="saved-reply-delete-btn" + icon="remove" + size="medium" + title="Delete" + variant="danger" + /> + </div> </div> <div @@ -17,5 +47,21 @@ exports[`Saved replies list item component renders list item 1`] = ` > /assign_reviewer </div> + + <gl-modal-stub + actionprimary="[object Object]" + actionsecondary="[object Object]" + arialabel="" + dismisslabel="Close" + modalclass="" + modalid="delete-saved-reply-2" + size="sm" + title="Delete saved reply" + titletag="h4" + > + <gl-sprintf-stub + message="Are you sure you want to delete %{name}? This action cannot be undone." + /> + </gl-modal-stub> </li> `; diff --git a/spec/frontend/saved_replies/components/form_spec.js b/spec/frontend/saved_replies/components/form_spec.js new file mode 100644 index 00000000000..adeda498e6f --- /dev/null +++ b/spec/frontend/saved_replies/components/form_spec.js @@ -0,0 +1,144 @@ +import Vue, { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createdSavedReplyResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply.mutation.graphql.json'; +import createdSavedReplyErrorResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply_with_errors.mutation.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import Form from '~/saved_replies/components/form.vue'; +import createSavedReplyMutation from '~/saved_replies/queries/create_saved_reply.mutation.graphql'; +import updateSavedReplyMutation from '~/saved_replies/queries/update_saved_reply.mutation.graphql'; + +let wrapper; +let createSavedReplyResponseSpy; +let updateSavedReplyResponseSpy; + +function createMockApolloProvider(response) { + Vue.use(VueApollo); + + createSavedReplyResponseSpy = jest.fn().mockResolvedValue(response); + updateSavedReplyResponseSpy = jest.fn().mockResolvedValue(response); + + const requestHandlers = [ + [createSavedReplyMutation, createSavedReplyResponseSpy], + [updateSavedReplyMutation, updateSavedReplyResponseSpy], + ]; + + return createMockApollo(requestHandlers); +} + +function createComponent(id = null, response = createdSavedReplyResponse) { + const mockApollo = createMockApolloProvider(response); + + return mount(Form, { + propsData: { + id, + }, + apolloProvider: mockApollo, + }); +} + +const findSavedReplyNameInput = () => wrapper.find('[data-testid="saved-reply-name-input"]'); +const findSavedReplyNameFormGroup = () => + wrapper.find('[data-testid="saved-reply-name-form-group"]'); +const findSavedReplyContentInput = () => wrapper.find('[data-testid="saved-reply-content-input"]'); +const findSavedReplyContentFormGroup = () => + wrapper.find('[data-testid="saved-reply-content-form-group"]'); +const findSavedReplyFrom = () => wrapper.find('[data-testid="saved-reply-form"]'); +const findAlerts = () => wrapper.findAllComponents(GlAlert); +const findSubmitBtn = () => wrapper.find('[data-testid="saved-reply-form-submit-btn"]'); + +describe('Saved replies form component', () => { + describe('create saved reply', () => { + it('calls apollo mutation', async () => { + wrapper = createComponent(); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(createSavedReplyResponseSpy).toHaveBeenCalledWith({ + id: null, + content: 'Test content', + name: 'Test', + }); + }); + + it('does not submit when form validation fails', async () => { + wrapper = createComponent(); + + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(createSavedReplyResponseSpy).not.toHaveBeenCalled(); + }); + + it.each` + findFormGroup | findInput | fieldName + ${findSavedReplyNameFormGroup} | ${findSavedReplyContentInput} | ${'name'} + ${findSavedReplyContentFormGroup} | ${findSavedReplyNameInput} | ${'content'} + `('shows errors for empty $fieldName input', async ({ findFormGroup, findInput }) => { + wrapper = createComponent(null, createdSavedReplyErrorResponse); + + findInput().setValue('Test'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(findFormGroup().classes('is-invalid')).toBe(true); + }); + + it('displays errors when mutation fails', async () => { + wrapper = createComponent(null, createdSavedReplyErrorResponse); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + const { errors } = createdSavedReplyErrorResponse; + const alertMessages = findAlerts().wrappers.map((x) => x.text()); + + expect(alertMessages).toEqual(errors.map((x) => x.message)); + }); + + it('shows loading state when saving', async () => { + wrapper = createComponent(); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await nextTick(); + + expect(findSubmitBtn().props('loading')).toBe(true); + + await waitForPromises(); + + expect(findSubmitBtn().props('loading')).toBe(false); + }); + }); + + describe('updates saved reply', () => { + it('calls apollo mutation', async () => { + wrapper = createComponent('1'); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(updateSavedReplyResponseSpy).toHaveBeenCalledWith({ + id: '1', + content: 'Test content', + name: 'Test', + }); + }); + }); +}); diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js index cad1000473b..f1ecdfecb15 100644 --- a/spec/frontend/saved_replies/components/list_item_spec.js +++ b/spec/frontend/saved_replies/components/list_item_spec.js @@ -1,22 +1,50 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createMockDirective } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; import ListItem from '~/saved_replies/components/list_item.vue'; +import deleteSavedReplyMutation from '~/saved_replies/queries/delete_saved_reply.mutation.graphql'; let wrapper; +let deleteSavedReplyMutationResponse; function createComponent(propsData = {}) { + Vue.use(VueApollo); + + deleteSavedReplyMutationResponse = jest + .fn() + .mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } }); + return shallowMount(ListItem, { propsData, + directives: { + GlModal: createMockDirective('gl-modal'), + }, + apolloProvider: createMockApollo([ + [deleteSavedReplyMutation, deleteSavedReplyMutationResponse], + ]), }); } describe('Saved replies list item component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('renders list item', async () => { wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } }); expect(wrapper.element).toMatchSnapshot(); }); + + describe('delete button', () => { + it('calls Apollo mutate', async () => { + wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer', id: 1 } }); + + wrapper.findComponent(GlModal).vm.$emit('primary'); + + await waitForPromises(); + + expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 }); + }); + }); }); diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js index 66e9ddfe148..bc69cb852e0 100644 --- a/spec/frontend/saved_replies/components/list_spec.js +++ b/spec/frontend/saved_replies/components/list_spec.js @@ -1,61 +1,39 @@ -import Vue from 'vue'; import { mount } from '@vue/test-utils'; -import VueApollo from 'vue-apollo'; import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json'; import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; import List from '~/saved_replies/components/list.vue'; import ListItem from '~/saved_replies/components/list_item.vue'; -import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql'; let wrapper; -function createMockApolloProvider(response) { - Vue.use(VueApollo); - - const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]]; - - return createMockApollo(requestHandlers); -} - -function createComponent(options = {}) { - const { mockApollo } = options; +function createComponent(res = {}) { + const { savedReplies } = res.data.currentUser; return mount(List, { - apolloProvider: mockApollo, + propsData: { + savedReplies: savedReplies.nodes, + pageInfo: savedReplies.pageInfo, + count: savedReplies.count, + }, }); } describe('Saved replies list component', () => { - afterEach(() => { - wrapper.destroy(); - }); - - it('does not render any list items when response is empty', async () => { - const mockApollo = createMockApolloProvider(noSavedRepliesResponse); - wrapper = createComponent({ mockApollo }); - - await waitForPromises(); + it('does not render any list items when response is empty', () => { + wrapper = createComponent(noSavedRepliesResponse); expect(wrapper.findAllComponents(ListItem).length).toBe(0); }); - it('render saved replies count', async () => { - const mockApollo = createMockApolloProvider(savedRepliesResponse); - wrapper = createComponent({ mockApollo }); - - await waitForPromises(); + it('render saved replies count', () => { + wrapper = createComponent(savedRepliesResponse); expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)'); }); - it('renders list of saved replies', async () => { - const mockApollo = createMockApolloProvider(savedRepliesResponse); + it('renders list of saved replies', () => { const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes; - wrapper = createComponent({ mockApollo }); - - await waitForPromises(); + wrapper = createComponent(savedRepliesResponse); expect(wrapper.findAllComponents(ListItem).length).toBe(2); expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual( diff --git a/spec/frontend/saved_replies/pages/index_spec.js b/spec/frontend/saved_replies/pages/index_spec.js new file mode 100644 index 00000000000..771025d64ec --- /dev/null +++ b/spec/frontend/saved_replies/pages/index_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import IndexPage from '~/saved_replies/pages/index.vue'; +import ListItem from '~/saved_replies/components/list_item.vue'; +import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql'; + +let wrapper; + +function createMockApolloProvider(response) { + Vue.use(VueApollo); + + const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]]; + + return createMockApollo(requestHandlers); +} + +function createComponent(options = {}) { + const { mockApollo } = options; + + return mount(IndexPage, { + apolloProvider: mockApollo, + }); +} + +describe('Saved replies index page component', () => { + it('renders list of saved replies', async () => { + const mockApollo = createMockApolloProvider(savedRepliesResponse); + const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes; + wrapper = createComponent({ mockApollo }); + + await waitForPromises(); + + expect(wrapper.findAllComponents(ListItem).length).toBe(2); + expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual( + expect.objectContaining(savedReplies[0]), + ); + expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual( + expect.objectContaining(savedReplies[1]), + ); + }); +}); diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js index fb9c0a93907..0aa4f0e1c84 100644 --- a/spec/frontend/search/mock_data.js +++ b/spec/frontend/search/mock_data.js @@ -653,3 +653,10 @@ export const TEST_FILTER_DATA = { JSON: { label: 'JSON', value: 'JSON', count: 15 }, }, }; + +export const SMALL_MOCK_AGGREGATIONS = [ + { + name: 'language', + buckets: TEST_RAW_BUCKETS, + }, +]; diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js index 83302b90233..8a35ae96d5e 100644 --- a/spec/frontend/search/sidebar/components/app_spec.js +++ b/spec/frontend/search/sidebar/components/app_spec.js @@ -17,6 +17,10 @@ describe('GlobalSearchSidebar', () => { resetQuery: jest.fn(), }; + const getterSpies = { + currentScope: jest.fn(() => 'issues'), + }; + const createComponent = (initialState, featureFlags) => { const store = new Vuex.Store({ state: { @@ -24,6 +28,7 @@ describe('GlobalSearchSidebar', () => { ...initialState, }, actions: actionSpies, + getters: getterSpies, }); wrapper = shallowMount(GlobalSearchSidebar, { @@ -52,22 +57,23 @@ describe('GlobalSearchSidebar', () => { }); describe.each` - scope | showFilters | ShowsLanguage + scope | showFilters | showsLanguage ${'issues'} | ${true} | ${false} ${'merge_requests'} | ${true} | ${false} ${'projects'} | ${false} | ${false} ${'blobs'} | ${false} | ${true} - `('sidebar scope: $scope', ({ scope, showFilters, ShowsLanguage }) => { + `('sidebar scope: $scope', ({ scope, showFilters, showsLanguage }) => { beforeEach(() => { - createComponent({ urlQuery: { scope } }, { searchBlobsLanguageAggregation: true }); + getterSpies.currentScope = jest.fn(() => scope); + createComponent({ urlQuery: { scope } }); }); it(`${!showFilters ? "doesn't" : ''} shows filters`, () => { expect(findFilters().exists()).toBe(showFilters); }); - it(`${!ShowsLanguage ? "doesn't" : ''} shows language filters`, () => { - expect(findLanguageAggregation().exists()).toBe(ShowsLanguage); + it(`${!showsLanguage ? "doesn't" : ''} shows language filters`, () => { + expect(findLanguageAggregation().exists()).toBe(showsLanguage); }); }); @@ -80,22 +86,4 @@ describe('GlobalSearchSidebar', () => { }); }); }); - - describe('when search_blobs_language_aggregation is enabled', () => { - beforeEach(() => { - createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: true }); - }); - it('shows the language filter', () => { - expect(findLanguageAggregation().exists()).toBe(true); - }); - }); - - describe('when search_blobs_language_aggregation is disabled', () => { - beforeEach(() => { - createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: false }); - }); - it('hides the language filter', () => { - expect(findLanguageAggregation().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js index 82017754b23..f7b35c7bb14 100644 --- a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js +++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js @@ -17,8 +17,12 @@ describe('CheckboxFilter', () => { setQuery: jest.fn(), }; + const getterSpies = { + queryLanguageFilters: jest.fn(() => []), + }; + const defaultProps = { - filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), + filtersData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), }; const createComponent = () => { @@ -27,6 +31,7 @@ describe('CheckboxFilter', () => { query: MOCK_QUERY, }, actions: actionSpies, + getters: getterSpies, }); wrapper = shallowMountExtended(CheckboxFilter, { @@ -73,7 +78,7 @@ describe('CheckboxFilter', () => { describe('actions', () => { it('triggers setQuery', () => { const filter = - defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value; + defaultProps.filtersData.filters[Object.keys(defaultProps.filtersData.filters)[0]].value; findFormCheckboxGroup().vm.$emit('input', filter); expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), { diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js index 7e564bfa005..51c7bdd9609 100644 --- a/spec/frontend/search/sidebar/components/filters_spec.js +++ b/spec/frontend/search/sidebar/components/filters_spec.js @@ -17,6 +17,10 @@ describe('GlobalSearchSidebarFilters', () => { resetQuery: jest.fn(), }; + const defaultGetters = { + currentScope: () => 'issues', + }; + const createComponent = (initialState) => { const store = new Vuex.Store({ state: { @@ -24,6 +28,7 @@ describe('GlobalSearchSidebarFilters', () => { ...initialState, }, actions: actionSpies, + getters: defaultGetters, }); wrapper = shallowMount(ResultsFilters, { @@ -31,10 +36,6 @@ describe('GlobalSearchSidebarFilters', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSidebarForm = () => wrapper.find('form'); const findStatusFilter = () => wrapper.findComponent(StatusFilter); const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter); @@ -142,7 +143,11 @@ describe('GlobalSearchSidebarFilters', () => { ${'blobs'} | ${false} `(`ConfidentialityFilter`, ({ scope, showFilter }) => { beforeEach(() => { - createComponent({ urlQuery: { scope } }); + defaultGetters.currentScope = () => scope; + createComponent(); + }); + afterEach(() => { + defaultGetters.currentScope = () => 'issues'; }); it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => { @@ -162,7 +167,11 @@ describe('GlobalSearchSidebarFilters', () => { ${'blobs'} | ${false} `(`StatusFilter`, ({ scope, showFilter }) => { beforeEach(() => { - createComponent({ urlQuery: { scope } }); + defaultGetters.currentScope = () => scope; + createComponent(); + }); + afterEach(() => { + defaultGetters.currentScope = () => 'issues'; }); it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => { diff --git a/spec/frontend/search/sidebar/components/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filter_spec.js index e297d1c33b0..17656ba749b 100644 --- a/spec/frontend/search/sidebar/components/language_filters_spec.js +++ b/spec/frontend/search/sidebar/components/language_filter_spec.js @@ -22,7 +22,8 @@ describe('GlobalSearchSidebarLanguageFilter', () => { }; const getterSpies = { - langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), + languageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), + queryLanguageFilters: jest.fn(() => []), }; const createComponent = (initialState) => { @@ -48,6 +49,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => { const findForm = () => wrapper.findComponent(GlForm); const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter); const findApplyButton = () => wrapper.findByTestId('apply-button'); + const findResetButton = () => wrapper.findByTestId('reset-button'); const findShowMoreButton = () => wrapper.findByTestId('show-more-button'); const findAlert = () => wrapper.findComponent(GlAlert); const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); @@ -84,6 +86,25 @@ describe('GlobalSearchSidebarLanguageFilter', () => { }); }); + describe('resetButton', () => { + describe.each` + description | sidebarDirty | queryFilters | isDisabled + ${'sidebar dirty only'} | ${true} | ${[]} | ${undefined} + ${'query filters only'} | ${false} | ${['JSON', 'C']} | ${undefined} + ${'sidebar dirty and query filters'} | ${true} | ${['JSON', 'C']} | ${undefined} + ${'no sidebar and no query filters'} | ${false} | ${[]} | ${'true'} + `('$description', ({ sidebarDirty, queryFilters, isDisabled }) => { + beforeEach(() => { + getterSpies.queryLanguageFilters = jest.fn(() => queryFilters); + createComponent({ sidebarDirty, query: { ...MOCK_QUERY, language: queryFilters } }); + }); + + it(`button is ${isDisabled ? 'enabled' : 'disabled'}`, () => { + expect(findResetButton().attributes('disabled')).toBe(isDisabled); + }); + }); + }); + describe('ApplyButton', () => { describe('when sidebarDirty is false', () => { beforeEach(() => { @@ -135,8 +156,8 @@ describe('GlobalSearchSidebarLanguageFilter', () => { createComponent({}); }); - it('uses getter langugageAggregationBuckets', () => { - expect(getterSpies.langugageAggregationBuckets).toHaveBeenCalled(); + it('uses getter languageAggregationBuckets', () => { + expect(getterSpies.languageAggregationBuckets).toHaveBeenCalled(); }); it('uses action fetchLanguageAggregation', () => { diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js index 94d529348a9..47235b828c3 100644 --- a/spec/frontend/search/sidebar/components/radio_filter_spec.js +++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js @@ -16,6 +16,10 @@ describe('RadioFilter', () => { setQuery: jest.fn(), }; + const defaultGetters = { + currentScope: jest.fn(() => 'issues'), + }; + const defaultProps = { filterData: stateFilterData, }; @@ -27,6 +31,7 @@ describe('RadioFilter', () => { ...initialState, }, actions: actionSpies, + getters: defaultGetters, }); wrapper = shallowMount(RadioFilter, { @@ -38,11 +43,6 @@ describe('RadioFilter', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findGlRadioButtonGroup = () => wrapper.findComponent(GlFormRadioGroup); const findGlRadioButtons = () => findGlRadioButtonGroup().findAllComponents(GlFormRadio); const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map((w) => w.text()); diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js index 23c158239dc..3b2d528e1d7 100644 --- a/spec/frontend/search/sidebar/components/scope_navigation_spec.js +++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js @@ -14,6 +14,10 @@ describe('ScopeNavigation', () => { fetchSidebarCount: jest.fn(), }; + const getterSpies = { + currentScope: jest.fn(() => 'issues'), + }; + const createComponent = (initialState) => { const store = new Vuex.Store({ state: { @@ -22,6 +26,7 @@ describe('ScopeNavigation', () => { ...initialState, }, actions: actionSpies, + getters: getterSpies, }); wrapper = shallowMount(ScopeNavigation, { @@ -29,16 +34,12 @@ describe('ScopeNavigation', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findNavElement = () => wrapper.find('nav'); const findGlNav = () => wrapper.findComponent(GlNav); const findGlNavItems = () => wrapper.findAllComponents(GlNavItem); - const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active')); - const findGlNavItemActiveLabel = () => findGlNavItemActive().at(0).findAll('span').at(0).text(); - const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1); + const findGlNavItemActive = () => wrapper.find('[active=true]'); + const findGlNavItemActiveLabel = () => findGlNavItemActive().find('[data-testid="label"]'); + const findGlNavItemActiveCount = () => findGlNavItemActive().find('[data-testid="count"]'); describe('scope navigation', () => { beforeEach(() => { @@ -71,8 +72,8 @@ describe('ScopeNavigation', () => { }); it('has correct active item', () => { - expect(findGlNavItemActive()).toHaveLength(1); - expect(findGlNavItemActiveLabel()).toBe('Issues'); + expect(findGlNavItemActive().exists()).toBe(true); + expect(findGlNavItemActiveLabel().text()).toBe('Issues'); }); it('has correct active item count', () => { @@ -80,7 +81,7 @@ describe('ScopeNavigation', () => { }); it('does not have plus sign after count text', () => { - expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(false); + expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(false); }); it('has count is highlighted correctly', () => { @@ -90,14 +91,26 @@ describe('ScopeNavigation', () => { describe('scope navigation sets proper state with NO url scope set', () => { beforeEach(() => { + getterSpies.currentScope = jest.fn(() => 'projects'); createComponent({ urlQuery: {}, + navigation: { + ...MOCK_NAVIGATION, + projects: { + ...MOCK_NAVIGATION.projects, + active: true, + }, + issues: { + ...MOCK_NAVIGATION.issues, + active: false, + }, + }, }); }); it('has correct active item', () => { - expect(findGlNavItems().at(0).attributes('active')).toBe('true'); - expect(findGlNavItemActiveLabel()).toBe('Projects'); + expect(findGlNavItemActive().exists()).toBe(true); + expect(findGlNavItemActiveLabel().text()).toBe('Projects'); }); it('has correct active item count', () => { @@ -105,7 +118,7 @@ describe('ScopeNavigation', () => { }); it('has correct active item count and over limit sign', () => { - expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(true); + expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js index a566b9b99d3..322ce1b16ef 100644 --- a/spec/frontend/search/sort/components/app_spec.js +++ b/spec/frontend/search/sort/components/app_spec.js @@ -38,11 +38,6 @@ describe('GlobalSearchSort', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findSortButtonGroup = () => wrapper.findComponent(GlButtonGroup); const findSortDropdown = () => wrapper.findComponent(GlDropdown); const findSortDirectionButton = () => wrapper.findComponent(GlButton); diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index 2f87802dfe6..0884411df0c 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import * as logger from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; @@ -33,7 +33,7 @@ import { MOCK_AGGREGATIONS, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ setUrlParams: jest.fn(), joinPaths: jest.fn().mockReturnValue(''), @@ -47,7 +47,7 @@ describe('Global Search Store Actions', () => { let mock; let state; - const flashCallback = (callCount) => { + const alertCallback = (callCount) => { expect(createAlert).toHaveBeenCalledTimes(callCount); createAlert.mockClear(); }; @@ -63,12 +63,12 @@ describe('Global Search Store Actions', () => { }); describe.each` - action | axiosMock | type | expectedMutations | flashCallCount + action | axiosMock | type | expectedMutations | alertCallCount ${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0} ${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1} ${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0} ${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1} - `(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => { + `(`axios calls`, ({ action, axiosMock, type, expectedMutations, alertCallCount }) => { describe(action.name, () => { describe(`on ${type}`, () => { beforeEach(() => { @@ -76,7 +76,7 @@ describe('Global Search Store Actions', () => { }); it(`should dispatch the correct mutations`, () => { return testAction({ action, state, expectedMutations }).then(() => - flashCallback(flashCallCount), + alertCallback(alertCallCount), ); }); }); @@ -84,12 +84,12 @@ describe('Global Search Store Actions', () => { }); describe.each` - action | axiosMock | type | expectedMutations | flashCallCount + action | axiosMock | type | expectedMutations | alertCallCount ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0} ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1} ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0} ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1} - `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => { + `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, alertCallCount }) => { describe(action.name, () => { describe(`on ${type}`, () => { beforeEach(() => { @@ -103,7 +103,7 @@ describe('Global Search Store Actions', () => { it(`should dispatch the correct mutations`, () => { return testAction({ action, state, expectedMutations }).then(() => { - flashCallback(flashCallCount); + alertCallback(alertCallCount); }); }); }); @@ -275,7 +275,7 @@ describe('Global Search Store Actions', () => { describe.each` action | axiosMock | type | scope | expectedMutations | errorLogs ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0} - ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0} + ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'error'} | ${'projects'} | ${[]} | ${1} ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${'issues'} | ${[]} | ${1} `('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => { describe(`on ${type}`, () => { @@ -290,9 +290,9 @@ describe('Global Search Store Actions', () => { } }); - it(`should ${expectedMutations.length === 0 ? 'NOT ' : ''}dispatch ${ - expectedMutations.length === 0 ? '' : 'the correct ' - }mutations for ${scope}`, () => { + it(`should ${expectedMutations.length === 0 ? 'NOT' : ''} dispatch ${ + expectedMutations.length === 0 ? '' : 'the correct' + } mutations for ${scope}`, () => { return testAction({ action, state, expectedMutations }).then(() => { expect(logger.logError).toHaveBeenCalledTimes(errorLogs); }); @@ -325,4 +325,26 @@ describe('Global Search Store Actions', () => { }); }); }); + + describe('resetLanguageQueryWithRedirect', () => { + it('calls visitUrl and setParams with the state.query', () => { + return testAction(actions.resetLanguageQueryWithRedirect, null, state, [], [], () => { + expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null }); + expect(urlUtils.visitUrl).toHaveBeenCalled(); + }); + }); + }); + + describe('resetLanguageQuery', () => { + it('calls commit SET_QUERY with value []', () => { + state = { ...state, query: { ...state.query, language: ['YAML', 'Text', 'Markdown'] } }; + return testAction( + actions.resetLanguageQuery, + null, + state, + [{ type: types.SET_QUERY, payload: { key: 'language', value: [] } }], + [], + ); + }); + }); }); diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js index 818902ee720..0ef0922c4b0 100644 --- a/spec/frontend/search/store/getters_spec.js +++ b/spec/frontend/search/store/getters_spec.js @@ -1,12 +1,15 @@ import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants'; import * as getters from '~/search/store/getters'; import createState from '~/search/store/state'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS, MOCK_AGGREGATIONS, MOCK_LANGUAGE_AGGREGATIONS_BUCKETS, + TEST_FILTER_DATA, + MOCK_NAVIGATION, } from '../mock_data'; describe('Global Search Store Getters', () => { @@ -14,37 +17,55 @@ describe('Global Search Store Getters', () => { beforeEach(() => { state = createState({ query: MOCK_QUERY }); + useMockLocationHelper(); }); describe('frequentGroups', () => { - beforeEach(() => { - state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS; - }); - it('returns the correct data', () => { + state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS; expect(getters.frequentGroups(state)).toStrictEqual(MOCK_GROUPS); }); }); describe('frequentProjects', () => { - beforeEach(() => { - state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS; - }); - it('returns the correct data', () => { + state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS; expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS); }); }); - describe('langugageAggregationBuckets', () => { - beforeEach(() => { + describe('languageAggregationBuckets', () => { + it('returns the correct data', () => { state.aggregations.data = MOCK_AGGREGATIONS; + expect(getters.languageAggregationBuckets(state)).toStrictEqual( + MOCK_LANGUAGE_AGGREGATIONS_BUCKETS, + ); }); + }); + describe('queryLanguageFilters', () => { it('returns the correct data', () => { - expect(getters.langugageAggregationBuckets(state)).toStrictEqual( - MOCK_LANGUAGE_AGGREGATIONS_BUCKETS, - ); + state.query.language = Object.keys(TEST_FILTER_DATA.filters); + expect(getters.queryLanguageFilters(state)).toStrictEqual(state.query.language); + }); + }); + + describe('currentScope', () => { + it('returns the correct scope name', () => { + state.navigation = MOCK_NAVIGATION; + expect(getters.currentScope(state)).toBe('issues'); + }); + }); + + describe('currentUrlQueryHasLanguageFilters', () => { + it.each` + description | lang | result + ${'has valid language'} | ${{ language: ['a', 'b'] }} | ${true} + ${'has empty lang'} | ${{ language: [] }} | ${false} + ${'has no lang'} | ${{}} | ${false} + `('$description', ({ lang, result }) => { + state.urlQuery = lang; + expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result); }); }); }); diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js index 487ed7bfe03..dfe4e801f11 100644 --- a/spec/frontend/search/store/utils_spec.js +++ b/spec/frontend/search/store/utils_spec.js @@ -7,6 +7,7 @@ import { isSidebarDirty, formatSearchResultCount, getAggregationsUrl, + prepareSearchAggregations, } from '~/search/store/utils'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { @@ -15,6 +16,9 @@ import { MOCK_INFLATED_DATA, FRESH_STORED_DATA, STALE_STORED_DATA, + MOCK_AGGREGATIONS, + SMALL_MOCK_AGGREGATIONS, + TEST_RAW_BUCKETS, } from '../mock_data'; const PREV_TIME = new Date().getTime() - 1; @@ -226,11 +230,14 @@ describe('Global Search Store Utils', () => { }); describe.each` - description | currentQuery | urlQuery | isDirty - ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false} - ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true} - ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false} - ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true} + description | currentQuery | urlQuery | isDirty + ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false} + ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'c'] }} | ${true} + ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null, [SIDEBAR_PARAMS[2]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: undefined }} | ${false} + ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: [] }} | ${true} + ${'language only no url params'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: undefined }} | ${true} + ${'language only url params symetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false} + ${'language only url params asymetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${true} `('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => { describe(`with ${description} sidebar query data`, () => { let res; @@ -263,4 +270,22 @@ describe('Global Search Store Utils', () => { expect(getAggregationsUrl()).toStrictEqual(`${testURL}search/aggregations`); }); }); + + const TEST_LANGUAGE_QUERY = ['Markdown', 'JSON']; + const TEST_EXPECTED_ORDERED_BUCKETS = [ + TEST_RAW_BUCKETS.find((x) => x.key === 'Markdown'), + TEST_RAW_BUCKETS.find((x) => x.key === 'JSON'), + ...TEST_RAW_BUCKETS.filter((x) => !TEST_LANGUAGE_QUERY.includes(x.key)), + ]; + + describe('prepareSearchAggregations', () => { + it.each` + description | query | data | result + ${'has no query'} | ${undefined} | ${MOCK_AGGREGATIONS} | ${MOCK_AGGREGATIONS} + ${'has query'} | ${{ language: TEST_LANGUAGE_QUERY }} | ${SMALL_MOCK_AGGREGATIONS} | ${[{ ...SMALL_MOCK_AGGREGATIONS[0], buckets: TEST_EXPECTED_ORDERED_BUCKETS }]} + ${'has bad query'} | ${{ language: ['sdf', 'wrt'] }} | ${SMALL_MOCK_AGGREGATIONS} | ${SMALL_MOCK_AGGREGATIONS} + `('$description', ({ query, data, result }) => { + expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result); + }); + }); }); diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js index 3975887cfff..423ec6ff63b 100644 --- a/spec/frontend/search/topbar/components/app_spec.js +++ b/spec/frontend/search/topbar/components/app_spec.js @@ -36,10 +36,6 @@ describe('GlobalSearchTopbar', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByClick); const findGroupFilter = () => wrapper.findComponent(GroupFilter); const findProjectFilter = () => wrapper.findComponent(ProjectFilter); diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js index b2d0297fdc2..78d9efbd686 100644 --- a/spec/frontend/search/topbar/components/group_filter_spec.js +++ b/spec/frontend/search/topbar/components/group_filter_spec.js @@ -49,10 +49,6 @@ describe('GroupFilter', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown); describe('template', () => { diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js index 297a536e075..9eda34b1633 100644 --- a/spec/frontend/search/topbar/components/project_filter_spec.js +++ b/spec/frontend/search/topbar/components/project_filter_spec.js @@ -49,10 +49,6 @@ describe('ProjectFilter', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown); describe('template', () => { diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js index e51fe9a4cf9..c911fe53d40 100644 --- a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js +++ b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js @@ -24,10 +24,6 @@ describe('Global Search Searchable Dropdown Item', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem); const findGlAvatar = () => wrapper.findComponent(GlAvatar); const findDropdownTitle = () => wrapper.findByTestId('item-title'); diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js index de1cefa9e9d..5e5f46ff34e 100644 --- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js +++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js @@ -1,6 +1,6 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data'; @@ -40,10 +40,6 @@ describe('Global Search Searchable Dropdown', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlDropdown = () => wrapper.findComponent(GlDropdown); const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType); const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text'); @@ -133,9 +129,7 @@ describe('Global Search Searchable Dropdown', () => { describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => { beforeEach(() => { createComponent({}, { frequentItems }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ searchText }); + findGlDropdownSearch().vm.$emit('input', searchText); }); it(`should${length ? '' : ' not'} render frequent dropdown items`, () => { @@ -191,28 +185,33 @@ describe('Global Search Searchable Dropdown', () => { }); describe('opening the dropdown', () => { - describe('for the first time', () => { - beforeEach(() => { - findGlDropdown().vm.$emit('show'); - }); + beforeEach(() => { + findGlDropdown().vm.$emit('show'); + }); - it('$emits @search and @first-open', () => { - expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]); - expect(wrapper.emitted('first-open')[0]).toStrictEqual([]); - }); + it('$emits @search and @first-open on the first open', async () => { + expect(wrapper.emitted('search')[0]).toStrictEqual(['']); + expect(wrapper.emitted('first-open')[0]).toStrictEqual([]); }); - describe('not for the first time', () => { - beforeEach(() => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ hasBeenOpened: true }); - findGlDropdown().vm.$emit('show'); + describe('when the dropdown has been opened', () => { + it('$emits @search with the searchText', async () => { + const searchText = 'foo'; + + findGlDropdownSearch().vm.$emit('input', searchText); + await nextTick(); + + expect(wrapper.emitted('search')[1]).toStrictEqual([searchText]); + expect(wrapper.emitted('first-open')).toHaveLength(1); }); - it('$emits @search and not @first-open', () => { - expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]); - expect(wrapper.emitted('first-open')).toBeUndefined(); + it('does not emit @first-open again', async () => { + expect(wrapper.emitted('first-open')).toHaveLength(1); + + findGlDropdownSearch().vm.$emit('input'); + await nextTick(); + + expect(wrapper.emitted('first-open')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js index a3098fb81ea..dfb31eeda78 100644 --- a/spec/frontend/search_autocomplete_spec.js +++ b/spec/frontend/search_autocomplete_spec.js @@ -119,7 +119,6 @@ describe('Search autocomplete dropdown', () => { afterEach(() => { // Undo what we did to the shared <body> removeBodyAttributes(); - window.gon = {}; resetHTMLFixture(); }); diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js index 3f856968db6..fe761049a70 100644 --- a/spec/frontend/search_settings/components/search_settings_spec.js +++ b/spec/frontend/search_settings/components/search_settings_spec.js @@ -79,10 +79,6 @@ describe('search_settings/components/search_settings.vue', () => { buildWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('hides sections that do not match the search term', () => { const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`); search(SEARCH_TERM); diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js index ddefda2ffc3..0ca350f9ed7 100644 --- a/spec/frontend/security_configuration/components/app_spec.js +++ b/spec/frontend/security_configuration/components/app_spec.js @@ -26,6 +26,8 @@ import { REPORT_TYPE_LICENSE_COMPLIANCE, REPORT_TYPE_SAST, } from '~/vue_shared/security_reports/constants'; +import { USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message'; +import { manageViaMRErrorMessage } from '../constants'; const upgradePath = '/upgrade'; const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath'; @@ -129,10 +131,6 @@ describe('App component', () => { const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert); const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab'); - afterEach(() => { - wrapper.destroy(); - }); - describe('basic structure', () => { beforeEach(() => { createComponent(); @@ -141,7 +139,7 @@ describe('App component', () => { it('renders main-heading with correct text', () => { const mainHeading = findMainHeading(); expect(mainHeading.exists()).toBe(true); - expect(mainHeading.text()).toContain('Security Configuration'); + expect(mainHeading.text()).toContain('Security configuration'); }); describe('tabs', () => { @@ -204,18 +202,21 @@ describe('App component', () => { }); }); - describe('when error occurs', () => { + describe('when user facing error occurs', () => { it('should show Alert with error Message', async () => { expect(findManageViaMRErrorAlert().exists()).toBe(false); - findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error'); + // Prefixed with USER_FACING_ERROR_MESSAGE_PREFIX as used in lib/gitlab/utils/error_message.rb to indicate a user facing error + findFeatureCards() + .at(1) + .vm.$emit('error', `${USER_FACING_ERROR_MESSAGE_PREFIX} ${manageViaMRErrorMessage}`); await nextTick(); expect(findManageViaMRErrorAlert().exists()).toBe(true); - expect(findManageViaMRErrorAlert().text()).toEqual('There was a manage via MR error'); + expect(findManageViaMRErrorAlert().text()).toEqual(manageViaMRErrorMessage); }); it('should hide Alert when it is dismissed', async () => { - findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error'); + findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage); await nextTick(); expect(findManageViaMRErrorAlert().exists()).toBe(true); @@ -225,6 +226,17 @@ describe('App component', () => { expect(findManageViaMRErrorAlert().exists()).toBe(false); }); }); + + describe('when non-user facing error occurs', () => { + it('should show Alert with generic error Message', async () => { + expect(findManageViaMRErrorAlert().exists()).toBe(false); + findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage); + + await nextTick(); + expect(findManageViaMRErrorAlert().exists()).toBe(true); + expect(findManageViaMRErrorAlert().text()).toEqual(i18n.genericErrorText); + }); + }); }); describe('Auto DevOps hint alert', () => { diff --git a/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js index 467ae35408c..df1fa1a8084 100644 --- a/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js +++ b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js @@ -23,10 +23,6 @@ describe('AutoDevopsAlert component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains correct body text', () => { expect(wrapper.text()).toContain('Quickly enable all'); }); diff --git a/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js index 778fea2896a..22f45a92f70 100644 --- a/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js +++ b/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js @@ -21,10 +21,6 @@ describe('AutoDevopsEnabledAlert component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('contains correct body text', () => { expect(wrapper.text()).toMatchInterpolatedText(AutoDevopsEnabledAlert.i18n.body); }); diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js index d10722be8ea..23edd8a69de 100644 --- a/spec/frontend/security_configuration/components/feature_card_spec.js +++ b/spec/frontend/security_configuration/components/feature_card_spec.js @@ -5,6 +5,7 @@ import FeatureCard from '~/security_configuration/components/feature_card.vue'; import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue'; import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue'; import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants'; +import { manageViaMRErrorMessage } from '../constants'; import { makeFeature } from './utils'; describe('FeatureCard component', () => { @@ -78,7 +79,6 @@ describe('FeatureCard component', () => { }; afterEach(() => { - wrapper.destroy(); feature = undefined; }); @@ -107,8 +107,8 @@ describe('FeatureCard component', () => { }); it('should catch and emit manage-via-mr-error', () => { - findManageViaMr().vm.$emit('error', 'There was a manage via MR error'); - expect(wrapper.emitted('error')).toEqual([['There was a manage via MR error']]); + findManageViaMr().vm.$emit('error', manageViaMRErrorMessage); + expect(wrapper.emitted('error')).toEqual([[manageViaMRErrorMessage]]); }); }); 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 8f2b5383191..1f8f306c931 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -106,7 +106,7 @@ describe('TrainingProviderList component', () => { projectFullPath: testProjectPath, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, propsData: { securityTrainingEnabled: true, @@ -132,7 +132,6 @@ describe('TrainingProviderList component', () => { const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', testProviderIds[0]); afterEach(() => { - wrapper.destroy(); apolloProvider = null; }); diff --git a/spec/frontend/security_configuration/components/upgrade_banner_spec.js b/spec/frontend/security_configuration/components/upgrade_banner_spec.js index c34d8e47a6c..97087877224 100644 --- a/spec/frontend/security_configuration/components/upgrade_banner_spec.js +++ b/spec/frontend/security_configuration/components/upgrade_banner_spec.js @@ -44,7 +44,6 @@ describe('UpgradeBanner component', () => { }); afterEach(() => { - wrapper.destroy(); unmockTracking(); }); diff --git a/spec/frontend/security_configuration/constants.js b/spec/frontend/security_configuration/constants.js new file mode 100644 index 00000000000..d31036a2534 --- /dev/null +++ b/spec/frontend/security_configuration/constants.js @@ -0,0 +1 @@ +export const manageViaMRErrorMessage = 'There was a manage via MR error'; diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap index efe3f7e8dbf..c278bb4579f 100644 --- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap +++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap @@ -40,6 +40,41 @@ exports[`self-monitor component When the self-monitor project has not been creat </p> </div> + <gl-alert-stub + class="gl-mb-3" + dismissible="true" + dismisslabel="Dismiss" + primarybuttonlink="" + primarybuttontext="" + secondarybuttonlink="" + secondarybuttontext="" + showicon="true" + title="Deprecation notice" + variant="danger" + > + <div> + Self-monitoring was + <a + href="/help/update/deprecations.md#gitlab-self-monitoring-project" + > + deprecated + </a> + in GitLab 14.9, and is + <a + href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909" + > + scheduled for removal + </a> + in GitLab 16.0. For information on a possible replacement, + <a + href="https://gitlab.com/groups/gitlab-org/-/epics/6976" + > + learn more about Opstrace + </a> + . + </div> + </gl-alert-stub> + <div class="settings-content" > diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js index 2dd528a8a1c..83195e9d306 100644 --- a/spec/frontend/sentry/index_spec.js +++ b/spec/frontend/sentry/index_spec.js @@ -4,8 +4,6 @@ import LegacySentryConfig from '~/sentry/legacy_sentry_config'; import SentryConfig from '~/sentry/sentry_config'; describe('Sentry init', () => { - let originalGon; - const dsn = 'https://123@sentry.gitlab.test/123'; const environment = 'test'; const currentUserId = '1'; @@ -14,7 +12,6 @@ describe('Sentry init', () => { const featureCategory = 'my_feature_category'; beforeEach(() => { - originalGon = window.gon; window.gon = { sentry_dsn: dsn, sentry_environment: environment, @@ -28,10 +25,6 @@ describe('Sentry init', () => { jest.spyOn(SentryConfig, 'init').mockImplementation(); }); - afterEach(() => { - window.gon = originalGon; - }); - it('exports new version of Sentry in the global object', () => { // eslint-disable-next-line no-underscore-dangle expect(window._Sentry.SDK_VERSION).not.toMatch(/^5\./); diff --git a/spec/frontend/sentry/legacy_index_spec.js b/spec/frontend/sentry/legacy_index_spec.js index 5c336f8392e..493b4dfde67 100644 --- a/spec/frontend/sentry/legacy_index_spec.js +++ b/spec/frontend/sentry/legacy_index_spec.js @@ -4,8 +4,6 @@ import LegacySentryConfig from '~/sentry/legacy_sentry_config'; import SentryConfig from '~/sentry/sentry_config'; describe('Sentry init', () => { - let originalGon; - const dsn = 'https://123@sentry.gitlab.test/123'; const environment = 'test'; const currentUserId = '1'; @@ -14,7 +12,6 @@ describe('Sentry init', () => { const featureCategory = 'my_feature_category'; beforeEach(() => { - originalGon = window.gon; window.gon = { sentry_dsn: dsn, sentry_environment: environment, @@ -28,10 +25,6 @@ describe('Sentry init', () => { jest.spyOn(SentryConfig, 'init').mockImplementation(); }); - afterEach(() => { - window.gon = originalGon; - }); - it('exports legacy version of Sentry in the global object', () => { // eslint-disable-next-line no-underscore-dangle expect(window._Sentry.SDK_VERSION).toMatch(/^5\./); diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js index 44acbee9b38..34c5221ef0d 100644 --- a/spec/frontend/sentry/sentry_config_spec.js +++ b/spec/frontend/sentry/sentry_config_spec.js @@ -1,5 +1,4 @@ import * as Sentry from 'sentrybrowser7'; -import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from '~/sentry/constants'; import SentryConfig from '~/sentry/sentry_config'; @@ -62,11 +61,8 @@ describe('SentryConfig', () => { expect(Sentry.init).toHaveBeenCalledWith({ dsn: options.dsn, release: options.release, - sampleRate: SAMPLE_RATE, allowUrls: options.allowUrls, environment: options.environment, - ignoreErrors: IGNORE_ERRORS, - denyUrls: DENY_URLS, }); }); @@ -82,11 +78,8 @@ describe('SentryConfig', () => { expect(Sentry.init).toHaveBeenCalledWith({ dsn: options.dsn, release: options.release, - sampleRate: SAMPLE_RATE, allowUrls: options.allowUrls, environment: 'development', - ignoreErrors: IGNORE_ERRORS, - denyUrls: DENY_URLS, }); }); }); diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js index 85cd8d51272..b5bf739b35a 100644 --- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js +++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js @@ -5,13 +5,13 @@ import { useFakeDate } from 'helpers/fake_date'; import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import * as UserApi from '~/api/user_api'; import EmojiPicker from '~/emoji/components/picker.vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import stubChildren from 'helpers/stub_children'; import SetStatusModalWrapper from '~/set_status_modal/set_status_modal_wrapper.vue'; import { AVAILABILITY_STATUS } from '~/set_status_modal/constants'; import SetStatusForm from '~/set_status_modal/set_status_form.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('SetStatusModalWrapper', () => { let wrapper; @@ -72,7 +72,6 @@ describe('SetStatusModalWrapper', () => { }; afterEach(() => { - wrapper.destroy(); clearEmojiMock(); }); @@ -244,7 +243,7 @@ describe('SetStatusModalWrapper', () => { return initModal({ mockOnUpdateFailure: false }); }); - it('flashes an error message', async () => { + it('alerts an error message', async () => { findModal().vm.$emit('primary'); await nextTick(); diff --git a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js index a4a2a86dc73..a6ad90123b7 100644 --- a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js +++ b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js @@ -37,10 +37,6 @@ describe('UserProfileSetStatusWrapper', () => { const findInput = (name) => wrapper.find(`[name="${name}"]`); const findSetStatusForm = () => wrapper.findComponent(SetStatusForm); - afterEach(() => { - wrapper.destroy(); - }); - it('renders `SetStatusForm` component and passes expected props', () => { createComponent(); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js index 60edab8766a..81b65f4f050 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -30,10 +30,6 @@ describe('AssigneeAvatarLink component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findTooltipText = () => wrapper.attributes('title'); const findUserLink = () => wrapper.findComponent(GlLink); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js index 7df37d11987..b6b3dbd5b6b 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js @@ -7,7 +7,6 @@ const TEST_AVATAR = `${TEST_HOST}/avatar.png`; const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`; describe('AssigneeAvatar', () => { - let origGon; let wrapper; function createComponent(props = {}) { @@ -24,15 +23,9 @@ describe('AssigneeAvatar', () => { } beforeEach(() => { - origGon = window.gon; window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL }; }); - afterEach(() => { - window.gon = origGon; - wrapper.destroy(); - }); - const findImg = () => wrapper.find('img'); it('does not show warning icon if assignee can merge', () => { diff --git a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js index 14a6bdbf907..d561c761c99 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js @@ -17,11 +17,6 @@ describe('AssigneeTitle component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('assignee title', () => { it('renders assignee', () => { wrapper = createComponent({ diff --git a/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js index 080171fb2ea..0501c1bae23 100644 --- a/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js @@ -49,7 +49,6 @@ describe('Assignees Realtime', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; SidebarMediator.singleton = null; }); diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js index d422292ed9e..1661e28abd2 100644 --- a/spec/frontend/sidebar/components/assignees/assignees_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js @@ -25,10 +25,6 @@ describe('Assignee component', () => { const findComponentTextNoUsers = () => wrapper.find('[data-testid="no-value"]'); const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *'); - afterEach(() => { - wrapper.destroy(); - }); - describe('No assignees/users', () => { it('displays no assignee icon when collapsed', () => { createWrapper(); diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js index 7e7d4921cfa..40d3d090bb4 100644 --- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js @@ -26,10 +26,6 @@ describe('CollapsedAssigneeList component', () => { const findAssignees = () => wrapper.findAllComponents(CollapsedAssignee); const getTooltipTitle = () => wrapper.attributes('title'); - afterEach(() => { - wrapper.destroy(); - }); - describe('No assignees/users', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js index 4db95114b96..851eaedf0bd 100644 --- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js @@ -21,10 +21,6 @@ describe('CollapsedAssignee assignee component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('has author name', () => { createComponent(); diff --git a/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js index 1161fefcc64..82145b82e21 100644 --- a/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js +++ b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js @@ -20,11 +20,6 @@ describe('IssuableAssignees', () => { const findUncollapsedAssigneeList = () => wrapper.findComponent(UncollapsedAssigneeList); const findEmptyAssignee = () => wrapper.find('[data-testid="none"]'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when no assignees are present', () => { it.each` signedIn | editable | message diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js index 58b174059fa..a189d3656a2 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js @@ -39,9 +39,6 @@ describe('sidebar assignees', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; - SidebarService.singleton = null; SidebarStore.singleton = null; SidebarMediator.singleton = null; 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 3aca346ff5f..9f7c587ca9d 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -5,8 +5,8 @@ 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 { createAlert } from '~/flash'; -import { IssuableType } from '~/issues/constants'; +import { createAlert } from '~/alert'; +import { TYPE_MERGE_REQUEST } from '~/issues/constants'; import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; @@ -17,7 +17,7 @@ import updateIssueAssigneesMutation from '~/sidebar/queries/update_issue_assigne import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const updateIssueAssigneesMutationSuccess = jest .fn() @@ -98,10 +98,7 @@ describe('Sidebar assignees widget', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; fakeApollo = null; - delete gon.current_username; }); describe('with passed initial assignees', () => { @@ -397,7 +394,7 @@ describe('Sidebar assignees widget', () => { }); it('does not render invite members link on non-issue sidebar', async () => { - createComponent({ props: { issuableType: IssuableType.MergeRequest } }); + createComponent({ props: { issuableType: TYPE_MERGE_REQUEST } }); await waitForPromises(); expect(findInviteMembersLink().exists()).toBe(false); }); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js index 6c22d2f687d..27c31ac56c9 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js @@ -21,11 +21,6 @@ describe('boards sidebar remove issue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('template', () => { it('renders title', () => { const title = 'Sidebar item title'; diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js index b738d931040..501048bf056 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js @@ -16,10 +16,6 @@ describe('Sidebar invite members component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when directly inviting members', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js index be0b14fa997..7895274ab6d 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js @@ -1,6 +1,6 @@ import { GlAvatarLabeled, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants'; import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; const user = { @@ -32,10 +32,6 @@ describe('Sidebar participant component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('does not show `Busy` status when user is not busy', () => { createComponent(); @@ -56,13 +52,13 @@ describe('Sidebar participant component', () => { describe('when on merge request sidebar', () => { it('when project member cannot merge', () => { - createComponent({ issuableType: IssuableType.MergeRequest }); + createComponent({ issuableType: TYPE_MERGE_REQUEST }); expect(findIcon().exists()).toBe(true); }); it('when project member can merge', () => { - createComponent({ issuableType: IssuableType.MergeRequest, canMerge: true }); + createComponent({ issuableType: TYPE_MERGE_REQUEST, canMerge: true }); expect(findIcon().exists()).toBe(false); }); diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js index 03c2e1a37a9..c74a714cca4 100644 --- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js +++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js @@ -24,10 +24,6 @@ describe('UncollapsedAssigneeList component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findMoreButton = () => wrapper.find('.user-list-more button'); describe('One assignee/user', () => { diff --git a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js index 37c16bc9235..877d7cd61ee 100644 --- a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js +++ b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js @@ -18,10 +18,6 @@ describe('UserNameWithStatus', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('will render the users name', () => { expect(wrapper.html()).toContain(name); }); diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js index 81354d64a90..4a2b3b30e6d 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js @@ -18,10 +18,6 @@ describe('Sidebar Confidentiality Content', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('emits `expandSidebar` event on collapsed icon click', () => { createComponent(); findCollapsedIcon().trigger('click'); 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 b27f7c6b4e1..1ca20dad1c6 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js @@ -2,11 +2,11 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue'; import { confidentialityQueries } from '~/sidebar/constants'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Sidebar Confidentiality Form', () => { let wrapper; @@ -38,10 +38,6 @@ describe('Sidebar Confidentiality Form', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('emits a `closeForm` event when Cancel button is clicked', () => { createComponent(); findCancelButton().vm.$emit('click'); @@ -58,7 +54,7 @@ describe('Sidebar Confidentiality Form', () => { expect(findConfidentialToggle().props('loading')).toBe(true); }); - it('creates a flash if mutation is rejected', async () => { + it('creates an alert if mutation is rejected', async () => { createComponent({ mutate: jest.fn().mockRejectedValue('Error!') }); findConfidentialToggle().vm.$emit('click', new MouseEvent('click')); await waitForPromises(); @@ -68,7 +64,7 @@ describe('Sidebar Confidentiality Form', () => { }); }); - it('creates a flash if mutation contains errors', async () => { + it('creates an alert if mutation contains errors', async () => { createComponent({ mutate: jest.fn().mockResolvedValue({ data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } }, 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 e486a8e9ec7..39b30307dd7 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js @@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue'; import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue'; import SidebarConfidentialityWidget, { @@ -14,7 +14,7 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue' import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; import { issueConfidentialityResponse } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -48,7 +48,6 @@ describe('Sidebar Confidentiality Widget', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -120,7 +119,7 @@ describe('Sidebar Confidentiality Widget', () => { }); }); - it('displays a flash message when query is rejected', async () => { + it('displays an alert message when query is rejected', async () => { createComponent({ confidentialQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'), }); diff --git a/spec/frontend/sidebar/components/copy/copyable_field_spec.js b/spec/frontend/sidebar/components/copy/copyable_field_spec.js index 7790d77bc65..03e131aab35 100644 --- a/spec/frontend/sidebar/components/copy/copyable_field_spec.js +++ b/spec/frontend/sidebar/components/copy/copyable_field_spec.js @@ -20,10 +20,6 @@ describe('SidebarCopyableField', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findClipboardButton = () => wrapper.findComponent(ClipboardButton); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); diff --git a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js index c3de076d6aa..2ae80b2c97b 100644 --- a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js +++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants'; import SidebarReferenceWidget from '~/sidebar/components/copy/sidebar_reference_widget.vue'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; @@ -39,10 +39,6 @@ describe('Sidebar Reference Widget', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when reference is loading', () => { it('sets CopyableField `is-loading` prop to `true`', () => { createComponent({ referenceQueryHandler: jest.fn().mockReturnValue(new Promise(() => {})) }); @@ -52,7 +48,7 @@ describe('Sidebar Reference Widget', () => { describe.each([ [TYPE_ISSUE, issueReferenceQuery], - [IssuableType.MergeRequest, mergeRequestReferenceQuery], + [TYPE_MERGE_REQUEST, mergeRequestReferenceQuery], ])('when issuableType is %s', (issuableType, referenceQuery) => { it('sets CopyableField `value` prop to reference value', async () => { createComponent({ diff --git a/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js index ca43c219d92..546cabd07d3 100644 --- a/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js +++ b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js @@ -3,7 +3,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import CrmContacts from '~/sidebar/components/crm_contacts/crm_contacts.vue'; import getIssueCrmContactsQuery from '~/sidebar/queries/get_issue_crm_contacts.query.graphql'; import issueCrmContactsSubscription from '~/sidebar/queries/issue_crm_contacts.subscription.graphql'; @@ -13,7 +13,7 @@ import { issueCrmContactsUpdateNullResponse, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Issue crm contacts component', () => { Vue.use(VueApollo); @@ -39,7 +39,6 @@ describe('Issue crm contacts component', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js index 67413cffdda..b9c8655d5d8 100644 --- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js +++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue'; import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue'; @@ -13,7 +13,7 @@ import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql' import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -22,10 +22,6 @@ describe('Sidebar date Widget', () => { let fakeApollo; const date = '2021-04-15'; - window.gon = { - first_day_of_week: 1, - }; - const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]'); const findDatePicker = () => wrapper.findComponent(GlDatepicker); @@ -61,8 +57,11 @@ describe('Sidebar date Widget', () => { }); }; + beforeEach(() => { + window.gon.first_day_of_week = 1; + }); + afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -159,7 +158,7 @@ describe('Sidebar date Widget', () => { expect(wrapper.findComponent(SidebarInheritDate).exists()).toBe(false); }); - it('displays a flash message when query is rejected', async () => { + it('displays an alert message when query is rejected', async () => { createComponent({ dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'), }); diff --git a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js index cbe01263dcd..1bb910c53ea 100644 --- a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js +++ b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js @@ -27,10 +27,6 @@ describe('SidebarFormattedDate', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays formatted date', () => { expect(findFormattedDate().text()).toBe('Apr 15, 2021'); }); diff --git a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js index a7556b9110c..97debe3088d 100644 --- a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js +++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js @@ -31,10 +31,6 @@ describe('SidebarInheritDate', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('displays formatted fixed and inherited dates with radio buttons', () => { expect(wrapper.findAllComponents(SidebarFormattedDate)).toHaveLength(2); expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(2); diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js index 1a78ce4ddee..e356f02a36b 100644 --- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js +++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js @@ -17,10 +17,6 @@ describe('EscalationStatus', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownComponent = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownMenu = () => findDropdownComponent().find('.dropdown-menu'); diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js index 2dded61c073..00b57b4916e 100644 --- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js +++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js @@ -18,11 +18,11 @@ import { } from '~/sidebar/constants'; import waitForPromises from 'helpers/wait_for_promises'; import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { logError } from '~/lib/logger'; jest.mock('~/lib/logger'); -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -57,7 +57,7 @@ describe('SidebarEscalationStatus', () => { canUpdate: true, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, apolloProvider, }); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js index 4f2a89e20db..084ca5ed3fc 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js @@ -29,10 +29,6 @@ describe('DropdownButton', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownButton = () => wrapper.findComponent(GlButton); const findDropdownText = () => wrapper.find('.dropdown-toggle-text'); const findDropdownIcon = () => wrapper.findComponent(GlIcon); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js index 59e95edfa20..bb7554ff21d 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js @@ -32,10 +32,6 @@ describe('DropdownContentsCreateView', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('disableCreate', () => { it('returns `true` when label title and color is not defined', () => { @@ -174,11 +170,17 @@ describe('DropdownContentsCreateView', () => { }); await nextTick(); - const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview'); - const colorInputEl = wrapper.find('.color-input-container').findComponent(GlFormInput); + const colorPreviewEl = wrapper + .find('.color-input-container') + .findAllComponents(GlFormInput) + .at(0); + const colorInputEl = wrapper + .find('.color-input-container') + .findAllComponents(GlFormInput) + .at(1); expect(colorPreviewEl.exists()).toBe(true); - expect(colorPreviewEl.attributes('style')).toContain('background-color'); + expect(colorPreviewEl.attributes('value')).toBe('#ff0000'); expect(colorInputEl.exists()).toBe(true); expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000'); expect(colorInputEl.attributes('value')).toBe('#ff0000'); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js index 865dc8fe8fb..7940518f1e8 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -51,10 +51,6 @@ describe('DropdownContentsLabelsView', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]'); const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]'); const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js index e9ffda7c251..0a17c5f8721 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js @@ -28,10 +28,6 @@ describe('DropdownContent', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('dropdownContentsView', () => { it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => { diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js index 6c3fda421ff..367f6007194 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js @@ -31,10 +31,6 @@ describe('DropdownTitle', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('renders component container element with string "Labels"', () => { expect(wrapper.text()).toContain('Labels'); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js index 56f25a1c6a4..6684cf0c5f4 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js @@ -19,15 +19,11 @@ describe('DropdownValueCollapsedComponent', () => { wrapper = shallowMount(DropdownValueCollapsedComponent, { propsData: { ...defaultProps, ...props }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlIcon = () => wrapper.findComponent(GlIcon); const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip'); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js index a1ccc9d2ab1..70aafceb00c 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js @@ -28,10 +28,6 @@ describe('DropdownValue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('methods', () => { describe('labelFilterUrl', () => { it('returns a label filter URL based on provided label param', () => { diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js index e14c0e308ce..468dd14c9ee 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js @@ -26,10 +26,6 @@ describe('LabelItem', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('renders gl-link component', () => { expect(wrapper.findComponent(GlLink).exists()).toBe(true); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js index a3b10c18374..806064b2202 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js @@ -40,10 +40,6 @@ describe('LabelsSelectRoot', () => { store = new Vuex.Store(labelsSelectModule()); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('methods', () => { describe('handleVuexActionDispatch', () => { const touchedLabels = [ diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js index 55651bccaa8..c27afb75375 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js @@ -1,14 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions'; import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types'; import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('LabelsSelect Actions', () => { let state; @@ -100,7 +100,7 @@ describe('LabelsSelect Actions', () => { ); }); - it('shows flash error', () => { + it('shows alert error', () => { actions.receiveLabelsFailure({ commit: () => {} }); expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' }); @@ -184,7 +184,7 @@ describe('LabelsSelect Actions', () => { ); }); - it('shows flash error', () => { + it('shows alert error', () => { actions.receiveCreateLabelFailure({ commit: () => {} }); expect(createAlert).toHaveBeenCalledWith({ message: 'Error creating label.' }); diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js index 79b164b0ea7..4ca0a813da2 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js @@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { workspaceLabelsQueries } from '~/sidebar/constants'; import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue'; import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql'; @@ -15,7 +15,7 @@ import { workspaceLabelsQueryResponse, } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const colors = Object.keys(mockSuggestedColors); @@ -87,10 +87,6 @@ describe('DropdownContentsCreateView', () => { gon.suggested_label_colors = mockSuggestedColors; }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders a palette of 21 colors', () => { createComponent(); expect(findAllColors()).toHaveLength(21); @@ -103,7 +99,7 @@ describe('DropdownContentsCreateView', () => { findAllColors().at(0).vm.$emit('click', new Event('mouseclick')); await nextTick(); - expect(findSelectedColor().attributes('style')).toBe('background-color: rgb(0, 153, 102);'); + expect(findSelectedColor().attributes('value')).toBe('#009966'); }); it('shows correct color hex code after selecting a color', async () => { diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js index 913badccbe4..c351a60735b 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -9,7 +9,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants'; import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue'; @@ -17,7 +17,7 @@ import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue'; import { mockConfig, workspaceLabelsQueryResponse } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -64,10 +64,6 @@ describe('DropdownContentsLabelsView', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findLabels = () => wrapper.findAllComponents(LabelItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findObserver = () => wrapper.findComponent(GlIntersectionObserver); diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js index 9bbb1413ee9..e9023cb9ff6 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js @@ -66,10 +66,6 @@ describe('DropdownContent', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView); const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView); const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub); diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js index 9a6e0ca3ccd..ad1edaa6671 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js @@ -20,10 +20,6 @@ describe('DropdownFooter', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); describe('Labels view', () => { diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js index d9001dface4..824f91812fb 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js @@ -28,10 +28,6 @@ describe('DropdownHeader', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findGoBackButton = () => wrapper.findByTestId('go-back-button'); const findDropdownTitle = () => wrapper.findByTestId('dropdown-header-title'); diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js index 585048983c9..d70b989b493 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js @@ -30,10 +30,6 @@ describe('DropdownValue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when there are no labels', () => { beforeEach(() => { createComponent( diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js index 4fa65c752f9..715dd4e034e 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js @@ -30,10 +30,6 @@ describe('EmbeddedLabelsList', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when there are no labels', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js index 74188a77994..377d1894411 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js @@ -19,10 +19,6 @@ describe('LabelItem', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('renders label color element', () => { const colorEl = wrapper.find('[data-testid="label-color-box"]'); diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js index fd8e72bac49..3101fd90f2e 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js @@ -3,8 +3,8 @@ 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 { createAlert } from '~/flash'; -import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; +import { createAlert } from '~/alert'; +import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue'; import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue'; @@ -25,7 +25,7 @@ import { mockRegularLabel, } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -36,9 +36,9 @@ const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a proble const updateLabelsMutation = { [TYPE_ISSUE]: updateIssueLabelsMutation, - [IssuableType.MergeRequest]: updateMergeRequestLabelsMutation, + [TYPE_MERGE_REQUEST]: updateMergeRequestLabelsMutation, [TYPE_EPIC]: updateEpicLabelsMutation, - [IssuableType.TestCase]: updateTestCaseLabelsMutation, + [TYPE_TEST_CASE]: updateTestCaseLabelsMutation, }; describe('LabelsSelectRoot', () => { @@ -83,10 +83,6 @@ describe('LabelsSelectRoot', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders component with classes `labels-select-wrapper gl-relative`', () => { createComponent(); expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'gl-relative']); @@ -150,7 +146,7 @@ describe('LabelsSelectRoot', () => { }); }); - it('creates flash with error message when query is rejected', async () => { + it('creates alert with error message when query is rejected', async () => { createComponent({ queryHandler: errorQueryHandler }); await waitForPromises(); expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' }); @@ -214,9 +210,9 @@ describe('LabelsSelectRoot', () => { describe.each` issuableType ${TYPE_ISSUE} - ${IssuableType.MergeRequest} + ${TYPE_MERGE_REQUEST} ${TYPE_EPIC} - ${IssuableType.TestCase} + ${TYPE_TEST_CASE} `('when updating labels for $issuableType', ({ issuableType }) => { const label = { id: 'gid://gitlab/ProjectLabel/2' }; diff --git a/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js index 2abb0c24d7d..ad9efc371f0 100644 --- a/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js +++ b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { createStore as createMrStore } from '~/mr_notes/stores'; import createStore from '~/notes/stores'; import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue'; @@ -8,7 +8,7 @@ import eventHub from '~/sidebar/event_hub'; import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants'; jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() })); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('EditFormButtons', () => { let wrapper; @@ -51,11 +51,6 @@ describe('EditFormButtons', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` pageType ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR} @@ -128,7 +123,7 @@ describe('EditFormButtons', () => { expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm'); }); - it('does not flash an error message', () => { + it('does not alert an error message', () => { expect(createAlert).not.toHaveBeenCalled(); }); }); @@ -161,7 +156,7 @@ describe('EditFormButtons', () => { expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm'); }); - it('calls flash with the correct message', () => { + it('calls alert with the correct message', () => { expect(createAlert).toHaveBeenCalledWith({ message: `Something went wrong trying to change the locked state of this ${issuableDisplayName}`, }); diff --git a/spec/frontend/sidebar/components/lock/edit_form_spec.js b/spec/frontend/sidebar/components/lock/edit_form_spec.js index 4ae9025ee39..06cce7bd7ca 100644 --- a/spec/frontend/sidebar/components/lock/edit_form_spec.js +++ b/spec/frontend/sidebar/components/lock/edit_form_spec.js @@ -24,11 +24,6 @@ describe('Edit Form Dropdown', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` pageType ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR} diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js index 8f825847cfc..d26ef7298ce 100644 --- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js +++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js @@ -62,16 +62,11 @@ describe('IssuableLockForm', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` pageType ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR} diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js index b492753867b..8a0db1715f3 100644 --- a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js +++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { TYPE_ISSUE, WorkspaceType } from '~/issues/constants'; +import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants'; import { __ } from '~/locale'; import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue'; import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue'; @@ -12,7 +12,7 @@ describe('MilestoneDropdown component', () => { const propsData = { attrWorkspacePath: 'full/path', issuableType: TYPE_ISSUE, - workspaceType: WorkspaceType.project, + workspaceType: WORKSPACE_PROJECT, }; const findHiddenInput = () => wrapper.find('input'); diff --git a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js index 72279f44e80..e247f5d27fa 100644 --- a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js +++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js @@ -63,7 +63,6 @@ describe('IssuableMoveDropdown', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/sidebar/components/move/move_issue_button_spec.js b/spec/frontend/sidebar/components/move/move_issue_button_spec.js index acd6b23c1f5..eb5e23c6047 100644 --- a/spec/frontend/sidebar/components/move/move_issue_button_spec.js +++ b/spec/frontend/sidebar/components/move/move_issue_button_spec.js @@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { visitUrl } from '~/lib/utils/url_utility'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import ProjectSelect from '~/sidebar/components/move/issuable_move_dropdown.vue'; import MoveIssueButton from '~/sidebar/components/move/move_issue_button.vue'; import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), })); @@ -118,7 +118,7 @@ describe('MoveIssueButton', () => { expect(findProjectSelect().props('moveInProgress')).toBe(false); }); - it('creates a flash and logs errors when a mutation returns errors', async () => { + it('creates an alert and logs errors when a mutation returns errors', async () => { createComponent(resolvedMutationWithErrorsMock); emitProjectSelectEvent(); diff --git a/spec/frontend/sidebar/components/move/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js index c65bad642a0..662a39c829d 100644 --- a/spec/frontend/sidebar/components/move/move_issues_button_spec.js +++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js @@ -6,7 +6,7 @@ import { GlAlert } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { logError } from '~/lib/logger'; import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue'; import issuableEventHub from '~/issues/list/eventhub'; @@ -22,7 +22,7 @@ import { WORK_ITEM_TYPE_ENUM_TEST_CASE, } from '~/work_items/constants'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/logger'); useMockLocationHelper(); @@ -159,7 +159,6 @@ describe('MoveIssuesButton', () => { }); afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -389,7 +388,7 @@ describe('MoveIssuesButton', () => { }); describe('shows errors', () => { - it('does not create flashes or logs errors when no issue is selected', async () => { + it('does not create alerts or logs errors when no issue is selected', async () => { createComponent(); emitMoveIssuablesEvent(); @@ -399,7 +398,7 @@ describe('MoveIssuesButton', () => { expect(createAlert).not.toHaveBeenCalled(); }); - it('does not create flashes or logs errors when only tasks are selected', async () => { + it('does not create alerts or logs errors when only tasks are selected', async () => { createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly }); emitMoveIssuablesEvent(); @@ -409,7 +408,7 @@ describe('MoveIssuesButton', () => { expect(createAlert).not.toHaveBeenCalled(); }); - it('does not create flashes or logs errors when only test cases are selected', async () => { + it('does not create alerts or logs errors when only test cases are selected', async () => { createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly }); emitMoveIssuablesEvent(); @@ -419,7 +418,7 @@ describe('MoveIssuesButton', () => { expect(createAlert).not.toHaveBeenCalled(); }); - it('does not create flashes or logs errors when only tasks and test cases are selected', async () => { + it('does not create alerts or logs errors when only tasks and test cases are selected', async () => { createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases }); emitMoveIssuablesEvent(); @@ -429,7 +428,7 @@ describe('MoveIssuesButton', () => { expect(createAlert).not.toHaveBeenCalled(); }); - it('does not create flashes or logs errors when issues are moved without errors', async () => { + it('does not create alerts or logs errors when issues are moved without errors', async () => { createComponent( { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, resolvedMutationWithoutErrorsMock, @@ -442,7 +441,7 @@ describe('MoveIssuesButton', () => { expect(createAlert).not.toHaveBeenCalled(); }); - it('creates a flash and logs errors when a mutation returns errors', async () => { + it('creates an alert and logs errors when a mutation returns errors', async () => { createComponent( { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, resolvedMutationWithErrorsMock, @@ -462,14 +461,14 @@ describe('MoveIssuesButton', () => { `Error moving issue. Error message: ${mockMutationErrorMessage}`, ); - // Only one flash is created even if multiple errors are reported + // Only one alert is created even if multiple errors are reported expect(createAlert).toHaveBeenCalledTimes(1); expect(createAlert).toHaveBeenCalledWith({ message: 'There was an error while moving the issues.', }); }); - it('creates a flash but not logs errors when a mutation is rejected', async () => { + it('creates an alert but not logs errors when a mutation is rejected', async () => { createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }); emitMoveIssuablesEvent(); diff --git a/spec/frontend/sidebar/components/participants/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js index f7a626a189c..72d83ebeca4 100644 --- a/spec/frontend/sidebar/components/participants/participants_spec.js +++ b/spec/frontend/sidebar/components/participants/participants_spec.js @@ -1,203 +1,114 @@ -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import Participants from '~/sidebar/components/participants/participants.vue'; -const PARTICIPANT = { - id: 1, - state: 'active', - username: 'marcene', - name: 'Allie Will', - web_url: 'foo.com', - avatar_url: 'gravatar.com/avatar/xxx', -}; - -const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }]; - -describe('Participants', () => { +describe('Participants component', () => { let wrapper; - const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]'); - const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]'); + const participant = { + id: 1, + state: 'active', + username: 'marcene', + name: 'Allie Will', + web_url: 'foo.com', + avatar_url: 'gravatar.com/avatar/xxx', + }; - const mountComponent = (propsData) => - shallowMount(Participants, { - propsData, - }); + const participants = [participant, { ...participant, id: 2 }, { ...participant, id: 3 }]; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findMoreParticipantsButton = () => wrapper.findComponent(GlButton); + const findCollapsedIcon = () => wrapper.find('.sidebar-collapsed-icon'); + const findParticipantsAuthor = () => wrapper.findAll('.participants-author'); + + const mountComponent = (propsData) => shallowMount(Participants, { propsData }); describe('collapsed sidebar state', () => { it('shows loading spinner when loading', () => { - wrapper = mountComponent({ - loading: true, - }); + wrapper = mountComponent({ loading: true }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('does not show loading spinner not loading', () => { - wrapper = mountComponent({ - loading: false, - }); + it('does not show loading spinner when not loading', () => { + wrapper = mountComponent({ loading: false }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(findLoadingIcon().exists()).toBe(false); }); it('shows participant count when given', () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - }); + wrapper = mountComponent({ participants }); - expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`); + expect(findCollapsedIcon().text()).toBe(participants.length.toString()); }); it('shows full participant count when there are hidden participants', () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 1, - }); + wrapper = mountComponent({ participants, numberOfLessParticipants: 1 }); - expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`); + expect(findCollapsedIcon().text()).toBe(participants.length.toString()); }); }); describe('expanded sidebar state', () => { it('shows loading spinner when loading', () => { - wrapper = mountComponent({ - loading: true, - }); + wrapper = mountComponent({ loading: true }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => { + it('when only showing visible participants, shows an avatar only for each participant under the limit', () => { const numberOfLessParticipants = 2; - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: false, - }); - - await nextTick(); - expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants); + wrapper = mountComponent({ participants, numberOfLessParticipants }); + + expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants); }); it('when only showing all participants, each has an avatar', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: true, - }); - - await nextTick(); - expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length); + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); + + await findMoreParticipantsButton().vm.$emit('click'); + + expect(findParticipantsAuthor()).toHaveLength(participants.length); }); it('does not have more participants link when they can all be shown', () => { const numberOfLessParticipants = 100; - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - - expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants); - expect(getMoreParticipantsButton().exists()).toBe(false); - }); + wrapper = mountComponent({ participants, numberOfLessParticipants }); - it('when too many participants, has more participants link to show more', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: false, - }); - - await nextTick(); - expect(getMoreParticipantsButton().text()).toBe('+ 1 more'); + expect(participants.length).toBeLessThan(numberOfLessParticipants); + expect(findMoreParticipantsButton().exists()).toBe(false); }); - it('when too many participants and already showing them, has more participants link to show less', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: true, - }); - - await nextTick(); - expect(getMoreParticipantsButton().text()).toBe('- show less'); - }); + it('when too many participants, has more participants link to show more', () => { + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); - it('clicking more participants link emits event', () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); + expect(findMoreParticipantsButton().text()).toBe('+ 1 more'); + }); - expect(wrapper.vm.isShowingMoreParticipants).toBe(false); + it('when too many participants and already showing them, has more participants link to show less', async () => { + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); - getMoreParticipantsButton().vm.$emit('click'); + await findMoreParticipantsButton().vm.$emit('click'); - expect(wrapper.vm.isShowingMoreParticipants).toBe(true); + expect(findMoreParticipantsButton().text()).toBe('- show less'); }); - it('clicking on participants icon emits `toggleSidebar` event', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - const spy = jest.spyOn(wrapper.vm, '$emit'); + it('clicking on participants icon emits `toggleSidebar` event', () => { + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); - wrapper.find('.sidebar-collapsed-icon').trigger('click'); + findCollapsedIcon().trigger('click'); - await nextTick(); - expect(spy).toHaveBeenCalledWith('toggleSidebar'); - spy.mockRestore(); + expect(wrapper.emitted('toggleSidebar')).toEqual([[]]); }); }); describe('when not showing participants label', () => { beforeEach(() => { - wrapper = mountComponent({ - participants: PARTICIPANT_LIST, - showParticipantLabel: false, - }); + wrapper = mountComponent({ participants, showParticipantLabel: false }); }); it('does not show sidebar collapsed icon', () => { - expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false); + expect(findCollapsedIcon().exists()).toBe(false); }); it('does not show participants label title', () => { 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 859e63b3df6..914e848eced 100644 --- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js +++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js @@ -35,7 +35,6 @@ describe('Sidebar Participants Widget', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js index 68ecd62e4c6..0f595ab21a5 100644 --- a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js +++ b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js @@ -16,11 +16,6 @@ describe('ReviewerTitle component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('reviewer title', () => { it('renders reviewer', () => { wrapper = createComponent({ diff --git a/spec/frontend/sidebar/components/reviewers/reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js index 229f7ffbe04..016ec9225da 100644 --- a/spec/frontend/sidebar/components/reviewers/reviewers_spec.js +++ b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js @@ -35,10 +35,6 @@ describe('Reviewer component', () => { const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *'); - afterEach(() => { - wrapper.destroy(); - }); - describe('No reviewers/users', () => { it('displays no reviewer icon when collapsed', () => { createWrapper(); diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index 57ae146a27a..a221d28704b 100644 --- a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -44,9 +44,6 @@ describe('sidebar reviewers', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; - SidebarService.singleton = null; SidebarStore.singleton = null; SidebarMediator.singleton = null; 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 d00c8dcb653..483449f924b 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -38,10 +38,6 @@ describe('UncollapsedReviewerList component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('single reviewer', () => { const user = userDataMock(); diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js index 71c6c259c32..8a27e82093d 100644 --- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js +++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js @@ -2,13 +2,14 @@ import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; -import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/constants'; +import { createAlert } from '~/alert'; +import { TYPE_INCIDENT } from '~/issues/constants'; +import { INCIDENT_SEVERITY } from '~/sidebar/constants'; import updateIssuableSeverity from '~/sidebar/queries/update_issuable_severity.mutation.graphql'; import SeverityToken from '~/sidebar/components/severity/severity.vue'; import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('SidebarSeverity', () => { let wrapper; @@ -22,7 +23,7 @@ describe('SidebarSeverity', () => { const propsData = { projectPath, iid, - issuableType: ISSUABLE_TYPES.INCIDENT, + issuableType: TYPE_INCIDENT, initialSeverity: severity, ...props, }; @@ -47,10 +48,6 @@ describe('SidebarSeverity', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findSeverityToken = () => wrapper.findAllComponents(SeverityToken); const findEditBtn = () => wrapper.findByTestId('edit-button'); const findDropdown = () => wrapper.findComponent(GlDropdown); diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js index 9f3d689edee..7a0044c00ac 100644 --- a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js +++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js @@ -10,7 +10,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { TYPE_ISSUE } from '~/issues/constants'; import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue'; import { IssuableAttributeType } from '~/sidebar/constants'; @@ -23,7 +23,7 @@ import { noCurrentMilestoneResponse, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('SidebarDropdown component', () => { let wrapper; diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js index 060a2873e04..53d81e3fcaf 100644 --- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js +++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js @@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { TYPE_ISSUE } from '~/issues/constants'; import { timeFor } from '~/lib/utils/datetime_utility'; @@ -27,7 +27,7 @@ import { mockMilestone2, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('SidebarDropdownWidget', () => { let wrapper; @@ -140,7 +140,7 @@ describe('SidebarDropdownWidget', () => { }, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, stubs: { SidebarEditableItem, @@ -157,11 +157,6 @@ describe('SidebarDropdownWidget', () => { jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when not editing', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/sidebar/components/status/status_dropdown_spec.js b/spec/frontend/sidebar/components/status/status_dropdown_spec.js index 5a75299c3a4..229b51ea568 100644 --- a/spec/frontend/sidebar/components/status/status_dropdown_spec.js +++ b/spec/frontend/sidebar/components/status/status_dropdown_spec.js @@ -14,10 +14,6 @@ describe('SubscriptionsDropdown component', () => { wrapper = shallowMount(StatusDropdown); } - afterEach(() => { - wrapper.destroy(); - }); - describe('with no value selected', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js index c94f9918243..7275557e7f2 100644 --- a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js +++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql'; @@ -15,7 +15,7 @@ import { mergeRequestSubscriptionMutationResponse, } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/vue_shared/plugins/global_toast'); Vue.use(VueApollo); @@ -62,7 +62,6 @@ describe('Sidebar Subscriptions Widget', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -138,7 +137,7 @@ describe('Sidebar Subscriptions Widget', () => { }); }); - it('displays a flash message when query is rejected', async () => { + it('displays an alert message when query is rejected', async () => { createComponent({ subscriptionsQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'), }); diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js index 3fb8214606c..eaf7bc13d20 100644 --- a/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js +++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js @@ -15,10 +15,6 @@ describe('SubscriptionsDropdown component', () => { wrapper = shallowMount(SubscriptionsDropdown); } - afterEach(() => { - wrapper.destroy(); - }); - describe('with no value selected', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js index 1a1aa370eef..cae21189ee0 100644 --- a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js +++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js @@ -16,11 +16,6 @@ describe('Subscriptions', () => { }), ); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('shows loading spinner when loading', () => { wrapper = mountComponent({ loading: true, diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js index 715f66d305a..a7c3867c359 100644 --- a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js +++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js @@ -47,8 +47,8 @@ describe('Create Timelog Form', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findDocsLink = () => wrapper.findByTestId('timetracking-docs-link'); const findSaveButton = () => findModal().props('actionPrimary'); - const findSaveButtonLoadingState = () => findSaveButton().attributes[0].loading; - const findSaveButtonDisabledState = () => findSaveButton().attributes[0].disabled; + const findSaveButtonLoadingState = () => findSaveButton().attributes.loading; + const findSaveButtonDisabledState = () => findSaveButton().attributes.disabled; const submitForm = () => findForm().trigger('submit'); diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js index 0259aee48f0..713ae83cbf1 100644 --- a/spec/frontend/sidebar/components/time_tracking/report_spec.js +++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js @@ -6,7 +6,7 @@ import VueApollo from 'vue-apollo'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import Report from '~/sidebar/components/time_tracking/report.vue'; import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.graphql'; import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql'; @@ -17,7 +17,7 @@ import { timelogToRemoveId, } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Issuable Time Tracking Report', () => { Vue.use(VueApollo); @@ -51,7 +51,6 @@ describe('Issuable Time Tracking Report', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js index 45d8b5e4647..91c013596d7 100644 --- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js +++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js @@ -32,7 +32,7 @@ describe('Issuable Time Tracker', () => { const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => { return mount(TimeTracker, { propsData: { ...defaultProps, ...props }, - directives: { GlTooltip: createMockDirective() }, + directives: { GlTooltip: createMockDirective('gl-tooltip') }, stubs: { transition: stubTransition(), }, @@ -53,10 +53,6 @@ describe('Issuable Time Tracker', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('Initialization', () => { beforeEach(() => { wrapper = mountComponent(); diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js index 5bfe3b59eb3..39b480b295c 100644 --- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js +++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js @@ -4,13 +4,13 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql'; import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue'; import { todosResponse, noTodosResponse } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -41,7 +41,6 @@ describe('Sidebar Todo Widget', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -77,7 +76,7 @@ describe('Sidebar Todo Widget', () => { }); }); - it('displays a flash message when query is rejected', async () => { + it('displays an alert message when query is rejected', async () => { createComponent({ todosQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'), }); diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js index fb07029a249..472a89e9b21 100644 --- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js +++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js @@ -22,7 +22,6 @@ describe('Todo Button', () => { }); afterEach(() => { - wrapper.destroy(); dispatchEventSpy = null; jest.clearAllMocks(); }); diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js index 8e6597bf80f..4da915f0dd3 100644 --- a/spec/frontend/sidebar/components/todo_toggle/todo_spec.js +++ b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js @@ -21,10 +21,6 @@ describe('SidebarTodo', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each` state | classes ${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']} diff --git a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js index cf9b2828dde..0370d5e337d 100644 --- a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js +++ b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js @@ -17,10 +17,6 @@ describe('ToggleSidebar', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findGlButton = () => wrapper.findComponent(GlButton); it('should render the "chevron-double-lg-left" icon when collapsed', () => { diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 391cbb1e0d5..844320efc1c 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -243,6 +243,7 @@ export const issuableDueDateResponse = (dueDate = null) => ({ __typename: 'Issue', id: 'gid://gitlab/Issue/4', dueDate, + dueDateFixed: dueDate, }, }, }, diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index 77b1ccb4f9a..f2003aee96e 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -7,7 +7,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import Mock from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/vue_shared/plugins/global_toast'); jest.mock('~/commons/nav/user_merge_requests'); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index fec300ddd7e..7eb0468c5be 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -28,10 +28,13 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = data-uploads-path="" > <markdown-header-stub + data-testid="markdownHeader" enablepreview="true" linecontent="" + markdownpreviewpath="foo/" restrictedtoolbaritems="" suggestionstartindex="0" + uploadspath="" /> <div diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index e7dab0ad79d..0d0e78e9179 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -9,7 +9,7 @@ import { stubPerformanceWebAPI } from 'helpers/performance'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import * as urlUtils from '~/lib/utils/url_utility'; import SnippetEditApp from '~/snippets/components/edit.vue'; import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue'; @@ -25,7 +25,7 @@ import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation. import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils'; -jest.mock('~/flash'); +jest.mock('~/alert'); const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js']; const TEST_API_ERROR = new Error('TEST_API_ERROR'); @@ -94,7 +94,6 @@ describe('Snippet Edit app', () => { let mutateSpy; const relativeUrlRoot = '/foo/'; - const originalRelativeUrlRoot = gon.relative_url_root; beforeEach(() => { stubPerformanceWebAPI(); @@ -108,12 +107,6 @@ describe('Snippet Edit app', () => { jest.spyOn(urlUtils, 'redirectTo').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - gon.relative_url_root = originalRelativeUrlRoot; - }); - const findBlobActions = () => wrapper.findComponent(SnippetBlobActionsEdit); const findCancelButton = () => wrapper.findByTestId('snippet-cancel-btn'); const clickSubmitBtn = () => wrapper.findByTestId('snippet-edit-form').trigger('submit'); @@ -132,10 +125,6 @@ describe('Snippet Edit app', () => { props = {}, selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING, } = {}) => { - if (wrapper) { - throw new Error('wrapper already created'); - } - const requestHandlers = [ [GetSnippetQuery, getSpy], // See `mutateSpy` declaration comment for why we send a key @@ -347,7 +336,7 @@ describe('Snippet Edit app', () => { projectPath ${'project/path'} ${''} - `('should flash error (projectPath=$projectPath)', async ({ projectPath }) => { + `('should alert error (projectPath=$projectPath)', async ({ projectPath }) => { mutateSpy.mockResolvedValue(createMutationResponseWithErrors('createSnippet')); await createComponentAndLoad({ @@ -373,7 +362,7 @@ describe('Snippet Edit app', () => { ${'project/path'} ${''} `( - 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)', + 'should alert error with (snippet=$snippetGid, projectPath=$projectPath)', async ({ projectPath }) => { mutateSpy.mockResolvedValue(createMutationResponseWithErrors('updateSnippet')); @@ -405,7 +394,7 @@ describe('Snippet Edit app', () => { expect(urlUtils.redirectTo).not.toHaveBeenCalled(); }); - it('should flash', () => { + it('should alert', () => { // Apollo automatically wraps the resolver's error in a NetworkError expect(createAlert).toHaveBeenCalledWith({ message: `Can't update snippet: ${TEST_API_ERROR.message}`, diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js index ed5ea6cab8a..d8c6ad3278a 100644 --- a/spec/frontend/snippets/components/embed_dropdown_spec.js +++ b/spec/frontend/snippets/components/embed_dropdown_spec.js @@ -17,11 +17,6 @@ describe('snippets/components/embed_dropdown', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findSectionsData = () => { const sections = []; let current = {}; diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index 032dcf8e5f5..45a7c7b0b4a 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -50,10 +50,6 @@ describe('Snippet view app', () => { stubPerformanceWebAPI(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders loader while the query is in flight', () => { createComponent({ loading: true }); expect(findLoadingIcon().exists()).toBe(true); diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js index a650353093d..58f47e8b0dc 100644 --- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js @@ -56,11 +56,6 @@ describe('snippets/components/snippet_blob_actions_edit', () => { const triggerBlobDelete = (idx) => findBlobEdits().at(idx).vm.$emit('delete'); const triggerBlobUpdate = (idx, props) => findBlobEdits().at(idx).vm.$emit('blob-updated', props); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('multi-file snippets rendering', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js index 82c4a37ccc9..b699e056576 100644 --- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -4,14 +4,14 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { joinPaths } from '~/lib/utils/url_utility'; import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; import SourceEditor from '~/vue_shared/components/source_editor.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); const TEST_ID = 'blob_local_7'; const TEST_PATH = 'foo/bar/test.md'; @@ -62,8 +62,6 @@ describe('Snippet Blob Edit component', () => { }); afterEach(() => { - wrapper.destroy(); - wrapper = null; axiosMock.restore(); }); @@ -123,7 +121,7 @@ describe('Snippet Blob Edit component', () => { createComponent(); }); - it('should call flash', async () => { + it('should call alert', async () => { await waitForPromises(); expect(createAlert).toHaveBeenCalledWith({ diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index c7ff8c21d80..840bca8c9c8 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -62,10 +62,6 @@ describe('Blob Embeddable', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { it('renders correct components', () => { createComponent(); diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js index ff75515e71a..2b42eba19c2 100644 --- a/spec/frontend/snippets/components/snippet_description_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js @@ -30,10 +30,6 @@ describe('Snippet Description Edit component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { it('matches the snapshot', () => { expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/snippets/components/snippet_description_view_spec.js b/spec/frontend/snippets/components/snippet_description_view_spec.js index 14f116f2aaf..3c5d50ccaa6 100644 --- a/spec/frontend/snippets/components/snippet_description_view_spec.js +++ b/spec/frontend/snippets/components/snippet_description_view_spec.js @@ -17,10 +17,6 @@ describe('Snippet Description component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('matches the snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index c930c9f635b..994cf65c1f5 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -1,8 +1,9 @@ -import { GlButton, GlModal, GlDropdown } from '@gitlab/ui'; +import { GlModal, GlButton, GlDropdown } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import { ApolloMutation } from 'vue-apollo'; +import VueApollo from 'vue-apollo'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; @@ -10,31 +11,41 @@ import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue'; import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql'; import axios from '~/lib/utils/axios_utils'; -import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert'; +import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql'; +import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql'; +import { getCanCreateProjectSnippetMock, getCanCreatePersonalSnippetMock } from '../mock_data'; -jest.mock('~/flash'); +const ERROR_MSG = 'Foo bar'; +const ERR = { message: ERROR_MSG }; + +const MUTATION_TYPES = { + RESOLVE: jest.fn().mockResolvedValue({ data: { destroySnippet: { errors: [] } } }), + REJECT: jest.fn().mockRejectedValue(ERR), +}; + +jest.mock('~/alert'); + +Vue.use(VueApollo); describe('Snippet header component', () => { let wrapper; let snippet; - let mutationTypes; - let mutationVariables; let mock; + let mockApollo; - let errorMsg; - let err; - const originalRelativeUrlRoot = gon.relative_url_root; const reportAbusePath = '/-/snippets/42/mark_as_spam'; const canReportSpam = true; const GlEmoji = { template: '<img/>' }; function createComponent({ - loading = false, permissions = {}, - mutationRes = mutationTypes.RESOLVE, snippetProps = {}, provide = {}, + canCreateProjectSnippetMock = jest.fn().mockResolvedValue(getCanCreateProjectSnippetMock()), + canCreatePersonalSnippetMock = jest.fn().mockResolvedValue(getCanCreatePersonalSnippetMock()), + deleteSnippetMock = MUTATION_TYPES.RESOLVE, } = {}) { const defaultProps = Object.assign(snippet, snippetProps); if (permissions) { @@ -42,17 +53,14 @@ describe('Snippet header component', () => { ...permissions, }); } - const $apollo = { - queries: { - canCreateSnippet: { - loading, - }, - }, - mutate: mutationRes, - }; + + mockApollo = createMockApollo([ + [CanCreateProjectSnippet, canCreateProjectSnippetMock], + [CanCreatePersonalSnippet, canCreatePersonalSnippetMock], + [DeleteSnippetMutation, deleteSnippetMock], + ]); wrapper = mount(SnippetHeader, { - mocks: { $apollo }, provide: { reportAbusePath, canReportSpam, @@ -64,9 +72,9 @@ describe('Snippet header component', () => { }, }, stubs: { - ApolloMutation, GlEmoji, }, + apolloProvider: mockApollo, }); } @@ -91,6 +99,7 @@ describe('Snippet header component', () => { title: x.attributes('title'), text: x.text(), })); + const findDeleteModal = () => wrapper.findComponent(GlModal); beforeEach(() => { gon.relative_url_root = '/foo/'; @@ -113,28 +122,12 @@ describe('Snippet header component', () => { createdAt: new Date(differenceInMilliseconds(32 * 24 * 3600 * 1000)).toISOString(), }; - mutationVariables = { - mutation: DeleteSnippetMutation, - variables: { - id: snippet.id, - }, - }; - - errorMsg = 'Foo bar'; - err = { message: errorMsg }; - - mutationTypes = { - RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })), - REJECT: jest.fn(() => Promise.reject(err)), - }; - mock = new MockAdapter(axios); }); afterEach(() => { - wrapper.destroy(); + mockApollo = null; mock.restore(); - gon.relative_url_root = originalRelativeUrlRoot; }); it('renders itself', () => { @@ -238,15 +231,16 @@ describe('Snippet header component', () => { }); it('with canCreateSnippet permission, renders create button', async () => { - createComponent(); + createComponent({ + canCreateProjectSnippetMock: jest + .fn() + .mockResolvedValue(getCanCreateProjectSnippetMock(true)), + canCreatePersonalSnippetMock: jest + .fn() + .mockResolvedValue(getCanCreatePersonalSnippetMock(true)), + }); - // TODO: we should avoid `wrapper.setData` since they - // are component internals. Let's use the apollo mock helpers - // in a follow-up. - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ canCreateSnippet: true }); - await nextTick(); + await waitForPromises(); expect(findButtonsAsModel()).toEqual( expect.arrayContaining([ @@ -271,7 +265,7 @@ describe('Snippet header component', () => { ${200} | ${VARIANT_SUCCESS} | ${i18n.snippetSpamSuccess} ${500} | ${VARIANT_DANGER} | ${i18n.snippetSpamFailure} `( - 'renders a "$variant" flash message with "$text" message for a request with a "$request" response', + 'renders a "$variant" alert message with "$text" message for a request with a "$request" response', async ({ request, variant, text }) => { const submitAsSpamBtn = findButtons().at(2); mock.onPost(reportAbusePath).reply(request); @@ -329,21 +323,37 @@ describe('Snippet header component', () => { }); describe('Delete mutation', () => { - it('dispatches a mutation to delete the snippet with correct variables', () => { + const deleteSnippet = async () => { + // Click delete action + findButtons().at(1).trigger('click'); + await nextTick(); + + expect(findDeleteModal().props().visible).toBe(true); + + // Click delete button in delete modal + document.querySelector('[data-testid="delete-snippet"').click(); + await waitForPromises(); + }; + + it('dispatches a mutation to delete the snippet with correct variables', async () => { createComponent(); - wrapper.vm.deleteSnippet(); - expect(mutationTypes.RESOLVE).toHaveBeenCalledWith(mutationVariables); + + await deleteSnippet(); + + expect(MUTATION_TYPES.RESOLVE).toHaveBeenCalledWith({ + id: snippet.id, + }); }); it('sets error message if mutation fails', async () => { - createComponent({ mutationRes: mutationTypes.REJECT }); + createComponent({ deleteSnippetMock: MUTATION_TYPES.REJECT }); expect(Boolean(wrapper.vm.errorMessage)).toBe(false); - wrapper.vm.deleteSnippet(); - - await waitForPromises(); + await deleteSnippet(); - expect(wrapper.vm.errorMessage).toEqual(errorMsg); + expect(document.querySelector('[data-testid="delete-alert"').textContent.trim()).toBe( + ERROR_MSG, + ); }); describe('in case of successful mutation, closes modal and redirects to correct listing', () => { @@ -353,15 +363,16 @@ describe('Snippet header component', () => { createComponent({ snippetProps, }); - wrapper.vm.closeDeleteModal = jest.fn(); - wrapper.vm.deleteSnippet(); - await nextTick(); + await deleteSnippet(); }; it('redirects to dashboard/snippets for personal snippet', async () => { await createDeleteSnippet(); - expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled(); + + // Check that the modal is hidden after deleting the snippet + expect(findDeleteModal().props().visible).toBe(false); + expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`); }); @@ -372,7 +383,10 @@ describe('Snippet header component', () => { fullPath, }, }); - expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled(); + + // Check that the modal is hidden after deleting the snippet + expect(findDeleteModal().props().visible).toBe(false); + expect(window.location.pathname).toBe(`${fullPath}/-/snippets`); }); }); diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js index 7c40735d64e..0a3b57c9244 100644 --- a/spec/frontend/snippets/components/snippet_title_spec.js +++ b/spec/frontend/snippets/components/snippet_title_spec.js @@ -26,10 +26,6 @@ describe('Snippet header component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders itself', () => { createComponent(); expect(wrapper.find('.snippet-header').exists()).toBe(true); diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js index 29eb002ef4a..70eb719f706 100644 --- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js @@ -51,10 +51,6 @@ describe('Snippet Visibility Edit component', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('rendering', () => { it('matches the snapshot', () => { createComponent(); diff --git a/spec/frontend/snippets/mock_data.js b/spec/frontend/snippets/mock_data.js new file mode 100644 index 00000000000..7546fa575c6 --- /dev/null +++ b/spec/frontend/snippets/mock_data.js @@ -0,0 +1,19 @@ +export const getCanCreateProjectSnippetMock = (createSnippet = false) => ({ + data: { + project: { + userPermissions: { + createSnippet, + }, + }, + }, +}); + +export const getCanCreatePersonalSnippetMock = (createSnippet = false) => ({ + data: { + currentUser: { + userPermissions: { + createSnippet, + }, + }, + }, +}); diff --git a/spec/frontend/streaming/chunk_writer_spec.js b/spec/frontend/streaming/chunk_writer_spec.js new file mode 100644 index 00000000000..2aadb332838 --- /dev/null +++ b/spec/frontend/streaming/chunk_writer_spec.js @@ -0,0 +1,214 @@ +import { ChunkWriter } from '~/streaming/chunk_writer'; +import { RenderBalancer } from '~/streaming/render_balancer'; + +jest.mock('~/streaming/render_balancer'); + +describe('ChunkWriter', () => { + let accumulator = ''; + let write; + let close; + let abort; + let config; + let render; + + const createChunk = (text) => { + const encoder = new TextEncoder(); + return encoder.encode(text); + }; + + const createHtmlStream = () => { + write = jest.fn((part) => { + accumulator += part; + }); + close = jest.fn(); + abort = jest.fn(); + return { + write, + close, + abort, + }; + }; + + const createWriter = () => { + return new ChunkWriter(createHtmlStream(), config); + }; + + const pushChunks = (...chunks) => { + const writer = createWriter(); + chunks.forEach((chunk) => { + writer.write(createChunk(chunk)); + }); + writer.close(); + }; + + afterAll(() => { + global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined; + }); + + beforeEach(() => { + global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = 100; + accumulator = ''; + config = undefined; + render = jest.fn((cb) => { + while (cb()) { + // render until 'false' + } + }); + RenderBalancer.mockImplementation(() => ({ render })); + }); + + describe('when chunk length must be "1"', () => { + beforeEach(() => { + config = { minChunkSize: 1, maxChunkSize: 1 }; + }); + + it('splits big chunks into smaller ones', () => { + const text = 'foobar'; + pushChunks(text); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(text.length); + }); + + it('handles small emoji chunks', () => { + const text = 'foo👀bar👨👩👧baz👧👧🏻👧🏼👧🏽👧🏾👧🏿'; + pushChunks(text); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(createChunk(text).length); + }); + }); + + describe('when chunk length must not be lower than "5" and exceed "10"', () => { + beforeEach(() => { + config = { minChunkSize: 5, maxChunkSize: 10 }; + }); + + it('joins small chunks', () => { + const text = '12345'; + pushChunks(...text.split('')); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(1); + expect(close).toHaveBeenCalledTimes(1); + }); + + it('handles overflow with small chunks', () => { + const text = '123456789'; + pushChunks(...text.split('')); + expect(accumulator).toBe(text); + expect(write).toHaveBeenCalledTimes(2); + expect(close).toHaveBeenCalledTimes(1); + }); + + it('calls flush on small chunks', () => { + global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined; + const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator'); + const text = '1'; + pushChunks(text); + expect(accumulator).toBe(text); + expect(flushAccumulator).toHaveBeenCalledTimes(1); + }); + + it('calls flush on large chunks', () => { + const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator'); + const text = '1234567890123'; + const writer = createWriter(); + writer.write(createChunk(text)); + jest.runAllTimers(); + expect(accumulator).toBe(text); + expect(flushAccumulator).toHaveBeenCalledTimes(1); + }); + }); + + describe('chunk balancing', () => { + let increase; + let decrease; + let renderOnce; + + beforeEach(() => { + render = jest.fn((cb) => { + let next = true; + renderOnce = () => { + if (!next) return; + next = cb(); + }; + }); + RenderBalancer.mockImplementation(({ increase: inc, decrease: dec }) => { + increase = jest.fn(inc); + decrease = jest.fn(dec); + return { + render, + }; + }); + }); + + describe('when frame time exceeds low limit', () => { + beforeEach(() => { + config = { + minChunkSize: 1, + maxChunkSize: 5, + balanceRate: 10, + }; + }); + + it('increases chunk size', () => { + const text = '111222223'; + const writer = createWriter(); + const chunk = createChunk(text); + + writer.write(chunk); + + renderOnce(); + increase(); + renderOnce(); + renderOnce(); + + writer.close(); + + expect(accumulator).toBe(text); + expect(write.mock.calls).toMatchObject([['111'], ['22222'], ['3']]); + expect(close).toHaveBeenCalledTimes(1); + }); + }); + + describe('when frame time exceeds high limit', () => { + beforeEach(() => { + config = { + minChunkSize: 1, + maxChunkSize: 10, + balanceRate: 2, + }; + }); + + it('decreases chunk size', () => { + const text = '1111112223345'; + const writer = createWriter(); + const chunk = createChunk(text); + + writer.write(chunk); + + renderOnce(); + decrease(); + + renderOnce(); + decrease(); + + renderOnce(); + decrease(); + + renderOnce(); + renderOnce(); + + writer.close(); + + expect(accumulator).toBe(text); + expect(write.mock.calls).toMatchObject([['111111'], ['222'], ['33'], ['4'], ['5']]); + expect(close).toHaveBeenCalledTimes(1); + }); + }); + }); + + it('calls abort on htmlStream', () => { + const writer = createWriter(); + writer.abort(); + expect(abort).toHaveBeenCalledTimes(1); + }); +}); diff --git a/spec/frontend/streaming/handle_streamed_anchor_link_spec.js b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js new file mode 100644 index 00000000000..ef17957b2fc --- /dev/null +++ b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js @@ -0,0 +1,132 @@ +import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; +import waitForPromises from 'helpers/wait_for_promises'; +import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link'; +import { scrollToElement } from '~/lib/utils/common_utils'; +import LineHighlighter from '~/blob/line_highlighter'; +import { TEST_HOST } from 'spec/test_constants'; + +jest.mock('~/lib/utils/common_utils'); +jest.mock('~/blob/line_highlighter'); + +describe('handleStreamedAnchorLink', () => { + const ANCHOR_START = 'L100'; + const ANCHOR_END = '300'; + const findRoot = () => document.querySelector('#root'); + + afterEach(() => { + resetHTMLFixture(); + }); + + describe('when single line anchor is given', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(`${TEST_HOST}#${ANCHOR_START}`); + }); + + describe('when element is present', () => { + beforeEach(() => { + setHTMLFixture(`<div id="root"><div id="${ANCHOR_START}"></div></div>`); + handleStreamedAnchorLink(findRoot()); + }); + + it('does nothing', async () => { + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + + describe('when element is streamed', () => { + let stop; + const insertElement = () => { + findRoot().insertAdjacentHTML('afterbegin', `<div id="${ANCHOR_START}"></div>`); + }; + + beforeEach(() => { + setHTMLFixture('<div id="root"></div>'); + stop = handleStreamedAnchorLink(findRoot()); + }); + + afterEach(() => { + stop = undefined; + }); + + it('scrolls to the anchor when inserted', async () => { + insertElement(); + await waitForPromises(); + expect(scrollToElement).toHaveBeenCalledTimes(1); + expect(LineHighlighter).toHaveBeenCalledTimes(1); + }); + + it("doesn't scroll to the anchor when destroyed", async () => { + stop(); + insertElement(); + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when line range anchor is given', () => { + beforeEach(() => { + delete window.location; + window.location = new URL(`${TEST_HOST}#${ANCHOR_START}-${ANCHOR_END}`); + }); + + describe('when last element is present', () => { + beforeEach(() => { + setHTMLFixture(`<div id="root"><div id="L${ANCHOR_END}"></div></div>`); + handleStreamedAnchorLink(findRoot()); + }); + + it('does nothing', async () => { + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + + describe('when last element is streamed', () => { + let stop; + const insertElement = () => { + findRoot().insertAdjacentHTML( + 'afterbegin', + `<div id="${ANCHOR_START}"></div><div id="L${ANCHOR_END}"></div>`, + ); + }; + + beforeEach(() => { + setHTMLFixture('<div id="root"></div>'); + stop = handleStreamedAnchorLink(findRoot()); + }); + + afterEach(() => { + stop = undefined; + }); + + it('scrolls to the anchor when inserted', async () => { + insertElement(); + await waitForPromises(); + expect(scrollToElement).toHaveBeenCalledTimes(1); + expect(LineHighlighter).toHaveBeenCalledTimes(1); + }); + + it("doesn't scroll to the anchor when destroyed", async () => { + stop(); + insertElement(); + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when anchor is not given', () => { + beforeEach(() => { + setHTMLFixture(`<div id="root"></div>`); + handleStreamedAnchorLink(findRoot()); + }); + + it('does nothing', async () => { + await waitForPromises(); + expect(scrollToElement).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/streaming/html_stream_spec.js b/spec/frontend/streaming/html_stream_spec.js new file mode 100644 index 00000000000..115a9ddc803 --- /dev/null +++ b/spec/frontend/streaming/html_stream_spec.js @@ -0,0 +1,46 @@ +import { HtmlStream } from '~/streaming/html_stream'; +import { ChunkWriter } from '~/streaming/chunk_writer'; + +jest.mock('~/streaming/chunk_writer'); + +describe('HtmlStream', () => { + let write; + let close; + let streamingElement; + + beforeEach(() => { + write = jest.fn(); + close = jest.fn(); + jest.spyOn(Document.prototype, 'write').mockImplementation(write); + jest.spyOn(Document.prototype, 'close').mockImplementation(close); + jest.spyOn(Document.prototype, 'querySelector').mockImplementation(() => { + streamingElement = document.createElement('div'); + return streamingElement; + }); + }); + + it('attaches to original document', () => { + // eslint-disable-next-line no-new + new HtmlStream(document.body); + expect(document.body.contains(streamingElement)).toBe(true); + }); + + it('can write to a document', () => { + const htmlStream = new HtmlStream(document.body); + htmlStream.write('foo'); + htmlStream.close(); + expect(write.mock.calls).toEqual([['<streaming-element>'], ['foo'], ['</streaming-element>']]); + expect(close).toHaveBeenCalledTimes(1); + }); + + it('returns chunked writer', () => { + const htmlStream = new HtmlStream(document.body).withChunkWriter(); + expect(htmlStream).toBeInstanceOf(ChunkWriter); + }); + + it('closes on abort', () => { + const htmlStream = new HtmlStream(document.body); + htmlStream.abort(); + expect(close).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/streaming/rate_limit_stream_requests_spec.js b/spec/frontend/streaming/rate_limit_stream_requests_spec.js new file mode 100644 index 00000000000..02e3cf93014 --- /dev/null +++ b/spec/frontend/streaming/rate_limit_stream_requests_spec.js @@ -0,0 +1,155 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests'; + +describe('rateLimitStreamRequests', () => { + const encoder = new TextEncoder('utf-8'); + const createStreamResponse = (content = 'foo') => + new ReadableStream({ + pull(controller) { + controller.enqueue(encoder.encode(content)); + controller.close(); + }, + }); + + const createFactory = (content) => { + return jest.fn(() => { + return Promise.resolve(createStreamResponse(content)); + }); + }; + + it('does nothing for zero total requests', () => { + const factory = jest.fn(); + const requests = rateLimitStreamRequests({ + factory, + total: 0, + }); + expect(factory).toHaveBeenCalledTimes(0); + expect(requests.length).toBe(0); + }); + + it('does not exceed total requests', () => { + const factory = createFactory(); + const requests = rateLimitStreamRequests({ + factory, + immediateCount: 100, + maxConcurrentRequests: 100, + total: 2, + }); + expect(factory).toHaveBeenCalledTimes(2); + expect(requests.length).toBe(2); + }); + + it('creates immediate requests', () => { + const factory = createFactory(); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 2, + total: 2, + }); + expect(factory).toHaveBeenCalledTimes(2); + expect(requests.length).toBe(2); + }); + + it('returns correct values', async () => { + const fixture = 'foobar'; + const factory = createFactory(fixture); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 2, + total: 2, + }); + + const decoder = new TextDecoder('utf-8'); + let result = ''; + for await (const stream of requests) { + await stream.pipeTo( + new WritableStream({ + // eslint-disable-next-line no-loop-func + write(content) { + result += decoder.decode(content); + }, + }), + ); + } + + expect(result).toBe(fixture + fixture); + }); + + it('delays rate limited requests', async () => { + const factory = createFactory(); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 2, + total: 3, + }); + expect(factory).toHaveBeenCalledTimes(2); + expect(requests.length).toBe(3); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(3); + }); + + it('runs next request after previous has been fulfilled', async () => { + let res; + const factory = jest + .fn() + .mockImplementationOnce( + () => + new Promise((resolve) => { + res = resolve; + }), + ) + .mockImplementationOnce(() => Promise.resolve(createStreamResponse())); + const requests = rateLimitStreamRequests({ + factory, + maxConcurrentRequests: 1, + total: 2, + }); + expect(factory).toHaveBeenCalledTimes(1); + expect(requests.length).toBe(2); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(1); + + res(createStreamResponse()); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(2); + }); + + it('uses timer to schedule next request', async () => { + let res; + const factory = jest + .fn() + .mockImplementationOnce( + () => + new Promise((resolve) => { + res = resolve; + }), + ) + .mockImplementationOnce(() => Promise.resolve(createStreamResponse())); + const requests = rateLimitStreamRequests({ + factory, + immediateCount: 1, + maxConcurrentRequests: 2, + total: 2, + timeout: 9999, + }); + expect(factory).toHaveBeenCalledTimes(1); + expect(requests.length).toBe(2); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(1); + + jest.runAllTimers(); + + await waitForPromises(); + + expect(factory).toHaveBeenCalledTimes(2); + res(createStreamResponse()); + }); +}); diff --git a/spec/frontend/streaming/render_balancer_spec.js b/spec/frontend/streaming/render_balancer_spec.js new file mode 100644 index 00000000000..dae0c98d678 --- /dev/null +++ b/spec/frontend/streaming/render_balancer_spec.js @@ -0,0 +1,69 @@ +import { RenderBalancer } from '~/streaming/render_balancer'; + +const HIGH_FRAME_TIME = 100; +const LOW_FRAME_TIME = 10; + +describe('renderBalancer', () => { + let frameTime = 0; + let frameTimeDelta = 0; + let decrease; + let increase; + + const createBalancer = () => { + decrease = jest.fn(); + increase = jest.fn(); + return new RenderBalancer({ + highFrameTime: HIGH_FRAME_TIME, + lowFrameTime: LOW_FRAME_TIME, + increase, + decrease, + }); + }; + + const renderTimes = (times) => { + const balancer = createBalancer(); + return new Promise((resolve) => { + let counter = 0; + balancer.render(() => { + if (counter === times) { + resolve(counter); + return false; + } + counter += 1; + return true; + }); + }); + }; + + beforeEach(() => { + jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + frameTime += frameTimeDelta; + cb(frameTime); + }); + }); + + afterEach(() => { + window.requestAnimationFrame.mockRestore(); + frameTime = 0; + frameTimeDelta = 0; + }); + + it('renders in a loop', async () => { + const count = await renderTimes(5); + expect(count).toBe(5); + }); + + it('calls decrease', async () => { + frameTimeDelta = 200; + await renderTimes(5); + expect(decrease).toHaveBeenCalled(); + expect(increase).not.toHaveBeenCalled(); + }); + + it('calls increase', async () => { + frameTimeDelta = 1; + await renderTimes(5); + expect(increase).toHaveBeenCalled(); + expect(decrease).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/streaming/render_html_streams_spec.js b/spec/frontend/streaming/render_html_streams_spec.js new file mode 100644 index 00000000000..55cef0ea469 --- /dev/null +++ b/spec/frontend/streaming/render_html_streams_spec.js @@ -0,0 +1,96 @@ +import { ReadableStream } from 'node:stream/web'; +import { renderHtmlStreams } from '~/streaming/render_html_streams'; +import { HtmlStream } from '~/streaming/html_stream'; +import waitForPromises from 'helpers/wait_for_promises'; + +jest.mock('~/streaming/html_stream'); +jest.mock('~/streaming/constants', () => { + return { + HIGH_FRAME_TIME: 0, + LOW_FRAME_TIME: 0, + MAX_CHUNK_SIZE: 1, + MIN_CHUNK_SIZE: 1, + }; +}); + +const firstStreamContent = 'foobar'; +const secondStreamContent = 'bazqux'; + +describe('renderHtmlStreams', () => { + let htmlWriter; + const encoder = new TextEncoder(); + const createSingleChunkStream = (chunk) => { + const encoded = encoder.encode(chunk); + const stream = new ReadableStream({ + pull(controller) { + controller.enqueue(encoded); + controller.close(); + }, + }); + return [stream, encoded]; + }; + + beforeEach(() => { + htmlWriter = { + write: jest.fn(), + close: jest.fn(), + abort: jest.fn(), + }; + jest.spyOn(HtmlStream.prototype, 'withChunkWriter').mockReturnValue(htmlWriter); + }); + + it('renders a single stream', async () => { + const [stream, encoded] = createSingleChunkStream(firstStreamContent); + + await renderHtmlStreams([Promise.resolve(stream)], document.body); + + expect(htmlWriter.write).toHaveBeenCalledWith(encoded); + expect(htmlWriter.close).toHaveBeenCalledTimes(1); + }); + + it('renders stream sequence', async () => { + const [stream1, encoded1] = createSingleChunkStream(firstStreamContent); + const [stream2, encoded2] = createSingleChunkStream(secondStreamContent); + + await renderHtmlStreams([Promise.resolve(stream1), Promise.resolve(stream2)], document.body); + + expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]); + expect(htmlWriter.close).toHaveBeenCalledTimes(1); + }); + + it("doesn't wait for the whole sequence to resolve before streaming", async () => { + const [stream1, encoded1] = createSingleChunkStream(firstStreamContent); + const [stream2, encoded2] = createSingleChunkStream(secondStreamContent); + + let res; + const delayedStream = new Promise((resolve) => { + res = resolve; + }); + + renderHtmlStreams([Promise.resolve(stream1), delayedStream], document.body); + + await waitForPromises(); + + expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1]]); + expect(htmlWriter.close).toHaveBeenCalledTimes(0); + + res(stream2); + await waitForPromises(); + + expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]); + expect(htmlWriter.close).toHaveBeenCalledTimes(1); + }); + + it('closes HtmlStream on error', async () => { + const [stream1] = createSingleChunkStream(firstStreamContent); + const error = new Error(); + + try { + await renderHtmlStreams([Promise.resolve(stream1), Promise.reject(error)], document.body); + } catch (err) { + expect(err).toBe(error); + } + + expect(htmlWriter.abort).toHaveBeenCalledTimes(1); + }); +}); diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js new file mode 100644 index 00000000000..538e87cf843 --- /dev/null +++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js @@ -0,0 +1,219 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlSearchBoxByType } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { s__ } from '~/locale'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue'; +import ProjectsList from '~/super_sidebar/components/projects_list.vue'; +import GroupsList from '~/super_sidebar/components/groups_list.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import searchUserProjectsAndGroupsQuery from '~/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql'; +import { trackContextAccess, formatContextSwitcherItems } from '~/super_sidebar/utils'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import { stubComponent } from 'helpers/stub_component'; +import { searchUserProjectsAndGroupsResponseMock } from '../mock_data'; + +jest.mock('~/super_sidebar/utils', () => ({ + getStorageKeyFor: jest.requireActual('~/super_sidebar/utils').getStorageKeyFor, + getTopFrequentItems: jest.requireActual('~/super_sidebar/utils').getTopFrequentItems, + formatContextSwitcherItems: jest.requireActual('~/super_sidebar/utils') + .formatContextSwitcherItems, + trackContextAccess: jest.fn(), +})); + +const username = 'root'; +const projectsPath = 'projectsPath'; +const groupsPath = 'groupsPath'; + +Vue.use(VueApollo); + +describe('ContextSwitcher component', () => { + let wrapper; + let mockApollo; + + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findProjectsList = () => wrapper.findComponent(ProjectsList); + const findGroupsList = () => wrapper.findComponent(GroupsList); + + const triggerSearchQuery = async () => { + findSearchBox().vm.$emit('input', 'foo'); + await nextTick(); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + return waitForPromises(); + }; + + const searchUserProjectsAndGroupsHandlerSuccess = jest + .fn() + .mockResolvedValue(searchUserProjectsAndGroupsResponseMock); + + const createWrapper = ({ props = {}, requestHandlers = {} } = {}) => { + mockApollo = createMockApollo([ + [ + searchUserProjectsAndGroupsQuery, + requestHandlers.searchUserProjectsAndGroupsQueryHandler ?? + searchUserProjectsAndGroupsHandlerSuccess, + ], + ]); + + wrapper = shallowMountExtended(ContextSwitcher, { + apolloProvider: mockApollo, + propsData: { + username, + projectsPath, + groupsPath, + ...props, + }, + stubs: { + GlSearchBoxByType: stubComponent(GlSearchBoxByType, { + props: ['placeholder'], + }), + ProjectsList: stubComponent(ProjectsList, { + props: ['username', 'viewAllLink', 'isSearch', 'searchResults'], + }), + GroupsList: stubComponent(GroupsList, { + props: ['username', 'viewAllLink', 'isSearch', 'searchResults'], + }), + }, + }); + }; + + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); + + it('passes the placeholder to the search box', () => { + expect(findSearchBox().props('placeholder')).toBe( + s__('Navigation|Search for projects or groups'), + ); + }); + + it('passes the correct props the frequent projects list', () => { + expect(findProjectsList().props()).toEqual({ + username, + viewAllLink: projectsPath, + isSearch: false, + searchResults: [], + }); + }); + + it('passes the correct props the frequent groups list', () => { + expect(findGroupsList().props()).toEqual({ + username, + viewAllLink: groupsPath, + isSearch: false, + searchResults: [], + }); + }); + + it('does not trigger the search query on mount', () => { + expect(searchUserProjectsAndGroupsHandlerSuccess).not.toHaveBeenCalled(); + }); + }); + + describe('item access tracking', () => { + it('does not track anything if not within a trackable context', () => { + createWrapper(); + + expect(trackContextAccess).not.toHaveBeenCalled(); + }); + + it('tracks item access if within a trackable context', () => { + const currentContext = { namespace: 'groups' }; + createWrapper({ + props: { + currentContext, + }, + }); + + expect(trackContextAccess).toHaveBeenCalledWith(username, currentContext); + }); + }); + + describe('on search', () => { + beforeEach(() => { + createWrapper(); + return triggerSearchQuery(); + }); + + it('triggers the search query on search', () => { + expect(searchUserProjectsAndGroupsHandlerSuccess).toHaveBeenCalled(); + }); + + it('passes the projects to the frequent projects list', () => { + expect(findProjectsList().props('isSearch')).toBe(true); + expect(findProjectsList().props('searchResults')).toEqual( + formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.projects.nodes), + ); + }); + + it('passes the groups to the frequent groups list', () => { + expect(findGroupsList().props('isSearch')).toBe(true); + expect(findGroupsList().props('searchResults')).toEqual( + formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.user.groups.nodes), + ); + }); + }); + + describe('when search query does not match any items', () => { + beforeEach(() => { + createWrapper({ + requestHandlers: { + searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({ + data: { + projects: { + nodes: [], + }, + user: { + id: '1', + groups: { + nodes: [], + }, + }, + }, + }), + }, + }); + return triggerSearchQuery(); + }); + + it('passes empty results to the lists', () => { + expect(findProjectsList().props('isSearch')).toBe(true); + expect(findProjectsList().props('searchResults')).toEqual([]); + expect(findGroupsList().props('isSearch')).toBe(true); + expect(findGroupsList().props('searchResults')).toEqual([]); + }); + }); + + describe('when search query fails', () => { + beforeEach(() => { + jest.spyOn(Sentry, 'captureException'); + }); + + it('captures exception if response is formatted incorrectly', async () => { + createWrapper({ + requestHandlers: { + searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({ + data: {}, + }), + }, + }); + await triggerSearchQuery(); + + expect(Sentry.captureException).toHaveBeenCalled(); + }); + + it('captures exception if query fails', async () => { + createWrapper({ + requestHandlers: { + searchUserProjectsAndGroupsQueryHandler: jest.fn().mockRejectedValue(), + }, + }); + await triggerSearchQuery(); + + expect(Sentry.captureException).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js new file mode 100644 index 00000000000..7172b60d0fa --- /dev/null +++ b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js @@ -0,0 +1,50 @@ +import { GlAvatar } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue'; + +describe('ContextSwitcherToggle component', () => { + let wrapper; + + const context = { + id: 1, + title: 'Title', + avatar: '/path/to/avatar.png', + }; + + const findGlAvatar = () => wrapper.getComponent(GlAvatar); + + const createWrapper = (props = {}) => { + wrapper = shallowMountExtended(ContextSwitcherToggle, { + propsData: { + context, + expanded: false, + ...props, + }, + }); + }; + + describe('with an avatar', () => { + it('passes the correct props to GlAvatar', () => { + createWrapper(); + const avatar = findGlAvatar(); + + expect(avatar.props('shape')).toBe('rect'); + expect(avatar.props('entityName')).toBe(context.title); + expect(avatar.props('entityId')).toBe(context.id); + expect(avatar.props('src')).toBe(context.avatar); + }); + + it('renders the avatar with a custom shape', () => { + const customShape = 'circle'; + createWrapper({ + context: { + ...context, + avatar_shape: customShape, + }, + }); + const avatar = findGlAvatar(); + + expect(avatar.props('shape')).toBe(customShape); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js new file mode 100644 index 00000000000..1e98db091f2 --- /dev/null +++ b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js @@ -0,0 +1,68 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; +import FrequentItemsList from '~/super_sidebar/components//frequent_items_list.vue'; +import ItemsList from '~/super_sidebar/components/items_list.vue'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import { cachedFrequentProjects } from '../mock_data'; + +const title = s__('Navigation|FREQUENT PROJECTS'); +const pristineText = s__('Navigation|Projects you visit often will appear here.'); +const storageKey = 'storageKey'; +const maxItems = 5; + +describe('FrequentItemsList component', () => { + useLocalStorageSpy(); + + let wrapper; + + const findListTitle = () => wrapper.findByTestId('list-title'); + const findItemsList = () => wrapper.findComponent(ItemsList); + const findEmptyText = () => wrapper.findByTestId('empty-text'); + + const createWrapper = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(FrequentItemsList, { + propsData: { + title, + pristineText, + storageKey, + maxItems, + ...props, + }, + }); + }; + + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); + + it("renders the list's title", () => { + expect(findListTitle().text()).toBe(title); + }); + + it('renders the empty text', () => { + expect(findEmptyText().exists()).toBe(true); + expect(findEmptyText().text()).toBe(pristineText); + }); + }); + + describe('when there are cached frequent items', () => { + beforeEach(() => { + window.localStorage.setItem(storageKey, cachedFrequentProjects); + createWrapper(); + }); + + it('attempts to retrieve the items from the local storage', () => { + expect(window.localStorage.getItem).toHaveBeenCalledTimes(1); + expect(window.localStorage.getItem).toHaveBeenCalledWith(storageKey); + }); + + it('renders the maximum amount of items', () => { + expect(findItemsList().props('items').length).toBe(maxItems); + }); + + it('does not render the empty text slot', () => { + expect(findEmptyText().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js new file mode 100644 index 00000000000..e5ba1c63996 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js @@ -0,0 +1,238 @@ +import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert, GlDropdownDivider } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue'; +import { + LARGE_AVATAR_PX, + SMALL_AVATAR_PX, +} from '~/super_sidebar/components/global_search/constants'; +import { + PROJECTS_CATEGORY, + GROUPS_CATEGORY, + ISSUES_CATEGORY, + MERGE_REQUEST_CATEGORY, + RECENT_EPICS_CATEGORY, +} from '~/vue_shared/global_search/constants'; +import { + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, + MOCK_SORTED_AUTOCOMPLETE_OPTIONS, + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP, + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP, + MOCK_SEARCH, + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2, +} from '../mock_data'; + +Vue.use(Vuex); + +describe('HeaderSearchAutocompleteItems', () => { + let wrapper; + + const createComponent = (initialState, mockGetters, props) => { + const store = new Vuex.Store({ + state: { + loading: false, + ...initialState, + }, + getters: { + autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, + ...mockGetters, + }, + }); + + wrapper = shallowMount(HeaderSearchAutocompleteItems, { + store, + propsData: { + ...props, + }, + }); + }; + + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider); + const findFirstDropdownItem = () => findDropdownItems().at(0); + const findDropdownItemTitles = () => + findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text()); + const findDropdownItemSubTitles = () => + findDropdownItems() + .wrappers.filter((w) => w.findAll('span').length > 2) + .map((w) => w.findAll('span').at(2).text()); + const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); + const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findGlAvatar = () => wrapper.findComponent(GlAvatar); + const findGlAlert = () => wrapper.findComponent(GlAlert); + + describe('template', () => { + describe('when loading is true', () => { + beforeEach(() => { + createComponent({ loading: true }); + }); + + it('renders GlLoadingIcon', () => { + expect(findGlLoadingIcon().exists()).toBe(true); + }); + + it('does not render autocomplete options', () => { + expect(findDropdownItems()).toHaveLength(0); + }); + }); + + describe('when api returns error', () => { + beforeEach(() => { + createComponent({ autocompleteError: true }); + }); + + it('renders Alert', () => { + expect(findGlAlert().exists()).toBe(true); + }); + }); + describe('when loading is false', () => { + beforeEach(() => { + createComponent({ loading: false }); + }); + + it('does not render GlLoadingIcon', () => { + expect(findGlLoadingIcon().exists()).toBe(false); + }); + + describe('Dropdown items', () => { + it('renders item for each option in autocomplete option', () => { + expect(findDropdownItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length); + }); + + it('renders titles correctly', () => { + const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label); + expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + }); + + it('renders sub-titles correctly', () => { + const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map( + (o) => o.label, + ); + expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles); + }); + + it('renders links correctly', () => { + const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url); + expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); + }); + }); + + describe.each` + item | showAvatar | avatarSize | searchContext | entityId | entityName + ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''} + ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''} + ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''} + ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false} + ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'} + ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'} + ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'} + ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'} + ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'} + ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'} + ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'} + ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'} + ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'} + ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'} + ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'} + ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'} + ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'} + ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'} + ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'} + `('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => { + describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => { + beforeEach(() => { + createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] }); + }); + + it(`should${showAvatar ? '' : ' not'} render`, () => { + expect(findGlAvatar().exists()).toBe(showAvatar); + }); + + it(`should set avatarSize to ${avatarSize}`, () => { + expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize); + }); + + it(`should set avatar entityId to ${entityId}`, () => { + expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId); + }); + + it(`should set avatar entityName to ${entityName}`, () => { + expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe( + entityName, + ); + }); + }); + }); + }); + + describe.each` + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { + describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { + beforeEach(() => { + createComponent({}, {}, { currentFocusedOption }); + }); + + it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { + expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); + }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); + }); + }); + + describe.each` + search | items | dividerCount + ${null} | ${[]} | ${0} + ${''} | ${[]} | ${0} + ${'1'} | ${[]} | ${0} + ${')'} | ${[]} | ${0} + ${'t'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP} | ${1} + ${'te'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP} | ${0} + ${'tes'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1} + ${MOCK_SEARCH} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1} + `('Header Search Dropdown Dividers', ({ search, items, dividerCount }) => { + describe(`when search is ${search}`, () => { + beforeEach(() => { + createComponent( + { search }, + { + autocompleteGroupedSearchOptions: () => items, + }, + {}, + ); + }); + + it(`component should have ${dividerCount} dividers`, () => { + expect(findGlDropdownDividers()).toHaveLength(dividerCount); + }); + }); + }); + }); + + describe('watchers', () => { + describe('currentFocusedOption', () => { + beforeEach(() => { + createComponent(); + }); + + it('when focused changes to existing element calls scroll into view on the newly focused element', async () => { + const focusedElement = findFirstDropdownItem().element; + const scrollSpy = jest.spyOn(focusedElement, 'scrollIntoView'); + + wrapper.setProps({ currentFocusedOption: MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0] }); + + await nextTick(); + + expect(scrollSpy).toHaveBeenCalledWith(false); + scrollSpy.mockRestore(); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js new file mode 100644 index 00000000000..132f8e60598 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js @@ -0,0 +1,102 @@ +import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue'; +import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data'; + +Vue.use(Vuex); + +describe('HeaderSearchDefaultItems', () => { + let wrapper; + + const createComponent = (initialState, props) => { + const store = new Vuex.Store({ + state: { + searchContext: MOCK_SEARCH_CONTEXT, + ...initialState, + }, + getters: { + defaultSearchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, + }, + }); + + wrapper = shallowMount(HeaderSearchDefaultItems, { + store, + propsData: { + ...props, + }, + }); + }; + + const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findFirstDropdownItem = () => findDropdownItems().at(0); + const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text()); + const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); + + describe('template', () => { + describe('Dropdown items', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders item for each option in defaultSearchOptions', () => { + expect(findDropdownItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length); + }); + + it('renders titles correctly', () => { + const expectedTitles = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.title); + expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + }); + + it('renders links correctly', () => { + const expectedLinks = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.url); + expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); + }); + }); + + describe.each` + group | project | dropdownTitle + ${null} | ${null} | ${'All GitLab'} + ${{ name: 'Test Group' }} | ${null} | ${'Test Group'} + ${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'} + `('Dropdown Header', ({ group, project, dropdownTitle }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createComponent({ + searchContext: { + group, + project, + }, + }); + }); + + it(`should render as ${dropdownTitle}`, () => { + expect(findDropdownHeader().text()).toBe(dropdownTitle); + }); + }); + }); + + describe.each` + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { + describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { + beforeEach(() => { + createComponent({}, { currentFocusedOption }); + }); + + it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { + expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); + }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js new file mode 100644 index 00000000000..fa91ef43ced --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js @@ -0,0 +1,120 @@ +import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { trimText } from 'helpers/text_helper'; +import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue'; +import { truncate } from '~/lib/utils/text_utility'; +import { SCOPE_TOKEN_MAX_LENGTH } from '~/super_sidebar/components/global_search/constants'; +import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants'; +import { + MOCK_SEARCH, + MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, +} from '../mock_data'; + +Vue.use(Vuex); + +describe('HeaderSearchScopedItems', () => { + let wrapper; + + const createComponent = (initialState, mockGetters, props) => { + const store = new Vuex.Store({ + state: { + search: MOCK_SEARCH, + ...initialState, + }, + getters: { + scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS, + autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, + ...mockGetters, + }, + }); + + wrapper = shallowMount(HeaderSearchScopedItems, { + store, + propsData: { + ...props, + }, + }); + }; + + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findFirstDropdownItem = () => findDropdownItems().at(0); + const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findScopeTokens = () => wrapper.findAllComponents(GlToken); + const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text())); + const findScopeTokensIcons = () => + findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon)); + const findDropdownItemAriaLabels = () => + findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label'))); + const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); + + describe('template', () => { + describe('Dropdown items', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders item for each option in scopedSearchOptions', () => { + expect(findDropdownItems()).toHaveLength(MOCK_SCOPED_SEARCH_OPTIONS.length); + }); + + it('renders titles correctly', () => { + findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH)); + }); + + it('renders scope names correctly', () => { + const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => + truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH), + ); + + expect(findScopeTokensText()).toStrictEqual(expectedTitles); + }); + + it('renders scope icons correctly', () => { + findScopeTokensIcons().forEach((icon, i) => { + const w = icon.wrappers[0]; + expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon); + }); + }); + + it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => { + expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false); + }); + + it('renders aria-labels correctly', () => { + const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => + trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`), + ); + expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels); + }); + + it('renders links correctly', () => { + const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url); + expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); + }); + }); + + describe.each` + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { + describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { + beforeEach(() => { + createComponent({}, {}, { currentFocusedOption }); + }); + + it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { + expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); + }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js new file mode 100644 index 00000000000..0dcfc448125 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js @@ -0,0 +1,516 @@ +import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mockTracking } from 'helpers/tracking_helper'; +import { s__, sprintf } from '~/locale'; +import HeaderSearchApp from '~/super_sidebar/components/global_search/components/global_search.vue'; +import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue'; +import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue'; +import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue'; +import { + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, + SEARCH_BOX_INDEX, + ICON_PROJECT, + ICON_GROUP, + ICON_SUBGROUP, + SCOPE_TOKEN_MAX_LENGTH, + IS_SEARCHING, + IS_NOT_FOCUSED, + IS_FOCUSED, + SEARCH_SHORTCUTS_MIN_CHARACTERS, +} from '~/super_sidebar/components/global_search/constants'; +import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; +import { ENTER_KEY } from '~/lib/utils/keys'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { truncate } from '~/lib/utils/text_utility'; +import { + MOCK_SEARCH, + MOCK_SEARCH_QUERY, + MOCK_USERNAME, + MOCK_DEFAULT_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_SEARCH_CONTEXT_FULL, +} from '../mock_data'; + +Vue.use(Vuex); + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); + +describe('HeaderSearchApp', () => { + let wrapper; + + const actionSpies = { + setSearch: jest.fn(), + fetchAutocompleteOptions: jest.fn(), + clearAutocomplete: jest.fn(), + }; + + const createComponent = (initialState, mockGetters) => { + const store = new Vuex.Store({ + state: { + ...initialState, + }, + actions: actionSpies, + getters: { + searchQuery: () => MOCK_SEARCH_QUERY, + searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, + ...mockGetters, + }, + }); + + wrapper = shallowMountExtended(HeaderSearchApp, { + store, + }); + }; + + const formatScopeName = (scopeName) => { + if (!scopeName) { + return false; + } + const searchResultsScope = s__('GlobalSearch|in %{scope}'); + return truncate( + sprintf(searchResultsScope, { + scope: scopeName, + }), + SCOPE_TOKEN_MAX_LENGTH, + ); + }; + + const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form'); + const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findScopeToken = () => wrapper.findComponent(GlToken); + const findHeaderSearchInputKBD = () => wrapper.find('.keyboard-shortcut-helper'); + const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu'); + const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems); + const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); + const findHeaderSearchAutocompleteItems = () => + wrapper.findComponent(HeaderSearchAutocompleteItems); + const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation); + const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`); + const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION); + + describe('template', () => { + describe('always renders', () => { + beforeEach(() => { + createComponent(); + }); + + it('Header Search Input', () => { + expect(findHeaderSearchInput().exists()).toBe(true); + }); + + it('Header Search Input KBD hint', () => { + expect(findHeaderSearchInputKBD().exists()).toBe(true); + expect(findHeaderSearchInputKBD().text()).toContain('/'); + expect(findHeaderSearchInputKBD().attributes('title')).toContain( + 'Use the shortcut key <kbd>/</kbd> to start a search', + ); + }); + + it('Search Input Description', () => { + expect(findSearchInputDescription().exists()).toBe(true); + }); + + it('Search Results Description', () => { + expect(findSearchResultsDescription().exists()).toBe(true); + }); + }); + + describe.each` + showDropdown | username | showSearchDropdown + ${false} | ${null} | ${false} + ${false} | ${MOCK_USERNAME} | ${false} + ${true} | ${null} | ${false} + ${true} | ${MOCK_USERNAME} | ${true} + `('Header Search Dropdown', ({ showDropdown, username, showSearchDropdown }) => { + describe(`when showDropdown is ${showDropdown} and current_username is ${username}`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent(); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); + }); + + it(`should${showSearchDropdown ? '' : ' not'} render`, () => { + expect(findHeaderSearchDropdown().exists()).toBe(showSearchDropdown); + }); + }); + }); + + describe.each` + search | showDefault | showScoped | showAutocomplete + ${null} | ${true} | ${false} | ${false} + ${''} | ${true} | ${false} | ${false} + ${'t'} | ${false} | ${false} | ${true} + ${'te'} | ${false} | ${false} | ${true} + ${'tes'} | ${false} | ${true} | ${true} + ${MOCK_SEARCH} | ${false} | ${true} | ${true} + `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => { + describe(`when search is ${search}`, () => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent({ search }, {}); + findHeaderSearchInput().vm.$emit('click'); + }); + + it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { + expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); + }); + + it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { + expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + }); + + it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => { + expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); + }); + + 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); + expect(wrapper.emitted().expandSearchBar.length).toBe(1); + }); + }); + }); + + describe.each` + username | showDropdown | expectedDesc + ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN} + ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN} + ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN} + ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN} + `('Search Input Description', ({ username, showDropdown, expectedDesc }) => { + describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent(); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); + }); + + it(`sets description to ${expectedDesc}`, () => { + expect(findSearchInputDescription().text()).toBe(expectedDesc); + }); + }); + }); + + describe.each` + username | showDropdown | search | loading | searchOptions | expectedDesc + ${null} | ${true} | ${''} | ${false} | ${[]} | ${''} + ${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''} + ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} + ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING} + `( + 'Search Results Description', + ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { + describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent( + { + search, + loading, + }, + { + searchOptions: () => searchOptions, + }, + ); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); + }); + + it(`sets description to ${expectedDesc}`, () => { + expect(findSearchResultsDescription().text()).toBe(expectedDesc); + }); + }); + }, + ); + + describe('input box', () => { + describe.each` + search | searchOptions | hasToken + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true} + ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false} + ${'x'} | ${[]} | ${false} + `('token', ({ search, searchOptions, hasToken }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + findHeaderSearchInput().vm.$emit('click'); + }); + + it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${ + searchOptions[0]?.html_id + }"`, () => { + expect(findScopeToken().exists()).toBe(hasToken); + }); + + it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${ + searchOptions[0]?.scope || searchOptions[0]?.description + }"`, () => { + expect(findScopeToken().exists() && findScopeToken().text()).toBe( + formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description), + ); + }); + }); + }); + + describe('form', () => { + describe.each` + 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 }); + if (isFocused) { + findHeaderSearchInput().vm.$emit('click'); + } + }); + + const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS; + + 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(`classes ${isSearching ? 'contain' : 'do not contain'} "${ + isFocused ? IS_FOCUSED : IS_NOT_FOCUSED + }"`, () => { + expect(findHeaderSearchForm().classes()).toContain( + isFocused ? IS_FOCUSED : IS_NOT_FOCUSED, + ); + }); + }); + }); + + describe.each` + search | searchOptions | hasIcon | iconName + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false} + `('token', ({ search, searchOptions, hasIcon, iconName }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + findHeaderSearchInput().vm.$emit('click'); + }); + + it(`icon for data set type "${searchOptions[0]?.html_id}" ${ + hasIcon ? 'is' : 'is NOT' + } rendered`, () => { + expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon); + }); + + it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${ + searchOptions[0]?.html_id + }"`, () => { + expect( + findScopeToken().findComponent(GlIcon).exists() && + findScopeToken().findComponent(GlIcon).attributes('name'), + ).toBe(iconName); + }); + }); + }); + + describe('events', () => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent(); + }); + + describe('Header Search Input', () => { + describe('when dropdown is closed', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + it('onFocus opens dropdown and triggers snowplow event', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(false); + findHeaderSearchInput().vm.$emit('focus'); + + await nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(true); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', { + label: 'global_search', + property: 'navigation_top', + }); + }); + + it('onClick opens dropdown and triggers snowplow event', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(false); + findHeaderSearchInput().vm.$emit('click'); + + await nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(true); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', { + label: 'global_search', + property: 'navigation_top', + }); + }); + + it('onClick followed by onFocus only triggers a single snowplow event', async () => { + findHeaderSearchInput().vm.$emit('click'); + findHeaderSearchInput().vm.$emit('focus'); + + expect(trackingSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('onInput', () => { + describe('when search has text', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH); + }); + + it('calls setSearch with search term', () => { + expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH); + }); + + it('calls fetchAutocompleteOptions', () => { + expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled(); + }); + + it('does not call clearAutocomplete', () => { + expect(actionSpies.clearAutocomplete).not.toHaveBeenCalled(); + }); + }); + + describe('when search is emptied', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('input', ''); + }); + + it('calls setSearch with empty term', () => { + expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), ''); + }); + + it('does not call fetchAutocompleteOptions', () => { + expect(actionSpies.fetchAutocompleteOptions).not.toHaveBeenCalled(); + }); + + it('calls clearAutocomplete', () => { + expect(actionSpies.clearAutocomplete).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('Dropdown Keyboard Navigation', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('click'); + }); + + it('closes dropdown when @tab is emitted', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(true); + findDropdownKeyboardNavigation().vm.$emit('tab'); + + await nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(false); + }); + }); + }); + + describe('computed', () => { + describe.each` + MOCK_INDEX | search + ${1} | ${null} + ${SEARCH_BOX_INDEX} | ${'test'} + ${2} | ${'test1'} + `('currentFocusedOption', ({ MOCK_INDEX, search }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent({ search }); + findHeaderSearchInput().vm.$emit('click'); + }); + + it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => { + findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); + expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]); + }); + }); + }); + + describe('Submitting a search', () => { + describe('with no currentFocusedOption', () => { + beforeEach(() => { + createComponent(); + }); + + it('onKey-enter submits a search', () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + + expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY); + }); + }); + + describe('with less than min characters and no dropdown results', () => { + beforeEach(() => { + createComponent({ search: 'x' }); + }); + + it('onKey-enter will NOT submit a search', () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + + expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY); + }); + }); + + describe('with currentFocusedOption', () => { + const MOCK_INDEX = 1; + + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent(); + findHeaderSearchInput().vm.$emit('click'); + }); + + it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => { + findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); + + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js new file mode 100644 index 00000000000..58e578e4c4c --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js @@ -0,0 +1,404 @@ +import { + ICON_PROJECT, + ICON_GROUP, + ICON_SUBGROUP, +} from '~/super_sidebar/components/global_search/constants'; +import { + PROJECTS_CATEGORY, + GROUPS_CATEGORY, + MSG_ISSUES_ASSIGNED_TO_ME, + MSG_ISSUES_IVE_CREATED, + MSG_MR_ASSIGNED_TO_ME, + MSG_MR_IM_REVIEWER, + MSG_MR_IVE_CREATED, + MSG_IN_ALL_GITLAB, +} from '~/vue_shared/global_search/constants'; + +export const MOCK_USERNAME = 'anyone'; + +export const MOCK_SEARCH_PATH = '/search'; + +export const MOCK_ISSUE_PATH = '/dashboard/issues'; + +export const MOCK_MR_PATH = '/dashboard/merge_requests'; + +export const MOCK_ALL_PATH = '/'; + +export const MOCK_AUTOCOMPLETE_PATH = '/autocomplete'; + +export const MOCK_PROJECT = { + id: 123, + name: 'MockProject', + path: '/mock-project', +}; + +export const MOCK_PROJECT_LONG = { + id: 124, + name: 'Mock Project Name That Is Ridiculously Long And It Goes Forever', + path: '/mock-project-name-that-is-ridiculously-long-and-it-goes-forever', +}; + +export const MOCK_GROUP = { + id: 321, + name: 'MockGroup', + path: '/mock-group', +}; + +export const MOCK_SUBGROUP = { + id: 322, + name: 'MockSubGroup', + path: `${MOCK_GROUP}/mock-subgroup`, +}; + +export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test'; + +export const MOCK_SEARCH = 'test'; + +export const MOCK_SEARCH_CONTEXT = { + project: null, + project_metadata: {}, + group: null, + group_metadata: {}, +}; + +export const MOCK_SEARCH_CONTEXT_FULL = { + group: { + id: 31, + name: 'testGroup', + full_name: 'testGroup', + }, + group_metadata: { + group_path: 'testGroup', + name: 'testGroup', + issues_path: '/groups/testGroup/-/issues', + mr_path: '/groups/testGroup/-/merge_requests', + }, +}; + +export const MOCK_DEFAULT_SEARCH_OPTIONS = [ + { + html_id: 'default-issues-assigned', + title: MSG_ISSUES_ASSIGNED_TO_ME, + url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`, + }, + { + html_id: 'default-issues-created', + title: MSG_ISSUES_IVE_CREATED, + url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`, + }, + { + html_id: 'default-mrs-assigned', + title: MSG_MR_ASSIGNED_TO_ME, + url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`, + }, + { + html_id: 'default-mrs-reviewer', + title: MSG_MR_IM_REVIEWER, + url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`, + }, + { + html_id: 'default-mrs-created', + title: MSG_MR_IVE_CREATED, + url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`, + }, +]; + +export const MOCK_SCOPED_SEARCH_OPTIONS = [ + { + html_id: 'scoped-in-project', + scope: MOCK_PROJECT.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT.path, + }, + { + html_id: 'scoped-in-project-long', + scope: MOCK_PROJECT_LONG.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT_LONG.path, + }, + { + html_id: 'scoped-in-group', + scope: MOCK_GROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_GROUP, + url: MOCK_GROUP.path, + }, + { + html_id: 'scoped-in-subgroup', + scope: MOCK_SUBGROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_SUBGROUP, + url: MOCK_SUBGROUP.path, + }, + { + html_id: 'scoped-in-all', + description: MSG_IN_ALL_GITLAB, + url: MOCK_ALL_PATH, + }, +]; + +export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [ + { + html_id: 'scoped-in-project', + scope: MOCK_PROJECT.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT.path, + }, + { + html_id: 'scoped-in-group', + scope: MOCK_GROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_GROUP, + url: MOCK_GROUP.path, + }, + { + html_id: 'scoped-in-all', + description: MSG_IN_ALL_GITLAB, + url: MOCK_ALL_PATH, + }, +]; + +export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [ + { + category: 'Projects', + id: 1, + label: 'Gitlab Org / MockProject1', + value: 'MockProject1', + url: 'project/1', + }, + { + category: 'Groups', + id: 1, + label: 'Gitlab Org / MockGroup1', + value: 'MockGroup1', + url: 'group/1', + }, + { + category: 'Projects', + id: 2, + label: 'Gitlab Org / MockProject2', + value: 'MockProject2', + url: 'project/2', + }, + { + category: 'Help', + label: 'GitLab Help', + url: 'help/gitlab', + }, +]; + +export const MOCK_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Projects', + html_id: 'autocomplete-Projects-0', + id: 1, + label: 'Gitlab Org / MockProject1', + value: 'MockProject1', + url: 'project/1', + }, + { + category: 'Groups', + html_id: 'autocomplete-Groups-1', + id: 1, + label: 'Gitlab Org / MockGroup1', + value: 'MockGroup1', + url: 'group/1', + }, + { + category: 'Projects', + html_id: 'autocomplete-Projects-2', + id: 2, + label: 'Gitlab Org / MockProject2', + value: 'MockProject2', + url: 'project/2', + }, + { + category: 'Help', + html_id: 'autocomplete-Help-3', + label: 'GitLab Help', + url: 'help/gitlab', + }, +]; + +export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Groups', + data: [ + { + category: 'Groups', + html_id: 'autocomplete-Groups-1', + + id: 1, + label: 'Gitlab Org / MockGroup1', + value: 'MockGroup1', + url: 'group/1', + }, + ], + }, + { + category: 'Projects', + data: [ + { + category: 'Projects', + html_id: 'autocomplete-Projects-0', + + id: 1, + label: 'Gitlab Org / MockProject1', + value: 'MockProject1', + url: 'project/1', + }, + { + category: 'Projects', + html_id: 'autocomplete-Projects-2', + + id: 2, + label: 'Gitlab Org / MockProject2', + value: 'MockProject2', + url: 'project/2', + }, + ], + }, + { + category: 'Help', + data: [ + { + category: 'Help', + html_id: 'autocomplete-Help-3', + + label: 'GitLab Help', + url: 'help/gitlab', + }, + ], + }, +]; + +export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Groups', + html_id: 'autocomplete-Groups-1', + id: 1, + label: 'Gitlab Org / MockGroup1', + value: 'MockGroup1', + url: 'group/1', + }, + { + category: 'Projects', + html_id: 'autocomplete-Projects-0', + id: 1, + label: 'Gitlab Org / MockProject1', + value: 'MockProject1', + url: 'project/1', + }, + { + category: 'Projects', + html_id: 'autocomplete-Projects-2', + id: 2, + label: 'Gitlab Org / MockProject2', + value: 'MockProject2', + url: 'project/2', + }, + { + category: 'Help', + html_id: 'autocomplete-Help-3', + label: 'GitLab Help', + url: 'help/gitlab', + }, +]; + +export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP = [ + { + category: 'Help', + data: [ + { + html_id: 'autocomplete-Help-1', + category: 'Help', + label: 'Rake Tasks Help', + url: '/help/raketasks/index', + }, + { + html_id: 'autocomplete-Help-2', + category: 'Help', + label: 'System Hooks Help', + url: '/help/system_hooks/system_hooks', + }, + ], + }, +]; + +export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP = [ + { + category: 'Settings', + data: [ + { + html_id: 'autocomplete-Settings-0', + category: 'Settings', + label: 'User settings', + url: '/-/profile', + }, + { + html_id: 'autocomplete-Settings-3', + category: 'Settings', + label: 'Admin Section', + url: '/admin', + }, + ], + }, + { + category: 'Help', + data: [ + { + html_id: 'autocomplete-Help-1', + category: 'Help', + label: 'Rake Tasks Help', + url: '/help/raketasks/index', + }, + { + html_id: 'autocomplete-Help-2', + category: 'Help', + label: 'System Hooks Help', + url: '/help/system_hooks/system_hooks', + }, + ], + }, +]; + +export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2 = [ + { + category: 'Groups', + data: [ + { + html_id: 'autocomplete-Groups-0', + category: 'Groups', + id: 148, + label: 'Jashkenas / Test Subgroup / test-subgroup', + url: '/jashkenas/test-subgroup/test-subgroup', + avatar_url: '', + }, + { + html_id: 'autocomplete-Groups-1', + category: 'Groups', + id: 147, + label: 'Jashkenas / Test Subgroup', + url: '/jashkenas/test-subgroup', + avatar_url: '', + }, + ], + }, + { + category: 'Projects', + data: [ + { + html_id: 'autocomplete-Projects-2', + category: 'Projects', + id: 1, + value: 'Gitlab Test', + label: 'Gitlab Org / Gitlab Test', + url: '/gitlab-org/gitlab-test', + avatar_url: '/uploads/-/system/project/avatar/1/icons8-gitlab-512.png', + }, + ], + }, +]; diff --git a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js new file mode 100644 index 00000000000..c87b4513309 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js @@ -0,0 +1,113 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import * as actions from '~/super_sidebar/components/global_search/store/actions'; +import * as types from '~/super_sidebar/components/global_search/store/mutation_types'; +import initState from '~/super_sidebar/components/global_search/store/state'; +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { + MOCK_SEARCH, + MOCK_AUTOCOMPLETE_OPTIONS_RES, + MOCK_AUTOCOMPLETE_PATH, + MOCK_PROJECT, + MOCK_SEARCH_CONTEXT, + MOCK_SEARCH_PATH, + MOCK_MR_PATH, + MOCK_ISSUE_PATH, +} from '../mock_data'; + +jest.mock('~/alert'); + +describe('Header Search Store Actions', () => { + let state; + let mock; + + const createState = (initialState) => + initState({ + searchPath: MOCK_SEARCH_PATH, + issuesPath: MOCK_ISSUE_PATH, + mrPath: MOCK_MR_PATH, + autocompletePath: MOCK_AUTOCOMPLETE_PATH, + searchContext: MOCK_SEARCH_CONTEXT, + ...initialState, + }); + + afterEach(() => { + state = null; + mock.restore(); + }); + + describe.each` + axiosMock | type | expectedMutations + ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]} + ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} + `('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => { + describe(`on ${type}`, () => { + beforeEach(() => { + state = createState({}); + mock = new MockAdapter(axios); + mock[axiosMock.method]().reply(axiosMock.code, axiosMock.res); + }); + it(`should dispatch the correct mutations`, () => { + return testAction({ + action: actions.fetchAutocompleteOptions, + state, + expectedMutations, + }); + }); + }); + }); + + describe.each` + project | ref | fetchType | expectedPath + ${null} | ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}`} + ${MOCK_PROJECT} | ${null} | ${'generic'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&filter=generic`} + ${null} | ${MOCK_PROJECT.id} | ${'generic'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_ref=${MOCK_PROJECT.id}&filter=generic`} + ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${'search'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}&filter=search`} + `('autocompleteQuery', ({ project, ref, fetchType, expectedPath }) => { + describe(`when project is ${project?.name} and project ref is ${ref}`, () => { + beforeEach(() => { + state = createState({ + search: MOCK_SEARCH, + searchContext: { + project, + ref, + }, + }); + }); + + it(`should return ${expectedPath}`, () => { + expect(actions.autocompleteQuery({ state, fetchType })).toBe(expectedPath); + }); + }); + }); + + describe('clearAutocomplete', () => { + beforeEach(() => { + state = createState({}); + }); + + it('calls the CLEAR_AUTOCOMPLETE mutation', () => { + return testAction({ + action: actions.clearAutocomplete, + state, + expectedMutations: [{ type: types.CLEAR_AUTOCOMPLETE }], + }); + }); + }); + + describe('setSearch', () => { + beforeEach(() => { + state = createState({}); + }); + + it('calls the SET_SEARCH mutation', () => { + return testAction({ + action: actions.setSearch, + payload: MOCK_SEARCH, + state, + expectedMutations: [{ type: types.SET_SEARCH, payload: MOCK_SEARCH }], + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js new file mode 100644 index 00000000000..dca96da01a7 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js @@ -0,0 +1,333 @@ +import * as getters from '~/super_sidebar/components/global_search/store/getters'; +import initState from '~/super_sidebar/components/global_search/store/state'; +import { + MOCK_USERNAME, + MOCK_SEARCH_PATH, + MOCK_ISSUE_PATH, + MOCK_MR_PATH, + MOCK_AUTOCOMPLETE_PATH, + MOCK_SEARCH_CONTEXT, + MOCK_DEFAULT_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS_DEF, + MOCK_PROJECT, + MOCK_GROUP, + MOCK_ALL_PATH, + MOCK_SEARCH, + MOCK_AUTOCOMPLETE_OPTIONS, + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, + MOCK_SORTED_AUTOCOMPLETE_OPTIONS, +} from '../mock_data'; + +describe('Header Search Store Getters', () => { + let state; + + const createState = (initialState) => { + state = initState({ + searchPath: MOCK_SEARCH_PATH, + issuesPath: MOCK_ISSUE_PATH, + mrPath: MOCK_MR_PATH, + autocompletePath: MOCK_AUTOCOMPLETE_PATH, + searchContext: MOCK_SEARCH_CONTEXT, + ...initialState, + }); + }; + + afterEach(() => { + state = null; + }); + + describe.each` + group | project | scope | forSnippets | codeSearch | ref | expectedPath + ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`} + ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`} + ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`} + ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`} + `('searchQuery', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope, + for_snippets: forSnippets, + code_search: codeSearch, + ref, + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.searchQuery(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | group_metadata | project | project_metadata | expectedPath + ${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ issues_path: 'project/path' }} | ${'project/path'} + `('scopedIssuesPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + group_metadata, + project, + project_metadata, + }, + }); + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.scopedIssuesPath(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | group_metadata | project | project_metadata | expectedPath + ${null} | ${null} | ${null} | ${null} | ${MOCK_MR_PATH} + ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${'group/path'} + ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${'project/path'} + `('scopedMRPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + group_metadata, + project, + project_metadata, + }, + }); + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.scopedMRPath(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | project | scope | forSnippets | codeSearch | ref | expectedPath + ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`} + ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`} + ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`} + ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`} + `('projectUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope, + for_snippets: forSnippets, + code_search: codeSearch, + ref, + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.projectUrl(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | project | scope | forSnippets | codeSearch | ref | expectedPath + ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`} + ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`} + ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`} + ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`} + `('groupUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope, + for_snippets: forSnippets, + code_search: codeSearch, + ref, + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.groupUrl(state)).toBe(expectedPath); + }); + }); + }); + + describe.each` + group | project | scope | forSnippets | codeSearch | ref | expectedPath + ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`} + ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`} + ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`} + ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true&search_code=true`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`} + `('allUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope, + for_snippets: forSnippets, + code_search: codeSearch, + ref, + }, + }); + state.search = MOCK_SEARCH; + }); + + it(`should return ${expectedPath}`, () => { + expect(getters.allUrl(state)).toBe(expectedPath); + }); + }); + }); + + describe('defaultSearchOptions', () => { + const mockGetters = { + scopedIssuesPath: MOCK_ISSUE_PATH, + scopedMRPath: MOCK_MR_PATH, + }; + + beforeEach(() => { + createState(); + window.gon.current_username = MOCK_USERNAME; + }); + + it('returns the correct array', () => { + expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_DEFAULT_SEARCH_OPTIONS, + ); + }); + + it('returns the correct array if issues path is false', () => { + mockGetters.scopedIssuesPath = undefined; + expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length), + ); + }); + }); + + describe('scopedSearchOptions', () => { + const mockGetters = { + projectUrl: MOCK_PROJECT.path, + groupUrl: MOCK_GROUP.path, + allUrl: MOCK_ALL_PATH, + }; + + beforeEach(() => { + createState({ + searchContext: { + project: MOCK_PROJECT, + group: MOCK_GROUP, + }, + }); + }); + + it('returns the correct array', () => { + expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_SCOPED_SEARCH_OPTIONS_DEF, + ); + }); + }); + + describe('autocompleteGroupedSearchOptions', () => { + beforeEach(() => { + createState(); + state.autocompleteOptions = MOCK_AUTOCOMPLETE_OPTIONS; + }); + + it('returns the correct grouped array', () => { + expect(getters.autocompleteGroupedSearchOptions(state)).toStrictEqual( + MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, + ); + }); + }); + + describe.each` + search | defaultSearchOptions | scopedSearchOptions | autocompleteGroupedSearchOptions | expectedArray + ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS} + ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} + ${1} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]} + ${'('} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]} + ${'t'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${'te'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${'tes'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} + `( + 'searchOptions', + ({ + search, + defaultSearchOptions, + scopedSearchOptions, + autocompleteGroupedSearchOptions, + expectedArray, + }) => { + describe(`when search is ${search} and the defaultSearchOptions${ + defaultSearchOptions.length ? '' : ' do not' + } exist, scopedSearchOptions${ + scopedSearchOptions.length ? '' : ' do not' + } exist, and autocompleteGroupedSearchOptions${ + autocompleteGroupedSearchOptions.length ? '' : ' do not' + } exist`, () => { + const mockGetters = { + defaultSearchOptions, + scopedSearchOptions, + autocompleteGroupedSearchOptions, + }; + + beforeEach(() => { + createState(); + state.search = search; + }); + + it(`should return the correct combined array`, () => { + expect(getters.searchOptions(state, mockGetters)).toStrictEqual(expectedArray); + }); + }); + }, + ); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js new file mode 100644 index 00000000000..d2dc484e825 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js @@ -0,0 +1,63 @@ +import * as types from '~/super_sidebar/components/global_search/store/mutation_types'; +import mutations from '~/super_sidebar/components/global_search/store/mutations'; +import createState from '~/super_sidebar/components/global_search/store/state'; +import { + MOCK_SEARCH, + MOCK_AUTOCOMPLETE_OPTIONS_RES, + MOCK_AUTOCOMPLETE_OPTIONS, +} from '../mock_data'; + +describe('Header Search Store Mutations', () => { + let state; + + beforeEach(() => { + state = createState({}); + }); + + describe('REQUEST_AUTOCOMPLETE', () => { + it('sets loading to true and empties autocompleteOptions array', () => { + mutations[types.REQUEST_AUTOCOMPLETE](state); + + expect(state.loading).toBe(true); + expect(state.autocompleteOptions).toStrictEqual([]); + expect(state.autocompleteError).toBe(false); + }); + }); + + describe('RECEIVE_AUTOCOMPLETE_SUCCESS', () => { + it('sets loading to false and then formats and sets the autocompleteOptions array', () => { + mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS_RES); + + expect(state.loading).toBe(false); + expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS); + expect(state.autocompleteError).toBe(false); + }); + }); + + describe('RECEIVE_AUTOCOMPLETE_ERROR', () => { + it('sets loading to false and empties autocompleteOptions array', () => { + mutations[types.RECEIVE_AUTOCOMPLETE_ERROR](state); + + expect(state.loading).toBe(false); + expect(state.autocompleteOptions).toStrictEqual([]); + expect(state.autocompleteError).toBe(true); + }); + }); + + describe('CLEAR_AUTOCOMPLETE', () => { + it('empties autocompleteOptions array', () => { + mutations[types.CLEAR_AUTOCOMPLETE](state); + + expect(state.autocompleteOptions).toStrictEqual([]); + expect(state.autocompleteError).toBe(false); + }); + }); + + describe('SET_SEARCH', () => { + it('sets search to value', () => { + mutations[types.SET_SEARCH](state, MOCK_SEARCH); + + expect(state.search).toBe(MOCK_SEARCH); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js new file mode 100644 index 00000000000..6aee895f611 --- /dev/null +++ b/spec/frontend/super_sidebar/components/groups_list_spec.js @@ -0,0 +1,87 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; +import GroupsList from '~/super_sidebar/components/groups_list.vue'; +import SearchResults from '~/super_sidebar/components/search_results.vue'; +import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue'; +import NavItem from '~/super_sidebar/components/nav_item.vue'; +import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants'; + +const username = 'root'; +const viewAllLink = '/path/to/groups'; +const storageKey = `${username}/frequent-groups`; + +describe('GroupsList component', () => { + let wrapper; + + const findSearchResults = () => wrapper.findComponent(SearchResults); + const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList); + const findViewAllLink = () => wrapper.findComponent(NavItem); + + const itRendersViewAllItem = () => { + it('renders the "View all..." item', () => { + expect(findViewAllLink().props('item')).toEqual({ + icon: 'group', + link: viewAllLink, + title: s__('Navigation|View all groups'), + }); + }); + }; + + const createWrapper = (props = {}) => { + wrapper = shallowMountExtended(GroupsList, { + propsData: { + username, + viewAllLink, + ...props, + }, + }); + }; + + describe('when displaying search results', () => { + const searchResults = ['A search result']; + + beforeEach(() => { + createWrapper({ + isSearch: true, + searchResults, + }); + }); + + it('renders the search results component', () => { + expect(findSearchResults().exists()).toBe(true); + expect(findFrequentItemsList().exists()).toBe(false); + }); + + it('passes the correct props to the search results component', () => { + expect(findSearchResults().props()).toEqual({ + title: s__('Navigation|Groups'), + noResultsText: s__('Navigation|No group matches found'), + searchResults, + }); + }); + + itRendersViewAllItem(); + }); + + describe('when displaying frequent groups', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders the frequent items list', () => { + expect(findFrequentItemsList().exists()).toBe(true); + expect(findSearchResults().exists()).toBe(false); + }); + + it('passes the correct props to the frequent items list', () => { + expect(findFrequentItemsList().props()).toEqual({ + title: s__('Navigation|Frequent groups'), + storageKey, + maxItems: MAX_FREQUENT_GROUPS_COUNT, + pristineText: s__('Navigation|Groups you visit often will appear here.'), + }); + }); + + itRendersViewAllItem(); + }); +}); diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index bc847a3e159..1d072c0ba3c 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -68,18 +68,23 @@ describe('HelpCenter component', () => { }); describe('showKeyboardShortcuts', () => { + let button; + beforeEach(() => { jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); - window.toggleShortcutsHelp = jest.fn(); - findButton('Keyboard shortcuts ?').click(); + + button = findButton('Keyboard shortcuts ?'); }); it('closes the dropdown', () => { + button.click(); expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled(); }); it('shows the keyboard shortcuts modal', () => { - expect(window.toggleShortcutsHelp).toHaveBeenCalled(); + // This relies on the event delegation set up by the Shortcuts class in + // ~/behaviors/shortcuts/shortcuts.js. + expect(button.classList.contains('js-shortcuts-modal-trigger')).toBe(true); }); }); diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js new file mode 100644 index 00000000000..8e00984f500 --- /dev/null +++ b/spec/frontend/super_sidebar/components/items_list_spec.js @@ -0,0 +1,63 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ItemsList from '~/super_sidebar/components/items_list.vue'; +import NavItem from '~/super_sidebar/components/nav_item.vue'; +import { cachedFrequentProjects } from '../mock_data'; + +const mockItems = JSON.parse(cachedFrequentProjects); +const [firstMockedProject] = mockItems; + +describe('ItemsList component', () => { + let wrapper; + + const findNavItems = () => wrapper.findAllComponents(NavItem); + + const createWrapper = ({ props = {}, slots = {} } = {}) => { + wrapper = shallowMountExtended(ItemsList, { + propsData: { + ...props, + }, + slots, + }); + }; + + it('does not render nav items when there are no items', () => { + createWrapper(); + + expect(findNavItems().length).toBe(0); + }); + + it('renders one nav item per item', () => { + createWrapper({ + props: { + items: mockItems, + }, + }); + + expect(findNavItems().length).not.toBe(0); + expect(findNavItems().length).toBe(mockItems.length); + }); + + it('passes the correct props to the nav items', () => { + createWrapper({ + props: { + items: mockItems, + }, + }); + const firstNavItem = findNavItems().at(0); + + expect(firstNavItem.props('item')).toEqual(firstMockedProject); + }); + + it('renders the `view-all-items` slot', () => { + const testId = 'view-all-items'; + createWrapper({ + slots: { + 'view-all-items': { + template: `<div data-testid="${testId}" />`, + }, + }, + }); + + expect(wrapper.findByTestId(testId).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js new file mode 100644 index 00000000000..22989c1a5f9 --- /dev/null +++ b/spec/frontend/super_sidebar/components/nav_item_spec.js @@ -0,0 +1,49 @@ +import { GlBadge } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NavItem from '~/super_sidebar/components/nav_item.vue'; + +describe('NavItem component', () => { + let wrapper; + + const findLink = () => wrapper.findByTestId('nav-item-link'); + const findPill = () => wrapper.findComponent(GlBadge); + const createWrapper = (item, props = {}) => { + wrapper = shallowMountExtended(NavItem, { + propsData: { + item, + ...props, + }, + }); + }; + + describe('pills', () => { + it.each([0, 5, 3.4, 'foo', '10%'])('item with pill_data `%p` renders a pill', (pillCount) => { + createWrapper({ title: 'Foo', pill_count: pillCount }); + + expect(findPill().text()).toEqual(pillCount.toString()); + }); + + it.each([null, undefined, false, true, '', NaN, Number.POSITIVE_INFINITY])( + 'item with pill_data `%p` renders no pill', + (pillCount) => { + createWrapper({ title: 'Foo', pill_count: pillCount }); + + expect(findPill().exists()).toEqual(false); + }, + ); + }); + + it('applies custom link classes', () => { + const customClass = 'customClass'; + createWrapper( + { title: 'Foo' }, + { + linkClasses: { + [customClass]: true, + }, + }, + ); + + expect(findLink().attributes('class')).toContain(customClass); + }); +}); diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js new file mode 100644 index 00000000000..cdc003b14e0 --- /dev/null +++ b/spec/frontend/super_sidebar/components/projects_list_spec.js @@ -0,0 +1,82 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; +import ProjectsList from '~/super_sidebar/components/projects_list.vue'; +import SearchResults from '~/super_sidebar/components/search_results.vue'; +import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue'; +import NavItem from '~/super_sidebar/components/nav_item.vue'; +import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants'; + +const username = 'root'; +const viewAllLink = '/path/to/projects'; +const storageKey = `${username}/frequent-projects`; + +describe('ProjectsList component', () => { + let wrapper; + + const findSearchResults = () => wrapper.findComponent(SearchResults); + const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList); + const findViewAllLink = () => wrapper.findComponent(NavItem); + + const itRendersViewAllItem = () => { + it('renders the "View all..." item', () => { + expect(findViewAllLink().props('item')).toEqual({ + icon: 'project', + link: viewAllLink, + title: s__('Navigation|View all projects'), + }); + }); + }; + + const createWrapper = (props = {}) => { + wrapper = shallowMountExtended(ProjectsList, { + propsData: { + username, + viewAllLink, + ...props, + }, + }); + }; + + describe('when displaying search results', () => { + const searchResults = ['A search result']; + + beforeEach(() => { + createWrapper({ + isSearch: true, + searchResults, + }); + }); + + it('renders the search results component', () => { + expect(findSearchResults().exists()).toBe(true); + expect(findFrequentItemsList().exists()).toBe(false); + }); + + it('passes the correct props to the search results component', () => { + expect(findSearchResults().props()).toEqual({ + title: s__('Navigation|Projects'), + noResultsText: s__('Navigation|No project matches found'), + searchResults, + }); + }); + + itRendersViewAllItem(); + }); + + describe('when displaying frequent projects', () => { + beforeEach(() => { + createWrapper(); + }); + + it('passes the correct props to the frequent items list', () => { + expect(findFrequentItemsList().props()).toEqual({ + title: s__('Navigation|Frequent projects'), + storageKey, + maxItems: MAX_FREQUENT_PROJECTS_COUNT, + pristineText: s__('Navigation|Projects you visit often will appear here.'), + }); + }); + + itRendersViewAllItem(); + }); +}); diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js new file mode 100644 index 00000000000..dd48935c138 --- /dev/null +++ b/spec/frontend/super_sidebar/components/search_results_spec.js @@ -0,0 +1,57 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; +import SearchResults from '~/super_sidebar/components/search_results.vue'; +import ItemsList from '~/super_sidebar/components/items_list.vue'; + +const title = s__('Navigation|PROJECTS'); +const noResultsText = s__('Navigation|No project matches found'); + +describe('SearchResults component', () => { + let wrapper; + + const findListTitle = () => wrapper.findByTestId('list-title'); + const findItemsList = () => wrapper.findComponent(ItemsList); + const findEmptyText = () => wrapper.findByTestId('empty-text'); + + const createWrapper = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(SearchResults, { + propsData: { + title, + noResultsText, + ...props, + }, + }); + }; + + describe('default state', () => { + beforeEach(() => { + createWrapper(); + }); + + it("renders the list's title", () => { + expect(findListTitle().text()).toBe(title); + }); + + it('renders the empty text', () => { + expect(findEmptyText().exists()).toBe(true); + expect(findEmptyText().text()).toBe(noResultsText); + }); + }); + + describe('when displaying search results', () => { + it('shows search results', () => { + const searchResults = [{ id: 1 }]; + createWrapper({ props: { isSearch: true, searchResults } }); + + expect(findItemsList().props('items')[0]).toEqual(searchResults[0]); + }); + + it('shows the no results text if search results are empty', () => { + const searchResults = []; + createWrapper({ props: { isSearch: true, searchResults } }); + + expect(findItemsList().props('items').length).toEqual(0); + expect(findEmptyText().text()).toBe(noResultsText); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/sidebar_portal_spec.js b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js new file mode 100644 index 00000000000..3ef1cb7e692 --- /dev/null +++ b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js @@ -0,0 +1,68 @@ +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue'; +import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; + +describe('SidebarPortal', () => { + let targetWrapper; + + const Target = { + components: { SidebarPortalTarget }, + props: ['show'], + template: '<sidebar-portal-target v-if="show" />', + }; + + const Source = { + components: { SidebarPortal }, + template: '<sidebar-portal><br data-testid="test"></sidebar-portal>', + }; + + const mountSource = () => { + mount(Source); + }; + + const mountTarget = ({ show = true } = {}) => { + targetWrapper = mount(Target, { + propsData: { show }, + attachTo: document.body, + }); + }; + + const findTestContent = () => targetWrapper.find('[data-testid="test"]'); + + it('renders content into the target', async () => { + mountTarget(); + await nextTick(); + + mountSource(); + await nextTick(); + + expect(findTestContent().exists()).toBe(true); + }); + + it('waits for target to be available before rendering', async () => { + mountSource(); + await nextTick(); + + mountTarget(); + await nextTick(); + + expect(findTestContent().exists()).toBe(true); + }); + + it('supports conditional rendering of target', async () => { + mountTarget({ show: false }); + await nextTick(); + + mountSource(); + await nextTick(); + + expect(findTestContent().exists()).toBe(false); + + await targetWrapper.setProps({ show: true }); + expect(findTestContent().exists()).toBe(true); + + await targetWrapper.setProps({ show: false }); + expect(findTestContent().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index 45fc30c08f0..32921da23aa 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -2,13 +2,21 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue'; import HelpCenter from '~/super_sidebar/components/help_center.vue'; import UserBar from '~/super_sidebar/components/user_bar.vue'; +import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; +import { isCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; import { sidebarData } from '../mock_data'; +jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager', () => ({ + isCollapsed: jest.fn(), +})); + describe('SuperSidebar component', () => { let wrapper; + const findSidebar = () => wrapper.find('.super-sidebar'); const findUserBar = () => wrapper.findComponent(UserBar); const findHelpCenter = () => wrapper.findComponent(HelpCenter); + const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget); const createWrapper = (props = {}) => { wrapper = shallowMountExtended(SuperSidebar, { @@ -20,16 +28,33 @@ describe('SuperSidebar component', () => { }; describe('default', () => { - beforeEach(() => { + it('add aria-hidden and inert attributes when collapsed', () => { + isCollapsed.mockReturnValue(true); createWrapper(); + expect(findSidebar().attributes('aria-hidden')).toBe('true'); + expect(findSidebar().attributes('inert')).toBe('inert'); + }); + + it('does not add aria-hidden and inert attributes when expanded', () => { + isCollapsed.mockReturnValue(false); + createWrapper(); + expect(findSidebar().attributes('aria-hidden')).toBe('false'); + expect(findSidebar().attributes('inert')).toBe(undefined); }); it('renders UserBar with sidebarData', () => { + createWrapper(); expect(findUserBar().props('sidebarData')).toBe(sidebarData); }); it('renders HelpCenter with sidebarData', () => { + createWrapper(); expect(findHelpCenter().props('sidebarData')).toBe(sidebarData); }); + + it('renders SidebarPortalTarget', () => { + createWrapper(); + expect(findSidebarPortalTarget().exists()).toBe(true); + }); }); }); diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js index eceb792c3db..ae15dd55644 100644 --- a/spec/frontend/super_sidebar/components/user_bar_spec.js +++ b/spec/frontend/super_sidebar/components/user_bar_spec.js @@ -1,3 +1,4 @@ +import { GlBadge } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { __ } from '~/locale'; import CreateMenu from '~/super_sidebar/components/create_menu.vue'; @@ -12,12 +13,12 @@ describe('UserBar component', () => { const findCreateMenu = () => wrapper.findComponent(CreateMenu); const findCounter = (at) => wrapper.findAllComponents(Counter).at(at); const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu); + const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo'); - const createWrapper = (props = {}) => { + const createWrapper = (extraSidebarData = {}) => { wrapper = shallowMountExtended(UserBar, { propsData: { - sidebarData, - ...props, + sidebarData: { ...sidebarData, ...extraSidebarData }, }, provide: { rootPath: '/', @@ -55,5 +56,29 @@ describe('UserBar component', () => { expect(findCounter(2).props('href')).toBe('/dashboard/todos'); expect(findCounter(2).props('label')).toBe(__('To-Do list')); }); + + it('renders branding logo', () => { + expect(findBrandLogo().exists()).toBe(true); + expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url); + }); + }); + + describe('GitLab Next badge', () => { + describe('when on canary', () => { + it('should render a badge to switch off GitLab Next', () => { + createWrapper({ gitlab_com_and_canary: true }); + const badge = wrapper.findComponent(GlBadge); + expect(badge.text()).toBe('Next'); + expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url); + }); + }); + + describe('when not on canary', () => { + it('should not render the GitLab Next badge', () => { + createWrapper({ gitlab_com_and_canary: false }); + const badge = wrapper.findComponent(GlBadge); + expect(badge.exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js new file mode 100644 index 00000000000..b6231e03722 --- /dev/null +++ b/spec/frontend/super_sidebar/components/user_menu_spec.js @@ -0,0 +1,375 @@ +import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import UserMenu from '~/super_sidebar/components/user_menu.vue'; +import UserNameGroup from '~/super_sidebar/components/user_name_group.vue'; +import NewNavToggle from '~/nav/components/new_nav_toggle.vue'; +import invalidUrl from '~/lib/utils/invalid_url'; +import { mockTracking } from 'helpers/tracking_helper'; +import PersistentUserCallout from '~/persistent_user_callout'; +import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data'; + +describe('UserMenu component', () => { + let wrapper; + let trackingSpy; + + const GlEmoji = { template: '<img/>' }; + const toggleNewNavEndpoint = invalidUrl; + const showDropdown = () => wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown'); + + const createWrapper = (userDataChanges = {}) => { + wrapper = mountExtended(UserMenu, { + propsData: { + data: { + ...userMenuMockData, + ...userDataChanges, + }, + }, + stubs: { + GlEmoji, + GlAvatar: true, + }, + provide: { + toggleNewNavEndpoint, + }, + }); + + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }; + + describe('Toggle button', () => { + let toggle; + + beforeEach(() => { + createWrapper(); + toggle = wrapper.findByTestId('base-dropdown-toggle'); + }); + + it('renders User Avatar in a toggle', () => { + const avatar = toggle.findComponent(GlAvatar); + expect(avatar.exists()).toBe(true); + expect(avatar.props()).toMatchObject({ + entityName: userMenuMockData.name, + src: userMenuMockData.avatar_url, + }); + }); + + it('renders screen reader text', () => { + expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`); + }); + }); + + describe('User Menu Group', () => { + it('renders and passes data to it', () => { + createWrapper(); + const userNameGroup = wrapper.findComponent(UserNameGroup); + expect(userNameGroup.exists()).toBe(true); + expect(userNameGroup.props('user')).toEqual(userMenuMockData); + }); + }); + + describe('User status item', () => { + let item; + + const setItem = ({ can_update, busy, customized } = {}) => { + createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } }); + item = wrapper.findByTestId('status-item'); + }; + + describe('When user cannot update the status', () => { + it('does not render the status menu item', () => { + setItem(); + expect(item.exists()).toBe(false); + }); + }); + + describe('When user can update the status', () => { + it('renders the status menu item', () => { + setItem({ can_update: true }); + expect(item.exists()).toBe(true); + }); + + it('should set the CSS class for triggering status update modal', () => { + setItem({ can_update: true }); + expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true); + }); + + describe('renders correct label', () => { + it.each` + busy | customized | label + ${false} | ${false} | ${'Set status'} + ${false} | ${true} | ${'Edit status'} + ${true} | ${false} | ${'Edit status'} + ${true} | ${true} | ${'Edit status'} + `( + 'when busy is "$busy" and customized is "$customized" the label is "$label"', + ({ busy, customized, label }) => { + setItem({ can_update: true, busy, customized }); + expect(item.text()).toBe(label); + }, + ); + }); + + describe('Status update modal wrapper', () => { + const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper'); + + it('renders the modal wrapper', () => { + setItem({ can_update: true }); + expect(findModalWrapper().exists()).toBe(true); + }); + + it('sets default data attributes when status is not customized', () => { + setItem({ can_update: true }); + expect(findModalWrapper().attributes()).toMatchObject({ + 'data-current-emoji': '', + 'data-current-message': '', + 'data-default-emoji': 'speech_balloon', + }); + }); + + it('sets user status as data attributes when status is customized', () => { + setItem({ can_update: true, customized: true }); + expect(findModalWrapper().attributes()).toMatchObject({ + 'data-current-emoji': userMenuMockStatus.emoji, + 'data-current-message': userMenuMockStatus.message, + 'data-current-availability': userMenuMockStatus.availability, + 'data-current-clear-status-after': userMenuMockStatus.clear_after, + }); + }); + }); + }); + }); + + describe('Start Ultimate trial item', () => { + let item; + + const setItem = ({ has_start_trial } = {}) => { + createWrapper({ trial: { has_start_trial } }); + item = wrapper.findByTestId('start-trial-item'); + }; + + describe('When Ultimate trial is not suggested for the user', () => { + it('does not render the start trial menu item', () => { + setItem(); + expect(item.exists()).toBe(false); + }); + }); + + describe('When Ultimate trial can be suggested for the user', () => { + it('does render the start trial menu item', () => { + setItem({ has_start_trial: true }); + expect(item.exists()).toBe(true); + }); + }); + }); + + describe('Buy Pipeline Minutes item', () => { + let item; + + const setItem = ({ + show_buy_pipeline_minutes, + show_with_subtext, + show_notification_dot, + } = {}) => { + createWrapper({ + pipeline_minutes: { + ...userMenuMockPipelineMinutes, + show_buy_pipeline_minutes, + show_with_subtext, + show_notification_dot, + }, + }); + item = wrapper.findByTestId('buy-pipeline-minutes-item'); + }; + + describe('When does NOT meet the condition to buy CI minutes', () => { + beforeEach(() => { + setItem(); + }); + + it('does NOT render the buy pipeline minutes item', () => { + expect(item.exists()).toBe(false); + }); + + it('does not track the Sentry event', () => { + showDropdown(); + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + + describe('When does meet the condition to buy CI minutes', () => { + it('does render the menu item', () => { + setItem({ show_buy_pipeline_minutes: true }); + expect(item.exists()).toBe(true); + }); + + it('tracks the Sentry event', () => { + setItem({ show_buy_pipeline_minutes: true }); + showDropdown(); + expect(trackingSpy).toHaveBeenCalledWith( + undefined, + userMenuMockPipelineMinutes.tracking_attrs['track-action'], + { + label: userMenuMockPipelineMinutes.tracking_attrs['track-label'], + property: userMenuMockPipelineMinutes.tracking_attrs['track-property'], + }, + ); + }); + + describe('Callout & notification dot', () => { + let spyFactory; + + beforeEach(() => { + spyFactory = jest.spyOn(PersistentUserCallout, 'factory'); + }); + + describe('When `show_notification_dot` is `false`', () => { + beforeEach(() => { + setItem({ show_buy_pipeline_minutes: true, show_notification_dot: false }); + showDropdown(); + }); + + it('does not set callout attributes', () => { + expect(item.attributes()).not.toEqual( + expect.objectContaining({ + 'data-feature-id': userMenuMockPipelineMinutes.callout_attrs.feature_id, + 'data-dismiss-endpoint': userMenuMockPipelineMinutes.callout_attrs.dismiss_endpoint, + }), + ); + }); + + it('does not initialize the Persistent Callout', () => { + expect(spyFactory).not.toHaveBeenCalled(); + }); + + it('does not render notification dot', () => { + expect(wrapper.findByTestId('buy-pipeline-minutes-notification-dot').exists()).toBe( + false, + ); + }); + }); + + describe('When `show_notification_dot` is `true`', () => { + beforeEach(() => { + setItem({ show_buy_pipeline_minutes: true, show_notification_dot: true }); + showDropdown(); + }); + + it('sets the callout data attributes', () => { + expect(item.attributes()).toEqual( + expect.objectContaining({ + 'data-feature-id': userMenuMockPipelineMinutes.callout_attrs.feature_id, + 'data-dismiss-endpoint': userMenuMockPipelineMinutes.callout_attrs.dismiss_endpoint, + }), + ); + }); + + it('initializes the Persistent Callout', () => { + expect(spyFactory).toHaveBeenCalled(); + }); + + it('renders notification dot', () => { + expect(wrapper.findByTestId('buy-pipeline-minutes-notification-dot').exists()).toBe( + true, + ); + }); + }); + }); + + describe('Warning message', () => { + it('does not display the warning message when `show_with_subtext` is `false`', () => { + setItem({ show_buy_pipeline_minutes: true }); + + expect(item.text()).not.toContain(UserMenu.i18n.oneOfGroupsRunningOutOfPipelineMinutes); + }); + + it('displays the text and warning message when `show_with_subtext` is true', () => { + setItem({ show_buy_pipeline_minutes: true, show_with_subtext: true }); + + expect(item.text()).toContain(UserMenu.i18n.oneOfGroupsRunningOutOfPipelineMinutes); + }); + }); + }); + }); + + describe('Edit profile item', () => { + it('should render a link to the profile page', () => { + createWrapper(); + const item = wrapper.findByTestId('edit-profile-item'); + expect(item.text()).toBe(UserMenu.i18n.editProfile); + expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path); + }); + }); + + describe('Preferences item', () => { + it('should render a link to the profile page', () => { + createWrapper(); + const item = wrapper.findByTestId('preferences-item'); + expect(item.text()).toBe(UserMenu.i18n.preferences); + expect(item.find('a').attributes('href')).toBe( + userMenuMockData.settings.profile_preferences_path, + ); + }); + }); + + describe('GitLab Next item', () => { + describe('on gitlab.com', () => { + it('should render a link to switch to GitLab Next', () => { + createWrapper({ gitlab_com_but_not_canary: true }); + const item = wrapper.findByTestId('gitlab-next-item'); + expect(item.text()).toBe(UserMenu.i18n.gitlabNext); + expect(item.find('a').attributes('href')).toBe(userMenuMockData.canary_toggle_com_url); + }); + }); + + describe('anywhere else', () => { + it('should not render the GitLab Next link', () => { + createWrapper({ gitlab_com_but_not_canary: false }); + const item = wrapper.findByTestId('gitlab-next-item'); + expect(item.exists()).toBe(false); + }); + }); + }); + + describe('New navigation toggle item', () => { + it('should render menu item with new navigation toggle', () => { + createWrapper(); + const toggleItem = wrapper.findComponent(NewNavToggle); + expect(toggleItem.exists()).toBe(true); + expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint); + }); + }); + + describe('Feedback item', () => { + it('should render feedback item with a link to a new GitLab issue', () => { + createWrapper(); + const feedbackItem = wrapper.findByTestId('feedback-item'); + expect(feedbackItem.find('a').attributes('href')).toBe(UserMenu.feedbackUrl); + }); + }); + + describe('Sign out group', () => { + const findSignOutGroup = () => wrapper.findByTestId('sign-out-group'); + + it('should not render sign out group when user cannot sign out', () => { + createWrapper(); + expect(findSignOutGroup().exists()).toBe(false); + }); + + describe('when user can sign out', () => { + beforeEach(() => { + createWrapper({ can_sign_out: true }); + }); + + it('should render sign out group', () => { + expect(findSignOutGroup().exists()).toBe(true); + }); + + it('should render the menu item with a link to sign out and correct data attribute', () => { + expect(findSignOutGroup().find('a').attributes('href')).toBe( + userMenuMockData.sign_out_link, + ); + expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post'); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js new file mode 100644 index 00000000000..c06c8c218d4 --- /dev/null +++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js @@ -0,0 +1,100 @@ +import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import UserNameGroup from '~/super_sidebar/components/user_name_group.vue'; +import { userMenuMockData, userMenuMockStatus } from '../mock_data'; + +describe('UserNameGroup component', () => { + let wrapper; + + const findGlDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup); + const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); + const findGlTooltip = () => wrapper.findComponent(GlTooltip); + const findUserStatus = () => wrapper.findByTestId('user-menu-status'); + + const GlEmoji = { template: '<img/>' }; + + const createWrapper = (userDataChanges = {}) => { + wrapper = shallowMountExtended(UserNameGroup, { + propsData: { + user: { + ...userMenuMockData, + ...userDataChanges, + }, + }, + stubs: { + GlEmoji, + GlDisclosureDropdownItem, + }, + }); + }; + + beforeEach(() => { + createWrapper(); + }); + + it('renders the menu item in a separate group', () => { + expect(findGlDisclosureDropdownGroup().exists()).toBe(true); + }); + + it('renders menu item', () => { + expect(findGlDisclosureDropdownItem().exists()).toBe(true); + }); + + it('passes the item to the disclosure dropdown item', () => { + expect(findGlDisclosureDropdownItem().props('item')).toEqual({ + text: userMenuMockData.name, + href: userMenuMockData.link_to_profile, + }); + }); + + it("renders user's name", () => { + expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.name); + }); + + it("renders user's username", () => { + expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.username); + }); + + describe('Busy status', () => { + it('should not render "Busy" when user is NOT busy', () => { + expect(findGlDisclosureDropdownItem().text()).not.toContain('Busy'); + }); + it('should render "Busy" when user is busy', () => { + createWrapper({ status: { customized: true, busy: true } }); + + expect(findGlDisclosureDropdownItem().text()).toContain('Busy'); + }); + }); + + describe('User status', () => { + describe('when not customized', () => { + it('should not render it', () => { + expect(findUserStatus().exists()).toBe(false); + }); + }); + + describe('when customized', () => { + beforeEach(() => { + createWrapper({ status: { ...userMenuMockStatus, customized: true } }); + }); + + it('should render it', () => { + expect(findUserStatus().exists()).toBe(true); + }); + + it('should render status emoji', () => { + expect(findUserStatus().findComponent(GlEmoji).attributes('data-name')).toBe( + userMenuMockData.status.emoji, + ); + }); + + it('should render status message', () => { + expect(findUserStatus().text()).toContain(userMenuMockData.status.message); + }); + + it("sets the tooltip's target to the status container", () => { + expect(findGlTooltip().props('target')?.()).toBe(findUserStatus().element); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index 91a2dc93a47..b540f85d9fe 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -1,3 +1,5 @@ +import invalidUrl from '~/lib/utils/invalid_url'; + export const createNewMenuGroups = [ { name: 'This group', @@ -58,15 +60,23 @@ export const mergeRequestMenuGroup = [ ]; export const sidebarData = { + current_menu_items: [], + current_context_header: { + title: 'Your Work', + icon: 'work', + }, name: 'Administrator', username: 'root', avatar_url: 'path/to/img_administrator', + logo_url: 'path/to/logo', assigned_open_issues_count: 1, todos_pending_count: 3, issues_dashboard_path: 'path/to/issues', total_merge_requests_count: 4, create_new_menu_groups: createNewMenuGroups, merge_request_menu: mergeRequestMenuGroup, + projects_path: 'path/to/projects', + groups_path: 'path/to/groups', support_path: '/support', display_whats_new: true, whats_new_most_recent_release_items_count: 5, @@ -74,4 +84,181 @@ export const sidebarData = { show_version_check: false, gitlab_version: { major: 16, minor: 0 }, gitlab_version_check: { severity: 'success' }, + gitlab_com_and_canary: false, + canary_toggle_com_url: 'https://next.gitlab.com', +}; + +export const userMenuMockStatus = { + can_update: false, + busy: false, + customized: false, + emoji: 'art', + message: 'Working on user menu in super sidebar', + availability: 'busy', + clear_after: '2023-02-09 20:06:35 UTC', +}; + +export const userMenuMockPipelineMinutes = { + show_buy_pipeline_minutes: false, + show_notification_dot: false, + callout_attrs: { + feature_id: 'pipeline_minutes', + dismiss_endpoint: '/-/dismiss', + }, + buy_pipeline_minutes_path: '/buy/pipeline_minutes', + tracking_attrs: { + 'track-action': 'trackAction', + 'track-label': 'label', + 'track-property': 'property', + }, +}; + +export const userMenuMockData = { + name: 'Orange Fox', + username: 'thefox', + avatar_url: invalidUrl, + has_link_to_profile: true, + link_to_profile: '/thefox', + status: userMenuMockStatus, + trial: { + has_start_trial: false, + }, + settings: { + profile_path: invalidUrl, + profile_preferences_path: invalidUrl, + }, + pipeline_minutes: userMenuMockPipelineMinutes, + can_sign_out: false, + sign_out_link: invalidUrl, + gitlab_com_but_not_canary: true, + canary_toggle_com_url: 'https://next.gitlab.com', +}; + +export const cachedFrequentProjects = JSON.stringify([ + { + id: 1, + name: 'Cached project 1', + namespace: 'Cached Namespace 1 / Cached project 1', + webUrl: '/cached-namespace-1/cached-project-1', + avatarUrl: '/uploads/-/avatar1.png', + lastAccessedOn: 1676325329054, + frequency: 10, + }, + { + id: 2, + name: 'Cached project 2', + namespace: 'Cached Namespace 2 / Cached project 2', + webUrl: '/cached-namespace-2/cached-project-2', + avatarUrl: '/uploads/-/avatar2.png', + lastAccessedOn: 1674314684308, + frequency: 8, + }, + { + id: 3, + name: 'Cached project 3', + namespace: 'Cached Namespace 3 / Cached project 3', + webUrl: '/cached-namespace-3/cached-project-3', + avatarUrl: '/uploads/-/avatar3.png', + lastAccessedOn: 1664977333191, + frequency: 12, + }, + { + id: 4, + name: 'Cached project 4', + namespace: 'Cached Namespace 4 / Cached project 4', + webUrl: '/cached-namespace-4/cached-project-4', + avatarUrl: '/uploads/-/avatar4.png', + lastAccessedOn: 1674315407569, + frequency: 3, + }, + { + id: 5, + name: 'Cached project 5', + namespace: 'Cached Namespace 5 / Cached project 5', + webUrl: '/cached-namespace-5/cached-project-5', + avatarUrl: '/uploads/-/avatar5.png', + lastAccessedOn: 1677084729436, + frequency: 21, + }, + { + id: 6, + name: 'Cached project 6', + namespace: 'Cached Namespace 6 / Cached project 6', + webUrl: '/cached-namespace-6/cached-project-6', + avatarUrl: '/uploads/-/avatar6.png', + lastAccessedOn: 1676325329679, + frequency: 5, + }, +]); + +export const cachedFrequentGroups = JSON.stringify([ + { + id: 1, + name: 'Cached group 1', + namespace: 'Cached Namespace 1', + webUrl: '/cached-namespace-1/cached-group-1', + avatarUrl: '/uploads/-/avatar1.png', + lastAccessedOn: 1676325329054, + frequency: 10, + }, + { + id: 2, + name: 'Cached group 2', + namespace: 'Cached Namespace 2', + webUrl: '/cached-namespace-2/cached-group-2', + avatarUrl: '/uploads/-/avatar2.png', + lastAccessedOn: 1674314684308, + frequency: 8, + }, + { + id: 3, + name: 'Cached group 3', + namespace: 'Cached Namespace 3', + webUrl: '/cached-namespace-3/cached-group-3', + avatarUrl: '/uploads/-/avatar3.png', + lastAccessedOn: 1664977333191, + frequency: 12, + }, + { + id: 4, + name: 'Cached group 4', + namespace: 'Cached Namespace 4', + webUrl: '/cached-namespace-4/cached-group-4', + avatarUrl: '/uploads/-/avatar4.png', + lastAccessedOn: 1674315407569, + frequency: 3, + }, +]); + +export const searchUserProjectsAndGroupsResponseMock = { + data: { + projects: { + nodes: [ + { + id: 'gid://gitlab/Project/2', + name: 'Gitlab Shell', + namespace: 'Gitlab Org / Gitlab Shell', + webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-shell', + avatarUrl: null, + __typename: 'Project', + }, + ], + }, + + user: { + id: 'gid://gitlab/User/1', + groups: { + nodes: [ + { + id: 'gid://gitlab/Group/60', + name: 'GitLab Instance', + namespace: 'gitlab-instance-2e4abb29', + webUrl: 'http://gdk.test:3000/groups/gitlab-instance-2e4abb29', + avatarUrl: null, + __typename: 'Group', + }, + ], + }, + }, + }, }; diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js new file mode 100644 index 00000000000..3824965970b --- /dev/null +++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js @@ -0,0 +1,157 @@ +import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils'; +import { getCookie, setCookie } from '~/lib/utils/common_utils'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { + SIDEBAR_COLLAPSED_CLASS, + SIDEBAR_COLLAPSED_COOKIE, + SIDEBAR_COLLAPSED_COOKIE_EXPIRATION, + toggleSuperSidebarCollapsed, + initSuperSidebarCollapsedState, + bindSuperSidebarCollapsedEvents, + findPage, + findSidebar, + findToggles, +} from '~/super_sidebar/super_sidebar_collapsed_state_manager'; + +const { xl, sm } = breakpoints; + +jest.mock('~/lib/utils/common_utils', () => ({ + getCookie: jest.fn(), + setCookie: jest.fn(), +})); + +const pageHasCollapsedClass = (hasClass) => { + if (hasClass) { + expect(findPage().classList).toContain(SIDEBAR_COLLAPSED_CLASS); + } else { + expect(findPage().classList).not.toContain(SIDEBAR_COLLAPSED_CLASS); + } +}; + +describe('Super Sidebar Collapsed State Manager', () => { + beforeEach(() => { + setHTMLFixture(` + <div class="page-with-super-sidebar"> + <aside class="super-sidebar"></aside> + <button class="js-super-sidebar-toggle"></button> + </div> + `); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + describe('toggleSuperSidebarCollapsed', () => { + it.each` + collapsed | saveCookie | windowWidth | hasClass + ${true} | ${true} | ${xl} | ${true} + ${true} | ${false} | ${xl} | ${true} + ${true} | ${true} | ${sm} | ${true} + ${true} | ${false} | ${sm} | ${true} + ${false} | ${true} | ${xl} | ${false} + ${false} | ${false} | ${xl} | ${false} + ${false} | ${true} | ${sm} | ${false} + ${false} | ${false} | ${sm} | ${false} + `( + 'when collapsed is $collapsed, saveCookie is $saveCookie, and windowWidth is $windowWidth then page class contains `page-with-super-sidebar-collapsed` is $hasClass', + ({ collapsed, saveCookie, windowWidth, hasClass }) => { + jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth); + + toggleSuperSidebarCollapsed(collapsed, saveCookie); + + pageHasCollapsedClass(hasClass); + expect(findSidebar().ariaHidden).toBe(collapsed); + expect(findSidebar().inert).toBe(collapsed); + + if (saveCookie && windowWidth >= xl) { + expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, collapsed, { + expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION, + }); + } else { + expect(setCookie).not.toHaveBeenCalled(); + } + }, + ); + + describe('focus', () => { + it.each` + collapsed | isUserAction + ${false} | ${true} + ${false} | ${false} + ${true} | ${true} + ${true} | ${false} + `( + 'when collapsed is $collapsed, isUserAction is $isUserAction', + ({ collapsed, isUserAction }) => { + const sidebar = findSidebar(); + jest.spyOn(sidebar, 'focus'); + toggleSuperSidebarCollapsed(collapsed, false, isUserAction); + + if (!collapsed && isUserAction) { + expect(sidebar.focus).toHaveBeenCalled(); + } else { + expect(sidebar.focus).not.toHaveBeenCalled(); + } + }, + ); + }); + }); + + describe('initSuperSidebarCollapsedState', () => { + it.each` + windowWidth | cookie | hasClass + ${xl} | ${undefined} | ${false} + ${sm} | ${undefined} | ${true} + ${xl} | ${'true'} | ${true} + ${sm} | ${'true'} | ${true} + `( + 'sets page class to `page-with-super-sidebar-collapsed` when windowWidth is $windowWidth and cookie value is $cookie', + ({ windowWidth, cookie, hasClass }) => { + jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth); + getCookie.mockReturnValue(cookie); + + initSuperSidebarCollapsedState(); + + pageHasCollapsedClass(hasClass); + expect(setCookie).not.toHaveBeenCalled(); + }, + ); + }); + + describe('bindSuperSidebarCollapsedEvents', () => { + it.each` + windowWidth | cookie | hasClass + ${xl} | ${undefined} | ${true} + ${sm} | ${undefined} | ${true} + ${xl} | ${'true'} | ${false} + ${sm} | ${'true'} | ${false} + `( + 'toggle click sets page class to `page-with-super-sidebar-collapsed` when windowWidth is $windowWidth and cookie value is $cookie', + ({ windowWidth, cookie, hasClass }) => { + setHTMLFixture(` + <div class="page-with-super-sidebar ${cookie ? SIDEBAR_COLLAPSED_CLASS : ''}"> + <aside class="super-sidebar"></aside> + <button class="js-super-sidebar-toggle"></button> + </div> + `); + jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth); + getCookie.mockReturnValue(cookie); + + bindSuperSidebarCollapsedEvents(); + + findToggles()[0].click(); + + pageHasCollapsedClass(hasClass); + + if (windowWidth >= xl) { + expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, !cookie, { + expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION, + }); + } else { + expect(setCookie).not.toHaveBeenCalled(); + } + }, + ); + }); +}); diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js new file mode 100644 index 00000000000..1f236616e77 --- /dev/null +++ b/spec/frontend/super_sidebar/utils_spec.js @@ -0,0 +1,160 @@ +import { + getTopFrequentItems, + trackContextAccess, + formatContextSwitcherItems, +} from '~/super_sidebar/utils'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import AccessorUtilities from '~/lib/utils/accessor'; +import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants'; +import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data'; +import { searchUserProjectsAndGroupsResponseMock } from './mock_data'; + +describe('Super sidebar utils spec', () => { + describe('getTopFrequentItems', () => { + const maxItems = 3; + + it('returns empty array if no items provided', () => { + const result = getTopFrequentItems(); + + expect(result.length).toBe(0); + }); + + it('returns the requested amount of items', () => { + const result = getTopFrequentItems(unsortedFrequentItems, maxItems); + + expect(result.length).toBe(maxItems); + }); + + it('sorts frequent items in order of frequency and lastAccessedOn', () => { + const result = getTopFrequentItems(unsortedFrequentItems, maxItems); + const expectedResult = sortedFrequentItems.slice(0, maxItems); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('trackContextAccess', () => { + useLocalStorageSpy(); + + const username = 'root'; + const context = { + namespace: 'groups', + item: { id: 1 }, + }; + const storageKey = `${username}/frequent-${context.namespace}`; + + it('returns `false` if local storage is not available', () => { + jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false); + + expect(trackContextAccess()).toBe(false); + }); + + it('creates a new item if it does not exist in the local storage', () => { + trackContextAccess(username, context); + + expect(window.localStorage.setItem).toHaveBeenCalledWith( + storageKey, + JSON.stringify([ + { + id: 1, + frequency: 1, + lastAccessedOn: Date.now(), + }, + ]), + ); + }); + + it('updates existing item if it was persisted to the local storage over 15 minutes ago', () => { + window.localStorage.setItem( + storageKey, + JSON.stringify([ + { + id: 1, + frequency: 2, + lastAccessedOn: Date.now() - FIFTEEN_MINUTES_IN_MS - 1, + }, + ]), + ); + trackContextAccess(username, context); + + expect(window.localStorage.setItem).toHaveBeenCalledWith( + storageKey, + JSON.stringify([ + { + id: 1, + frequency: 3, + lastAccessedOn: Date.now(), + }, + ]), + ); + }); + + it('leaves item as is if it was persisted to the local storage under 15 minutes ago', () => { + const jsonString = JSON.stringify([ + { + id: 1, + frequency: 2, + lastAccessedOn: Date.now() - FIFTEEN_MINUTES_IN_MS, + }, + ]); + window.localStorage.setItem(storageKey, jsonString); + + expect(window.localStorage.setItem).toHaveBeenCalledTimes(1); + expect(window.localStorage.setItem).toHaveBeenCalledWith(storageKey, jsonString); + + trackContextAccess(username, context); + + expect(window.localStorage.setItem).toHaveBeenCalledTimes(3); + expect(window.localStorage.setItem).toHaveBeenLastCalledWith(storageKey, jsonString); + }); + + it('replaces the least popular item in the local storage once the persisted items limit has been hit', () => { + // Add the maximum amount of items to the local storage, in increasing popularity + const storedItems = Array.from({ length: FREQUENT_ITEMS.MAX_COUNT }).map((_, i) => ({ + id: i + 1, + frequency: i + 1, + lastAccessedOn: Date.now(), + })); + // The first item is considered the least popular one as it has the lowest frequency (1) + const [leastPopularItem] = storedItems; + // Persist the list to the local storage + const jsonString = JSON.stringify(storedItems); + window.localStorage.setItem(storageKey, jsonString); + // Track some new item that hasn't been visited yet + const newItem = { + id: FREQUENT_ITEMS.MAX_COUNT + 1, + }; + trackContextAccess(username, { + namespace: 'groups', + item: newItem, + }); + // Finally, retrieve the final data from the local storage + const finallyStoredItems = JSON.parse(window.localStorage.getItem(storageKey)); + + expect(finallyStoredItems).not.toEqual(expect.arrayContaining([leastPopularItem])); + expect(finallyStoredItems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: newItem.id, + frequency: 1, + }), + ]), + ); + }); + }); + + describe('formatContextSwitcherItems', () => { + it('returns the formatted items', () => { + const projects = searchUserProjectsAndGroupsResponseMock.data.projects.nodes; + expect(formatContextSwitcherItems(projects)).toEqual([ + { + id: projects[0].id, + avatar: null, + title: projects[0].name, + subtitle: 'Gitlab Org', + link: projects[0].webUrl, + }, + ]); + }); + }); +}); diff --git a/spec/frontend/syntax_highlight_spec.js b/spec/frontend/syntax_highlight_spec.js index 1be6c213350..a573c37ca44 100644 --- a/spec/frontend/syntax_highlight_spec.js +++ b/spec/frontend/syntax_highlight_spec.js @@ -1,14 +1,10 @@ -/* eslint-disable no-return-assign */ import $ from 'jquery'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import syntaxHighlight from '~/syntax_highlight'; describe('Syntax Highlighter', () => { const stubUserColorScheme = (value) => { - if (window.gon == null) { - window.gon = {}; - } - return (window.gon.user_color_scheme = value); + window.gon.user_color_scheme = value; }; // We have to bind `document.querySelectorAll` to `document` to not mess up the fn's context diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js index b1726a2c0ef..8438bdb7db0 100644 --- a/spec/frontend/tags/components/delete_tag_modal_spec.js +++ b/spec/frontend/tags/components/delete_tag_modal_spec.js @@ -44,10 +44,6 @@ const findFormInput = () => wrapper.findComponent(GlFormInput); const findForm = () => wrapper.find('form'); describe('Delete tag modal', () => { - afterEach(() => { - wrapper.destroy(); - }); - describe('Deleting a regular tag', () => { const expectedTitle = 'Delete tag. Are you ABSOLUTELY SURE?'; const expectedMessage = "You're about to permanently delete the tag test-tag."; diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js index 99f61a31dbd..c60c6c79f17 100644 --- a/spec/frontend/terms/components/app_spec.js +++ b/spec/frontend/terms/components/app_spec.js @@ -37,10 +37,6 @@ describe('TermsApp', () => { isLoggedIn.mockReturnValue(true); }); - afterEach(() => { - wrapper.destroy(); - }); - const findFormWithAction = (path) => wrapper.find(`form[action="${path}"]`); const findButton = (path) => findFormWithAction(path).find('button[type="submit"]'); const findScrollableViewport = () => wrapper.findByTestId('scrollable-viewport'); diff --git a/spec/frontend/terraform/components/empty_state_spec.js b/spec/frontend/terraform/components/empty_state_spec.js index 21bfff5f1be..db3de556244 100644 --- a/spec/frontend/terraform/components/empty_state_spec.js +++ b/spec/frontend/terraform/components/empty_state_spec.js @@ -16,10 +16,6 @@ describe('EmptyStateComponent', () => { wrapper = shallowMount(EmptyState, { propsData }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render content', () => { expect(findEmptyState().props('title')).toBe( "Your project doesn't have any Terraform state files", diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js index 911bb8878da..ca9aa26a776 100644 --- a/spec/frontend/terraform/components/init_command_modal_spec.js +++ b/spec/frontend/terraform/components/init_command_modal_spec.js @@ -49,10 +49,6 @@ describe('InitCommandModal', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('on rendering', () => { it('renders the explanatory text', () => { expect(findExplanatoryText().text()).toContain('personal access token'); diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js index 40b7448d78d..31a644b39b4 100644 --- a/spec/frontend/terraform/components/states_table_actions_spec.js +++ b/spec/frontend/terraform/components/states_table_actions_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui'; +import { GlDropdown, GlModal, GlSprintf, GlFormInput } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; @@ -85,6 +85,7 @@ describe('StatesTableActions', () => { const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]'); const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]'); const findRemoveModal = () => wrapper.findComponent(GlModal); + const findFormInput = () => wrapper.findComponent(GlFormInput); beforeEach(() => { return createComponent(); @@ -96,7 +97,6 @@ describe('StatesTableActions', () => { toast = null; unlockResponse = null; updateStateResponse = null; - wrapper.destroy(); }); describe('when the state is loading', () => { @@ -296,9 +296,7 @@ describe('StatesTableActions', () => { describe('when state name is present', () => { beforeEach(async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - await wrapper.setData({ removeConfirmText: defaultProps.state.name }); + await findFormInput().vm.$emit('input', defaultProps.state.name); findRemoveModal().vm.$emit('ok'); diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js index 0b3b169891b..7c783c9f717 100644 --- a/spec/frontend/terraform/components/states_table_spec.js +++ b/spec/frontend/terraform/components/states_table_spec.js @@ -127,7 +127,7 @@ describe('StatesTable', () => { propsData, provide: { projectPath: 'path/to/project' }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }), ); @@ -140,11 +140,6 @@ describe('StatesTable', () => { return createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it.each` name | toolTipText | hasBadge | loading | lineNumber ${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0} diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js index 580951e799a..dc59e2769f6 100644 --- a/spec/frontend/terraform/components/terraform_list_spec.js +++ b/spec/frontend/terraform/components/terraform_list_spec.js @@ -63,11 +63,6 @@ describe('TerraformList', () => { const findStatesTable = () => wrapper.findComponent(StatesTable); const findTab = () => wrapper.findComponent(GlTab); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when the terraform query has succeeded', () => { describe('when there is a list of terraform states', () => { const states = [ diff --git a/spec/frontend/toggles/index_spec.js b/spec/frontend/toggles/index_spec.js index f8c43e0ad0c..89e35991914 100644 --- a/spec/frontend/toggles/index_spec.js +++ b/spec/frontend/toggles/index_spec.js @@ -44,7 +44,6 @@ describe('toggles/index.js', () => { afterEach(() => { document.body.innerHTML = ''; instance = null; - toggleWrapper = null; }); describe('initToggle', () => { diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js index fcd1a33fa68..1ca58053e68 100644 --- a/spec/frontend/token_access/inbound_token_access_spec.js +++ b/spec/frontend/token_access/inbound_token_access_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue'; import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql'; import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql'; @@ -26,7 +26,7 @@ const error = new Error(message); Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('TokenAccess component', () => { let wrapper; diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js index 3a68f247aa6..cdb385aa743 100644 --- a/spec/frontend/token_access/opt_in_jwt_spec.js +++ b/spec/frontend/token_access/opt_in_jwt_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { OPT_IN_JWT_HELP_LINK } from '~/token_access/constants'; import OptInJwt from '~/token_access/components/opt_in_jwt.vue'; import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql'; @@ -16,7 +16,7 @@ const error = new Error(errorMessage); Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('OptInJwt component', () => { let wrapper; diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js index 893a021197f..347ea1178bc 100644 --- a/spec/frontend/token_access/outbound_token_access_spec.js +++ b/spec/frontend/token_access/outbound_token_access_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue'; import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql'; import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql'; @@ -26,7 +26,7 @@ const error = new Error(message); Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); describe('TokenAccess component', () => { let wrapper; diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js index 7f269ee5fda..cff16fd125c 100644 --- a/spec/frontend/token_access/token_access_app_spec.js +++ b/spec/frontend/token_access/token_access_app_spec.js @@ -11,12 +11,8 @@ describe('TokenAccessApp component', () => { const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess); const findOptInJwt = () => wrapper.findComponent(OptInJwt); - const createComponent = (flagState = false) => { - wrapper = shallowMount(TokenAccessApp, { - provide: { - glFeatures: { ciInboundJobTokenScope: flagState }, - }, - }); + const createComponent = () => { + wrapper = shallowMount(TokenAccessApp); }; describe('default', () => { @@ -32,12 +28,6 @@ describe('TokenAccessApp component', () => { expect(findOutboundTokenAccess().exists()).toBe(true); }); - it('does not render the inbound token access component', () => { - expect(findInboundTokenAccess().exists()).toBe(false); - }); - }); - - describe('with feature flag enabled', () => { it('renders the inbound token access component', () => { createComponent(true); diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js index b51d8b3ccea..7654aa09b0a 100644 --- a/spec/frontend/token_access/token_projects_table_spec.js +++ b/spec/frontend/token_access/token_projects_table_spec.js @@ -6,14 +6,19 @@ import { mockProjects, mockFields } from './mock_data'; describe('Token projects table', () => { let wrapper; - const createComponent = () => { + const defaultProps = { + tableFields: mockFields, + projects: mockProjects, + }; + + const createComponent = (props) => { wrapper = mountExtended(TokenProjectsTable, { provide: { fullPath: 'root/ci-project', }, propsData: { - tableFields: mockFields, - projects: mockProjects, + ...defaultProps, + ...props, }, }); }; @@ -25,31 +30,52 @@ describe('Token projects table', () => { const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name'); const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace'); - beforeEach(() => { + it('displays a table', () => { createComponent(); - }); - it('displays a table', () => { expect(findTable().exists()).toBe(true); }); it('displays the correct amount of table rows', () => { + createComponent(); + expect(findAllTableRows()).toHaveLength(mockProjects.length); }); it('delete project button emits event with correct project to delete', async () => { + createComponent(); + await findDeleteProjectBtn().trigger('click'); expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]); }); it('does not show the remove icon if the project is locked', () => { + createComponent(); + // currently two mock projects with one being a locked project expect(findAllDeleteProjectBtn()).toHaveLength(1); }); it('displays project and namespace cells', () => { + createComponent(); + expect(findProjectNameCell().text()).toBe('merge-train-stuff'); expect(findProjectNamespaceCell().text()).toBe('root'); }); + + it('displays empty string for namespace when namespace is null', () => { + const nullNamespace = { + id: '1', + name: 'merge-train-stuff', + namespace: null, + fullPath: 'root/merge-train-stuff', + isLocked: false, + __typename: 'Project', + }; + + createComponent({ projects: [nullNamespace] }); + + expect(findProjectNamespaceCell().text()).toBe(''); + }); }); diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js index d5a63a99601..e473091f405 100644 --- a/spec/frontend/tooltips/components/tooltips_spec.js +++ b/spec/frontend/tooltips/components/tooltips_spec.js @@ -30,11 +30,6 @@ describe('tooltips/components/tooltips.vue', () => { const allTooltips = () => wrapper.findAllComponents(GlTooltip); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('addTooltips', () => { let target; diff --git a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js index cb70ea4e72d..3508bf7cfde 100644 --- a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js +++ b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js @@ -23,10 +23,6 @@ describe('UsageQuotasApp', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle'); it('renders the view title', () => { diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js index 3379af3f41c..1a200090805 100644 --- a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js +++ b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js @@ -53,10 +53,6 @@ describe('ProjectStorageApp', () => { const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link'); const findUsageGraph = () => wrapper.findComponent(UsageGraph); - afterEach(() => { - wrapper.destroy(); - }); - describe('with apollo fetching successful', () => { let mockApollo; diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js index ce489f69cad..6065ec9e4bf 100644 --- a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js +++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js @@ -54,9 +54,6 @@ describe('ProjectStorageDetail', () => { beforeEach(() => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); describe('with storage types', () => { it.each(storageTypes)( diff --git a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js index 1eb3386bfb8..364cf1e587b 100644 --- a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js +++ b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js @@ -16,10 +16,6 @@ describe('StorageTypeIcon', () => { const findGlIcon = () => wrapper.findComponent(GlIcon); describe('rendering icon', () => { - afterEach(() => { - wrapper.destroy(); - }); - it.each` expected | provided ${'doc-image'} | ${'lfsObjectsSize'} diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js index 75b970d937a..02268e1c9d8 100644 --- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js +++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js @@ -39,10 +39,6 @@ describe('Storage Counter usage graph component', () => { mountComponent(data); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the legend in order', () => { const types = wrapper.findAll('[data-testid="storage-type-legend"]'); diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js index 161eb036361..603289ac11e 100644 --- a/spec/frontend/user_lists/components/user_lists_spec.js +++ b/spec/frontend/user_lists/components/user_lists_spec.js @@ -39,11 +39,6 @@ describe('~/user_lists/components/user_lists.vue', () => { const newButton = () => within(wrapper.element).queryAllByText('New user list'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('without permissions', () => { const provideData = { ...mockProvide, diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js index 3324b040b86..96e9705f02b 100644 --- a/spec/frontend/user_lists/components/user_lists_table_spec.js +++ b/spec/frontend/user_lists/components/user_lists_table_spec.js @@ -22,10 +22,6 @@ describe('User Lists Table', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should display the details of a user list', () => { expect(wrapper.find('[data-testid="ffUserListName"]').text()).toBe(userList.name); expect(wrapper.find('[data-testid="ffUserListIds"]').text()).toBe( diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js index 8ce071c075f..3808cc8b0fc 100644 --- a/spec/frontend/user_popovers_spec.js +++ b/spec/frontend/user_popovers_spec.js @@ -10,8 +10,6 @@ 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]'; @@ -60,15 +58,6 @@ describe('User Popovers', () => { }); }; - beforeEach(() => { - origGon = window.gon; - window.gon = {}; - }); - - afterEach(() => { - window.gon = origGon; - }); - describe('when signed out', () => { beforeEach(() => { setupTestSubject(); diff --git a/spec/frontend/validators/length_validator_spec.js b/spec/frontend/validators/length_validator_spec.js new file mode 100644 index 00000000000..ece8238b3e3 --- /dev/null +++ b/spec/frontend/validators/length_validator_spec.js @@ -0,0 +1,91 @@ +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import LengthValidator, { isAboveMaxLength, isBelowMinLength } from '~/validators/length_validator'; + +describe('length_validator', () => { + describe('isAboveMaxLength', () => { + it('should return true if the string is longer than the maximum length', () => { + expect(isAboveMaxLength('123456', '5')).toBe(true); + }); + + it('should return false if the string is shorter than the maximum length', () => { + expect(isAboveMaxLength('1234', '5')).toBe(false); + }); + }); + + describe('isBelowMinLength', () => { + it('should return true if the string is shorter than the minimum length and not empty', () => { + expect(isBelowMinLength('1234', '5', 'false')).toBe(true); + }); + + it('should return false if the string is longer than the minimum length', () => { + expect(isBelowMinLength('123456', '5', 'false')).toBe(false); + }); + + it('should return false if the string is empty and allowed to be empty', () => { + expect(isBelowMinLength('', '5', 'true')).toBe(false); + }); + + it('should return true if the string is empty and not allowed to be empty', () => { + expect(isBelowMinLength('', '5', 'false')).toBe(true); + }); + }); + + describe('LengthValidator', () => { + let input; + let validator; + + beforeEach(() => { + setHTMLFixture( + '<div class="container"><input class="js-validate-length" /><span class="gl-field-error"></span></div>', + ); + input = document.querySelector('input'); + input.dataset.minLength = '3'; + input.dataset.maxLength = '5'; + input.dataset.minLengthMessage = 'Too short'; + input.dataset.maxLengthMessage = 'Too long'; + validator = new LengthValidator({ container: '.container' }); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('sets error message for input with value longer than max length', () => { + input.value = '123456'; + input.dispatchEvent(new Event('input')); + expect(validator.errorMessage).toBe('Too long'); + }); + + it('sets error message for input with value shorter than min length', () => { + input.value = '12'; + input.dispatchEvent(new Event('input')); + expect(validator.errorMessage).toBe('Too short'); + }); + + it('does not set error message for input with valid length', () => { + input.value = '123'; + input.dispatchEvent(new Event('input')); + expect(validator.errorMessage).toBeNull(); + }); + + it('does not set error message for empty input if allowEmpty is true', () => { + input.dataset.allowEmpty = 'true'; + input.value = ''; + input.dispatchEvent(new Event('input')); + expect(validator.errorMessage).toBeNull(); + }); + + it('sets error message for empty input if allowEmpty is false', () => { + input.dataset.allowEmpty = 'false'; + input.value = ''; + input.dispatchEvent(new Event('input')); + expect(validator.errorMessage).toBe('Too short'); + }); + + it('sets error message for empty input if allowEmpty is not defined', () => { + input.value = ''; + input.dispatchEvent(new Event('input')); + expect(validator.errorMessage).toBe('Too short'); + }); + }); +}); diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js new file mode 100644 index 00000000000..b6234c51535 --- /dev/null +++ b/spec/frontend/vue_compat_test_setup.js @@ -0,0 +1,60 @@ +/* eslint-disable import/no-commonjs */ +const Vue = require('vue'); +const VTU = require('@vue/test-utils'); +const { installCompat: installVTUCompat, fullCompatConfig } = require('vue-test-utils-compat'); + +if (global.document) { + const compatConfig = { + MODE: 2, + + GLOBAL_MOUNT: 'suppress-warning', + GLOBAL_EXTEND: 'suppress-warning', + GLOBAL_PROTOTYPE: 'suppress-warning', + RENDER_FUNCTION: 'suppress-warning', + + INSTANCE_DESTROY: 'suppress-warning', + INSTANCE_DELETE: 'suppress-warning', + + INSTANCE_ATTRS_CLASS_STYLE: 'suppress-warning', + INSTANCE_CHILDREN: 'suppress-warning', + INSTANCE_SCOPED_SLOTS: 'suppress-warning', + INSTANCE_LISTENERS: 'suppress-warning', + INSTANCE_EVENT_EMITTER: 'suppress-warning', + INSTANCE_EVENT_HOOKS: 'suppress-warning', + INSTANCE_SET: 'suppress-warning', + GLOBAL_OBSERVABLE: 'suppress-warning', + GLOBAL_SET: 'suppress-warning', + COMPONENT_FUNCTIONAL: 'suppress-warning', + COMPONENT_V_MODEL: 'suppress-warning', + CUSTOM_DIR: 'suppress-warning', + OPTIONS_BEFORE_DESTROY: 'suppress-warning', + OPTIONS_DATA_MERGE: 'suppress-warning', + OPTIONS_DATA_FN: 'suppress-warning', + OPTIONS_DESTROYED: 'suppress-warning', + ATTR_FALSE_VALUE: 'suppress-warning', + + COMPILER_V_ON_NATIVE: 'suppress-warning', + COMPILER_V_BIND_OBJECT_ORDER: 'suppress-warning', + + CONFIG_WHITESPACE: 'suppress-warning', + CONFIG_OPTION_MERGE_STRATS: 'suppress-warning', + PRIVATE_APIS: 'suppress-warning', + WATCH_ARRAY: 'suppress-warning', + }; + + let compatH; + Vue.config.compilerOptions.whitespace = 'condense'; + Vue.createApp({ + compatConfig: { + MODE: 3, + RENDER_FUNCTION: 'suppress-warning', + }, + render(h) { + compatH = h; + }, + }).mount(document.createElement('div')); + + Vue.configureCompat(compatConfig); + installVTUCompat(VTU, fullCompatConfig, compatH); + VTU.config.global.renderStubDefaultSlot = true; +} diff --git a/spec/frontend/vue_merge_request_widget/components/action_buttons.js b/spec/frontend/vue_merge_request_widget/components/action_buttons.js index 6d714aeaf18..7334f061dc9 100644 --- a/spec/frontend/vue_merge_request_widget/components/action_buttons.js +++ b/spec/frontend/vue_merge_request_widget/components/action_buttons.js @@ -11,10 +11,6 @@ function factory(propsData = {}) { } describe('MR widget extension actions', () => { - afterEach(() => { - wrapper.destroy(); - }); - describe('tertiaryButtons', () => { it('renders buttons', () => { factory({ diff --git a/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js index 063425454d7..4164a7df482 100644 --- a/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js @@ -14,10 +14,6 @@ function factory(propsData) { } describe('Widget added commit message', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('displays changes where not merged when state is closed', () => { factory({ state: 'closed' }); diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js index bf208f16d18..e78e1be7882 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js @@ -1,26 +1,32 @@ -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { GlButton, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { createAlert } from '~/flash'; +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { createAlert } from '~/alert'; import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue'; import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue'; import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue'; import { - FETCH_LOADING, - FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR, } from '~/vue_merge_request_widget/components/approvals/messages'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql'; +import { createCanApproveResponse } from 'jest/approvals/mock_data'; + +Vue.use(VueApollo); const mockAlertDismiss = jest.fn(); -jest.mock('~/flash', () => ({ +jest.mock('~/alert', () => ({ createAlert: jest.fn().mockImplementation(() => ({ dismiss: mockAlertDismiss, })), })); -const RULE_NAME = 'first_rule'; const TEST_HELP_PATH = 'help/path'; const testApprovedBy = () => [1, 7, 10].map((id) => ({ id })); const testApprovals = () => ({ @@ -34,15 +40,18 @@ const testApprovals = () => ({ require_password_to_approve: false, invalid_approvers_rules: [], }); -const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] }); describe('MRWidget approvals', () => { let wrapper; let service; let mr; - const createComponent = (props = {}) => { + const createComponent = (props = {}, response = approvedByCurrentUser) => { + const requestHandlers = [[approvedByQuery, jest.fn().mockResolvedValue(response)]]; + const apolloProvider = createMockApollo(requestHandlers); + wrapper = shallowMount(Approvals, { + apolloProvider, propsData: { mr, service, @@ -68,15 +77,10 @@ describe('MRWidget approvals', () => { }; const findSummary = () => wrapper.findComponent(ApprovalsSummary); const findOptionalSummary = () => wrapper.findComponent(ApprovalsSummaryOptional); - const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]'); beforeEach(() => { service = { ...{ - fetchApprovals: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), - fetchApprovalSettings: jest - .fn() - .mockReturnValue(Promise.resolve(testApprovalRulesResponse())), approveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), unapproveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), approveMergeRequestWithAuth: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), @@ -97,55 +101,21 @@ describe('MRWidget approvals', () => { }; jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('when created', () => { - it('shows loading message', async () => { - service = { - fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})), - }; - - createComponent(); - await nextTick(); - expect(wrapper.text()).toContain(FETCH_LOADING); - }); - - it('fetches approvals', () => { - createComponent(); - expect(service.fetchApprovals).toHaveBeenCalled(); - }); - }); - - describe('when fetch approvals error', () => { - beforeEach(() => { - jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject()); - createComponent(); - return nextTick(); - }); - - it('still shows loading message', () => { - expect(wrapper.text()).toContain(FETCH_LOADING); - }); - - it('flashes error', () => { - expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR }); - }); + gon.current_user_id = getIdFromGraphQLId( + approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes[0].id, + ); }); describe('action button', () => { describe('when mr is closed', () => { - beforeEach(() => { + beforeEach(async () => { + const response = createCanApproveResponse(); + mr.isOpen = false; - mr.approvals.user_has_approved = false; - mr.approvals.user_can_approve = true; - createComponent(); - return nextTick(); + createComponent({}, response); + await waitForPromises(); }); it('action is not rendered', () => { @@ -154,12 +124,12 @@ describe('MRWidget approvals', () => { }); describe('when user cannot approve', () => { - beforeEach(() => { - mr.approvals.user_has_approved = false; - mr.approvals.user_can_approve = false; + beforeEach(async () => { + const response = JSON.parse(JSON.stringify(approvedByCurrentUser)); + response.data.project.mergeRequest.approvedBy.nodes = []; - createComponent(); - return nextTick(); + createComponent({}, response); + await waitForPromises(); }); it('action is not rendered', () => { @@ -168,15 +138,16 @@ describe('MRWidget approvals', () => { }); describe('when user can approve', () => { + let canApproveResponse; + beforeEach(() => { - mr.approvals.user_has_approved = false; - mr.approvals.user_can_approve = true; + canApproveResponse = createCanApproveResponse(); }); describe('and MR is unapproved', () => { - beforeEach(() => { - createComponent(); - return nextTick(); + beforeEach(async () => { + createComponent({}, canApproveResponse); + await waitForPromises(); }); it('approve action is rendered', () => { @@ -190,30 +161,33 @@ describe('MRWidget approvals', () => { describe('and MR is approved', () => { beforeEach(() => { - mr.approvals.approved = true; + canApproveResponse.data.project.mergeRequest.approved = true; }); describe('with no approvers', () => { - beforeEach(() => { - mr.approvals.approved_by = []; - createComponent(); - return nextTick(); + beforeEach(async () => { + canApproveResponse.data.project.mergeRequest.approvedBy.nodes = []; + createComponent({}, canApproveResponse); + await nextTick(); }); - it('approve action (with inverted style) is rendered', () => { - expect(findActionData()).toEqual({ + it('approve action is rendered', () => { + expect(findActionData()).toMatchObject({ variant: 'confirm', text: 'Approve', - category: 'secondary', }); }); }); describe('with approvers', () => { - beforeEach(() => { - mr.approvals.approved_by = [{ user: { id: 7 } }]; - createComponent(); - return nextTick(); + beforeEach(async () => { + canApproveResponse.data.project.mergeRequest.approvedBy.nodes = + approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes; + + canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 2; + + createComponent({}, canApproveResponse); + await waitForPromises(); }); it('approve additionally action is rendered', () => { @@ -227,9 +201,9 @@ describe('MRWidget approvals', () => { }); describe('when approve action is clicked', () => { - beforeEach(() => { - createComponent(); - return nextTick(); + beforeEach(async () => { + createComponent({}, canApproveResponse); + await waitForPromises(); }); it('shows loading icon', () => { @@ -258,10 +232,6 @@ describe('MRWidget approvals', () => { it('emits to eventHub', () => { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); }); - - it('calls store setApprovals', () => { - expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals()); - }); }); describe('and error', () => { @@ -286,12 +256,12 @@ describe('MRWidget approvals', () => { }); describe('when user has approved', () => { - beforeEach(() => { - mr.approvals.user_has_approved = true; - mr.approvals.user_can_approve = false; + beforeEach(async () => { + const response = JSON.parse(JSON.stringify(approvedByCurrentUser)); - createComponent(); - return nextTick(); + createComponent({}, response); + + await waitForPromises(); }); it('revoke action is rendered', () => { @@ -316,10 +286,6 @@ describe('MRWidget approvals', () => { it('emits to eventHub', () => { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); }); - - it('calls store setApprovals', () => { - expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals()); - }); }); describe('and error', () => { @@ -329,7 +295,7 @@ describe('MRWidget approvals', () => { return nextTick(); }); - it('flashes error message', () => { + it('alerts error message', () => { expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR }); }); }); @@ -338,19 +304,24 @@ describe('MRWidget approvals', () => { }); describe('approvals optional summary', () => { + let optionalApprovalsResponse; + + beforeEach(() => { + optionalApprovalsResponse = JSON.parse(JSON.stringify(approvedByCurrentUser)); + }); + describe('when no approvals required and no approvers', () => { beforeEach(() => { - mr.approvals.approved_by = []; - mr.approvals.approvals_required = 0; - mr.approvals.user_has_approved = false; + optionalApprovalsResponse.data.project.mergeRequest.approvedBy.nodes = []; + optionalApprovalsResponse.data.project.mergeRequest.approvalsRequired = 0; }); describe('and can approve', () => { - beforeEach(() => { - mr.approvals.user_can_approve = true; + beforeEach(async () => { + optionalApprovalsResponse.data.project.mergeRequest.userPermissions.canApprove = true; - createComponent(); - return nextTick(); + createComponent({}, optionalApprovalsResponse); + await waitForPromises(); }); it('is shown', () => { @@ -363,11 +334,9 @@ describe('MRWidget approvals', () => { }); describe('and cannot approve', () => { - beforeEach(() => { - mr.approvals.user_can_approve = false; - - createComponent(); - return nextTick(); + beforeEach(async () => { + createComponent({}, optionalApprovalsResponse); + await nextTick(); }); it('is shown', () => { @@ -382,9 +351,9 @@ describe('MRWidget approvals', () => { }); describe('approvals summary', () => { - beforeEach(() => { + beforeEach(async () => { createComponent(); - return nextTick(); + await nextTick(); }); it('is rendered with props', () => { @@ -393,41 +362,7 @@ describe('MRWidget approvals', () => { expect(findOptionalSummary().exists()).toBe(false); expect(summary.exists()).toBe(true); expect(summary.props()).toMatchObject({ - projectPath: 'gitlab-org/gitlab', - iid: '1', - updatedCount: 0, - }); - }); - }); - - describe('invalid rules', () => { - beforeEach(() => { - mr.approvals.merge_request_approvers_available = true; - createComponent(); - }); - - it('does not render related components', () => { - expect(findInvalidRules().exists()).toBe(false); - }); - - describe('when invalid rules are present', () => { - beforeEach(() => { - mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }]; - createComponent(); - }); - - it('renders related components', () => { - const invalidRules = findInvalidRules(); - - expect(invalidRules.exists()).toBe(true); - - const invalidRulesText = invalidRules.text(); - - expect(invalidRulesText).toContain(RULE_NAME); - expect(invalidRulesText).toContain( - 'GitLab has approved this rule automatically to unblock the merge request.', - ); - expect(invalidRulesText).toContain('Learn more.'); + approvalState: approvedByCurrentUser.data.project.mergeRequest, }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js index e6fb0495947..bf3df70d423 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js @@ -13,11 +13,6 @@ describe('MRWidget approvals summary optional', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findHelpLink = () => wrapper.findComponent(GlLink); describe('when can approve', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js index e75ce7c60c9..8c6b3cc464c 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js @@ -1,11 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { mount } from '@vue/test-utils'; -import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_multiple_users.json'; -import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_no_approvals.json'; -import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql.json'; +import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_multiple_users.json'; +import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_no_approvals.json'; +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue'; import { @@ -14,32 +13,22 @@ import { APPROVED_BY_YOU_AND_OTHERS, } from '~/vue_merge_request_widget/components/approvals/messages'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; -import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approved_by.query.graphql'; Vue.use(VueApollo); describe('MRWidget approvals summary', () => { - const originalUserId = gon.current_user_id; let wrapper; - const createComponent = (response = approvedByCurrentUser) => { + const createComponent = (data = approvedByCurrentUser) => { wrapper = mount(ApprovalsSummary, { propsData: { - projectPath: 'gitlab-org/gitlab', - iid: '1', + approvalState: data.data.project.mergeRequest, }, - apolloProvider: createMockApollo([[approvedByQuery, jest.fn().mockResolvedValue(response)]]), }); }; const findAvatars = () => wrapper.findComponent(UserAvatarList); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - gon.current_user_id = originalUserId; - }); - describe('when approved', () => { beforeEach(async () => { createComponent(); diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js index 52e2393bf05..332f14a1721 100644 --- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js @@ -26,7 +26,6 @@ describe('Merge Requests Artifacts list app', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js index b7bf72cd215..bb049a5d52f 100644 --- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js @@ -18,10 +18,6 @@ describe('Artifacts List', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { mountComponent(data); }); diff --git a/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js index 198a4c2823a..3a621db7b44 100644 --- a/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js @@ -20,11 +20,6 @@ function factory(propsData) { } describe('MR widget extension child content', () => { - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders child components', () => { factory({ data: { diff --git a/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js index f3aa5bb774f..ffa6b5538d3 100644 --- a/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js @@ -11,10 +11,6 @@ function factory(propsData = {}) { } describe('MR widget extensions status icon', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('renders loading icon', () => { factory({ name: 'test', isLoading: true, iconName: 'failed' }); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js index 81f266d8070..6b22c2e26ac 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js @@ -25,10 +25,6 @@ describe('Merge Request Collapsible Extension', () => { const findErrorMessage = () => wrapper.find('.js-error-state'); const findIcon = () => wrapper.findComponent(GlIcon); - afterEach(() => { - wrapper.destroy(); - }); - describe('while collapsed', () => { beforeEach(() => { mountComponent(data); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js index 5d923d0383f..01178dab9bb 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js @@ -11,10 +11,6 @@ function createComponent(propsData = {}) { } describe('MrWidgetAlertMessage', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('should render a GlAert', () => { createComponent({ type: 'danger' }); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js index 8a42e2e2ce7..7eafccae083 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js @@ -29,7 +29,6 @@ describe('MrWidgetAuthor', () => { }); afterEach(() => { - wrapper.destroy(); window.gl = oldWindowGl; }); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js index 90a29d15488..534b745aed2 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js @@ -23,10 +23,6 @@ describe('MrWidgetAuthorTime', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders provided action text', () => { expect(wrapper.text()).toContain('Merged by'); }); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js index 8dadb0c65d0..25de76ba33c 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js @@ -13,10 +13,6 @@ describe('MrWidgetContainer', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('has layout', () => { factory(); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js index 6a9b019fb4f..090a96d576c 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js @@ -15,10 +15,6 @@ describe('MrWidgetIcon', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders icon and container', () => { expect(wrapper.element.className).toContain('circle-icon-container'); expect(wrapper.findComponent(GlIcon).props('name')).toEqual(TEST_ICON); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js index 13beb43e10b..18842e996de 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js @@ -29,10 +29,6 @@ describe('MrWidgetPipelineContainer', () => { mock.onGet().reply(HTTP_STATUS_OK, {}); }); - afterEach(() => { - wrapper.destroy(); - }); - const findDeploymentList = () => wrapper.findComponent(DeploymentList); const findCIErrorMessage = () => wrapper.findByTestId('ci-error-message'); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js index ec047fe0714..f284ec98a73 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js @@ -1,3 +1,4 @@ +import { GlModal } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; @@ -8,8 +9,11 @@ jest.mock('~/vue_shared/plugins/global_toast'); let wrapper; -function createWrapper(propsData) { +function createWrapper(propsData, provideData) { wrapper = mount(WidgetRebase, { + provide: { + ...provideData, + }, propsData, data() { return { @@ -19,6 +23,7 @@ function createWrapper(propsData) { userPermissions: { pushToSourceBranch: propsData.mr.canPushToSourceBranch, }, + pipelines: propsData.mr.pipelines, }, }; }, @@ -37,11 +42,8 @@ describe('Merge request widget rebase component', () => { const findRebaseMessageText = () => findRebaseMessage().text(); const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]'); const findRebaseWithoutCiButton = () => wrapper.find('[data-testid="rebase-without-ci-button"]'); + const findModal = () => wrapper.findComponent(GlModal); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); describe('while rebasing', () => { it('should show progress message', () => { createWrapper({ @@ -199,6 +201,72 @@ describe('Merge request widget rebase component', () => { expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); }); }); + + describe('security modal', () => { + it('displays modal and rebases after confirming', () => { + createWrapper( + { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + sourceProjectFullPath: 'user/forked', + targetProjectFullPath: 'root/original', + pipelines: { + nodes: [ + { + id: '1', + project: { + id: '2', + fullPath: 'user/forked', + }, + }, + ], + }, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + { canCreatePipelineInTargetProject: true }, + ); + + findModal().vm.show = jest.fn(); + + findStandardRebaseButton().vm.$emit('click'); + + expect(findModal().vm.show).toHaveBeenCalled(); + + findModal().vm.$emit('primary'); + + expect(rebaseMock).toHaveBeenCalled(); + }); + + it('does not display modal', () => { + createWrapper( + { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + sourceProjectFullPath: 'user/forked', + targetProjectFullPath: 'root/original', + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + { canCreatePipelineInTargetProject: false }, + ); + + findModal().vm.show = jest.fn(); + + findStandardRebaseButton().vm.$emit('click'); + + expect(findModal().vm.show).not.toHaveBeenCalled(); + expect(rebaseMock).toHaveBeenCalled(); + }); + }); }); describe('without permissions', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js index 15522f7ac1d..42a16090510 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js @@ -9,10 +9,6 @@ describe('MRWidgetRelatedLinks', () => { wrapper = shallowMount(RelatedLinks, { propsData }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('closesText', () => { it('returns Closes text for open merge request', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js index 530549b7b9c..b210327aa31 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js @@ -17,11 +17,6 @@ describe('MR widget status icon component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('while loading', () => { it('renders loading icon', () => { createWrapper({ status: 'loading' }); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js index 73358edee78..70c76687a79 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js @@ -18,10 +18,6 @@ describe('MRWidgetSuggestPipeline', () => { describe('template', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - describe('core functionality', () => { const findOkBtn = () => wrapper.find('[data-testid="ok"]'); let trackingSpy; diff --git a/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js index e393b56034d..48484551d59 100644 --- a/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js @@ -17,10 +17,6 @@ describe('review app link', () => { wrapper = shallowMount(ReviewAppLink, { propsData: props }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders provided link as href attribute', () => { expect(wrapper.attributes('href')).toBe(props.link); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js index c0add94e6ed..f520c6a4f78 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js @@ -26,10 +26,6 @@ describe('Commits edit component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findTextarea = () => wrapper.find('.form-control'); it('has a correct label', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js index e4448346685..c2ab0e384e8 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js @@ -12,10 +12,6 @@ function factory(propsData = {}) { } describe('Merge request widget merge checks failed state component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it.each` mrState | displayText ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'} diff --git a/spec/frontend/vue_merge_request_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 c9aca01083d..7d471b91c37 100644 --- a/spec/frontend/vue_merge_request_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 @@ -37,10 +37,6 @@ describe('MergeFailedPipelineConfirmationDialog', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render informational text explaining why merging immediately can be dangerous', () => { expect(trimText(wrapper.text())).toContain( 'The latest pipeline for this merge request did not succeed. The latest changes are unverified. Are you sure you want to attempt to merge?', diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js index 08700e834d7..3e18ee75125 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js @@ -10,11 +10,6 @@ describe('MRWidgetArchived', () => { wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders error icon', () => { expect(wrapper.findComponent(StateContainer).exists()).toBe(true); expect(wrapper.findComponent(StateContainer).props().status).toBe('failed'); diff --git a/spec/frontend/vue_merge_request_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 fef5fee5f19..65d170cae8b 100644 --- a/spec/frontend/vue_merge_request_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 @@ -83,8 +83,6 @@ describe('MRWidgetAutoMergeEnabled', () => { afterEach(() => { window.gl = oldWindowGl; - wrapper.destroy(); - wrapper = null; }); describe('computed', () => { diff --git a/spec/frontend/vue_merge_request_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 826f708069c..9b043bda72d 100644 --- a/spec/frontend/vue_merge_request_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 @@ -18,10 +18,6 @@ describe('MRWidgetAutoMergeFailed', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { createComponent({ mr: { mergeError }, diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js index ac18ccf9e26..6c3b7f76fe6 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js @@ -9,11 +9,6 @@ describe('MRWidgetChecking', () => { wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders loading icon', () => { expect(wrapper.findComponent(StateContainer).exists()).toBe(true); expect(wrapper.findComponent(StateContainer).props().status).toBe('loading'); diff --git a/spec/frontend/vue_merge_request_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 5d2d1fdd6f1..e4febda1daa 100644 --- a/spec/frontend/vue_merge_request_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 @@ -36,10 +36,6 @@ describe('Commits message dropdown component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem); const findFirstDropdownElement = () => findDropdownElements().at(0); diff --git a/spec/frontend/vue_merge_request_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 a6d3a6286a7..b3843b066df 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js @@ -21,10 +21,6 @@ describe('Commits header component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count'); const findCommitToggle = () => wrapper.find('.commit-edit-toggle'); const findTargetBranchMessage = () => wrapper.find('.label-branch'); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js index 2ca9dc61745..7f0a171d712 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js @@ -50,10 +50,6 @@ describe('MRWidgetConflicts', () => { await nextTick(); } - afterEach(() => { - wrapper.destroy(); - }); - // There are two permissions we need to consider: // // 1. Is the user allowed to merge to the target branch? diff --git a/spec/frontend/vue_merge_request_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 833fa27d453..38e5422325a 100644 --- a/spec/frontend/vue_merge_request_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 @@ -28,10 +28,6 @@ describe('MRWidgetFailedToMerge', () => { jest.spyOn(window, 'clearInterval').mockImplementation(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('interval', () => { it('sets interval to refresh', () => { createComponent(); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js index a3aa563b516..e44e2834a0e 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js @@ -62,10 +62,6 @@ describe('MRWidgetMerged', () => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); }); - afterEach(() => { - wrapper.destroy(); - }); - const findButtonByText = (text) => wrapper.findAll('button').wrappers.find((w) => w.text() === text); const findRemoveSourceBranchButton = () => findButtonByText('Delete source branch'); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js index 5408f731b34..ca75ca11e5b 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js @@ -29,10 +29,6 @@ describe('MRWidgetMerging', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders information about merge request being merged', () => { const message = wrapper.findComponent(BoldText).props('message'); expect(message).toContain('Merging!'); diff --git a/spec/frontend/vue_merge_request_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 f29cf55f7ce..fca25b8bb94 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js @@ -15,10 +15,6 @@ function factory(sourceBranchRemoved) { } describe('MRWidgetMissingBranch', () => { - afterEach(() => { - wrapper.destroy(); - }); - it.each` sourceBranchRemoved | branchName ${true} | ${'source'} diff --git a/spec/frontend/vue_merge_request_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 42515c597c5..40b053282de 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js @@ -10,11 +10,6 @@ describe('MRWidgetNotAllowed', () => { wrapper = shallowMount(notAllowedComponent); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders success icon', () => { expect(wrapper.findComponent(StatusIcon).exists()).toBe(true); expect(wrapper.findComponent(StatusIcon).props().status).toBe('success'); diff --git a/spec/frontend/vue_merge_request_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 6de0c06c33d..c8fa1399dcb 100644 --- a/spec/frontend/vue_merge_request_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 @@ -1,28 +1,58 @@ -import Vue, { nextTick } from 'vue'; +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NothingToMerge from '~/vue_merge_request_widget/components/states/nothing_to_merge.vue'; describe('NothingToMerge', () => { - describe('template', () => { - const Component = Vue.extend(NothingToMerge); - const newBlobPath = '/foo'; - const vm = new Component({ - el: document.createElement('div'), + let wrapper; + const newBlobPath = '/foo'; + + const defaultProps = { + mr: { + newBlobPath, + }, + }; + + const createComponent = (props = defaultProps) => { + wrapper = shallowMountExtended(NothingToMerge, { propsData: { - mr: { newBlobPath }, + ...props, + }, + stubs: { + GlSprintf, }, }); + }; + + const findCreateButton = () => wrapper.findByTestId('createFileButton'); + const findNothingToMergeTextBody = () => wrapper.findByTestId('nothing-to-merge-body'); + + describe('With Blob link', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the component with the correct text and highlights', () => { + expect(wrapper.text()).toContain('This merge request contains no changes.'); + expect(findNothingToMergeTextBody().text()).toContain( + 'Use merge requests to propose changes to your project and discuss them with your team. To make changes, push a commit or edit this merge request to use a different branch.', + ); + }); + + it('shows the Create file button with the correct attributes', () => { + const createButton = findCreateButton(); + + expect(createButton.exists()).toBe(true); + expect(createButton.attributes('href')).toBe(newBlobPath); + }); + }); - it('should have correct elements', () => { - 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'); + describe('Without Blob link', () => { + beforeEach(() => { + createComponent({ mr: { newBlobPath: '' } }); }); - it('should not show new blob link if there is no link available', () => { - vm.mr.newBlobPath = null; - nextTick(() => { - expect(vm.$el.querySelector('[data-testid="createFileButton"]')).toEqual(null); - }); + it('does not show the Create file button', () => { + expect(findCreateButton().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/vue_merge_request_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 c0197b5e20a..d99106df0a2 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -10,11 +10,6 @@ describe('MRWidgetPipelineBlocked', () => { wrapper = shallowMount(PipelineBlockedComponent); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders error icon', () => { expect(wrapper.findComponent(StatusIcon).exists()).toBe(true); expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed'); diff --git a/spec/frontend/vue_merge_request_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 8bae2b62ed1..ea93463f3ab 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -19,11 +19,6 @@ describe('PipelineFailed', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render error status icon', () => { createComponent(); diff --git a/spec/frontend/vue_merge_request_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 aaa4591d67d..02b71ebf183 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -20,10 +20,6 @@ describe('ShaMismatch', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render warning message', () => { expect(wrapper.text()).toContain('Merge blocked: new changes were just added.'); }); diff --git a/spec/frontend/vue_merge_request_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 c839fa17fe5..97f8e695df9 100644 --- a/spec/frontend/vue_merge_request_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 @@ -14,10 +14,6 @@ describe('Squash before merge component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); describe('checkbox', () => { diff --git a/spec/frontend/vue_merge_request_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 c97b42f61ac..58b9f162815 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js @@ -21,10 +21,6 @@ describe('UnresolvedDiscussions', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('triggers the correct notes event when the jump to first unresolved discussion button is clicked', () => { jest.spyOn(notesEventHub, '$emit'); @@ -38,10 +34,6 @@ describe('UnresolvedDiscussions', () => { wrapper = createComponent({ path: TEST_HOST }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should have correct elements', () => { const text = removeBreakLine(wrapper.text()).trim(); expect(text).toContain('Merge blocked:'); diff --git a/spec/frontend/vue_merge_request_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..20d06a7aaee 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js @@ -15,10 +15,6 @@ function factory({ canMerge }) { } describe('New ready to merge state component', () => { - afterEach(() => { - wrapper.destroy(); - }); - it.each` canMerge ${true} diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js index e610ceb2122..43ce1769ff3 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import WorkInProgress, { MSG_SOMETHING_WENT_WRONG, MSG_MARK_READY, @@ -22,7 +22,7 @@ const TEST_MR_IID = '23'; const TEST_MR_TITLE = 'Test MR Title'; const TEST_PROJECT_PATH = 'lorem/ipsum'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/merge_request'); describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js index 366ea113162..adefce9060c 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js @@ -11,10 +11,6 @@ function factory(propsData = {}) { } describe('~/vue_merge_request_widget/components/widget/action_buttons.vue', () => { - afterEach(() => { - wrapper.destroy(); - }); - describe('tertiaryButtons', () => { it('renders buttons', () => { factory({ 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 index 973866176c2..5887670a58d 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js @@ -50,10 +50,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('on mount', () => { it('fetches collapsed', async () => { const fetchCollapsedData = jest diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js index 1bad5dacefa..785515ae846 100644 --- a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js @@ -25,10 +25,6 @@ describe('Deployment action button', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when passed only icon via props', () => { beforeEach(() => { factory({ diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js index 41df485b0de..1fdbbadf8b0 100644 --- a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { visitUrl } from '~/lib/utils/url_utility'; import { @@ -21,7 +21,7 @@ import { retryDetails, } from './deployment_mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); @@ -54,7 +54,6 @@ describe('DeploymentAction component', () => { }); afterEach(() => { - wrapper.destroy(); confirmAction.mockReset(); }); diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js index 948d7ebab5e..77dac4204db 100644 --- a/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js @@ -28,7 +28,6 @@ describe('~/vue_merge_request_widget/components/deployment/deployment_list.vue', afterEach(() => { wrapper?.destroy?.(); - wrapper = null; }); describe('with few deployments', () => { diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js index f310f7669a9..74122f47ad3 100644 --- a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js @@ -32,10 +32,6 @@ describe('Deployment component', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('always renders DeploymentInfo', () => { expect(wrapper.findComponent(DeploymentInfo).exists()).toBe(true); }); diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js index 8994fa522d0..7a151c26934 100644 --- a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js +++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js @@ -28,10 +28,6 @@ describe('Deployment View App button', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink); const findMrWigdetDeploymentDropdown = () => wrapper.findComponent(GlDropdown); const findMrWigdetDeploymentDropdownIcon = () => diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js index 548b68bc103..d2d622d0534 100644 --- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js @@ -73,7 +73,6 @@ describe('Test report extension', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js index 01049e54a7f..40158917f52 100644 --- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js @@ -39,7 +39,6 @@ describe('Accessibility extension', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js index 67b327217ef..4b7870842bd 100644 --- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js @@ -61,7 +61,6 @@ describe('Code Quality extension', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js index 13384e1efca..52a244107bd 100644 --- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js @@ -48,7 +48,6 @@ describe('Terraform extension', () => { }); afterEach(() => { - wrapper.destroy(); mock.restore(); }); diff --git a/spec/frontend/vue_merge_request_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 015d394312a..20f1796008a 100644 --- a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js +++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js @@ -15,11 +15,6 @@ describe('MRWidgetHowToMerge', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - beforeEach(() => { mountComponent(); }); diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js index f37276ad594..fad501ee7f5 100644 --- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js @@ -1,9 +1,10 @@ import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import * as Sentry from '@sentry/browser'; +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json'; import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -20,6 +21,7 @@ import { registerExtension, registeredExtensions, } from '~/vue_merge_request_widget/components/extensions'; +import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget/constants'; 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'; @@ -28,6 +30,7 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_ import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql'; import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql'; +import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql'; import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql'; import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; @@ -60,6 +63,8 @@ jest.mock('@sentry/browser', () => ({ Vue.use(VueApollo); describe('MrWidgetOptions', () => { + let stateQueryHandler; + let queryResponse; let wrapper; let mock; @@ -83,37 +88,41 @@ describe('MrWidgetOptions', () => { afterEach(() => { mock.restore(); + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy wrapper.destroy(); - gl.mrWidgetData = {}; - gon.features = {}; }); - const createComponent = (mrData = mockData, options = {}) => { - wrapper = mount(MrWidgetOptions, { + const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => { + const mounting = fullMount ? mount : shallowMount; + + queryResponse = { + data: { + project: { + ...getStateQueryResponse.data.project, + mergeRequest: { + ...getStateQueryResponse.data.project.mergeRequest, + mergeError: mrData.mergeError || null, + }, + }, + }, + }; + stateQueryHandler = jest.fn().mockResolvedValue(queryResponse); + wrapper = mounting(MrWidgetOptions, { propsData: { mrData: { ...mrData }, }, data() { - return { loading: false }; + return { + loading: false, + ...data, + }; }, ...options, apolloProvider: createMockApollo([ - [ - getStateQuery, - jest.fn().mockResolvedValue({ - data: { - project: { - ...getStateQueryResponse.data.project, - mergeRequest: { - ...getStateQueryResponse.data.project.mergeRequest, - mergeError: mrData.mergeError || null, - }, - }, - }, - }), - ], + [approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)], + [getStateQuery, stateQueryHandler], [readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)], [ userPermissionsQuery, @@ -351,18 +360,6 @@ describe('MrWidgetOptions', () => { }); }); - describe('initPolling', () => { - it('should call SmartInterval', () => { - wrapper.vm.initPolling(); - - expect(SmartInterval).toHaveBeenCalledWith( - expect.objectContaining({ - callback: wrapper.vm.checkStatus, - }), - ); - }); - }); - describe('initDeploymentsPolling', () => { it('should call SmartInterval', () => { wrapper.vm.initDeploymentsPolling(); @@ -529,23 +526,64 @@ describe('MrWidgetOptions', () => { }); }); - describe('resumePolling', () => { - it('should call stopTimer on pollingInterval', () => { - jest.spyOn(wrapper.vm.pollingInterval, 'resume').mockImplementation(() => {}); + describe('Apollo query', () => { + const interval = 5; + const data = 'foo'; + const mockCheckStatus = jest.fn().mockResolvedValue({ data }); + const mockSetGraphqlData = jest.fn(); + const mockSetData = jest.fn(); - wrapper.vm.resumePolling(); + beforeEach(() => { + wrapper.destroy(); + + return createComponent( + mockData, + {}, + { + pollInterval: interval, + startingPollInterval: interval, + mr: { + setData: mockSetData, + setGraphqlData: mockSetGraphqlData, + }, + service: { + checkStatus: mockCheckStatus, + }, + }, + false, + ); + }); - expect(wrapper.vm.pollingInterval.resume).toHaveBeenCalled(); + describe('normal polling behavior', () => { + it('responds to the GraphQL query finishing', () => { + expect(mockSetGraphqlData).toHaveBeenCalledWith(queryResponse.data.project); + expect(mockCheckStatus).toHaveBeenCalled(); + expect(mockSetData).toHaveBeenCalledWith(data, undefined); + expect(stateQueryHandler).toHaveBeenCalledTimes(1); + }); }); - }); - describe('stopPolling', () => { - it('should call stopTimer on pollingInterval', () => { - jest.spyOn(wrapper.vm.pollingInterval, 'stopTimer').mockImplementation(() => {}); + describe('external event control', () => { + describe('enablePolling', () => { + it('enables the Apollo query polling using the event hub', () => { + eventHub.$emit('EnablePolling'); + + expect(stateQueryHandler).toHaveBeenCalled(); + jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF); + expect(stateQueryHandler).toHaveBeenCalledTimes(2); + }); + }); + + describe('disablePolling', () => { + it('disables the Apollo query polling using the event hub', () => { + expect(stateQueryHandler).toHaveBeenCalledTimes(1); - wrapper.vm.stopPolling(); + eventHub.$emit('DisablePolling'); + jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF); - expect(wrapper.vm.pollingInterval.stopTimer).toHaveBeenCalled(); + expect(stateQueryHandler).toHaveBeenCalledTimes(1); // no additional polling after a real interval timeout + }); + }); }); }); }); @@ -890,11 +928,7 @@ describe('MrWidgetOptions', () => { }); describe('mock extension', () => { - let pollRequest; - beforeEach(() => { - pollRequest = jest.spyOn(Poll.prototype, 'makeRequest'); - registerExtension(workingExtension()); createComponent(); @@ -945,10 +979,6 @@ describe('MrWidgetOptions', () => { expect(collapsedSection.findComponent(GlButton).exists()).toBe(true); expect(collapsedSection.findComponent(GlButton).text()).toBe('Full report'); }); - - it('extension polling is not called if enablePolling flag is not passed', () => { - expect(pollRequest).toHaveBeenCalledTimes(0); - }); }); describe('expansion', () => { @@ -1235,10 +1265,6 @@ describe('MrWidgetOptions', () => { }); describe('widget container', () => { - afterEach(() => { - delete window.gon.features.refactorSecurityExtension; - }); - it('should not be displayed when the refactor_security_extension feature flag is turned off', () => { createComponent(); expect(findWidgetContainer().exists()).toBe(false); diff --git a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js index 88d9d0b4cff..a6288b9c725 100644 --- a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js @@ -20,7 +20,7 @@ describe('getStateKey', () => { }; const bound = getStateKey.bind(context); - expect(bound()).toEqual(null); + expect(bound()).toEqual('checking'); context.detailedMergeStatus = 'MERGEABLE'; diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js index 12c5c190e26..217103ab25c 100644 --- a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js @@ -32,10 +32,6 @@ describe('Alert Details Sidebar To Do', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]'); describe('updating the alert to do', () => { diff --git a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap deleted file mode 100644 index ca9d4488870..00000000000 --- a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`File row header component adds multiple ellipsises after 40 characters 1`] = ` -<div - class="file-row-header bg-white sticky-top p-2 js-file-row-header" - title="app/assets/javascripts/merge_requests/widget/diffs/notes" -> - <gl-truncate-stub - class="bold" - position="middle" - text="app/assets/javascripts/merge_requests/widget/diffs/notes" - /> -</div> -`; - -exports[`File row header component renders file path 1`] = ` -<div - class="file-row-header bg-white sticky-top p-2 js-file-row-header" - title="app/assets" -> - <gl-truncate-stub - class="bold" - position="middle" - text="app/assets" - /> -</div> -`; - -exports[`File row header component trucates path after 40 characters 1`] = ` -<div - class="file-row-header bg-white sticky-top p-2 js-file-row-header" - title="app/assets/javascripts/merge_requests" -> - <gl-truncate-stub - class="bold" - position="middle" - text="app/assets/javascripts/merge_requests" - /> -</div> -`; diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js index f3fb840b270..8c2f2b52f8e 100644 --- a/spec/frontend/vue_shared/components/actions_button_spec.js +++ b/spec/frontend/vue_shared/components/actions_button_spec.js @@ -34,10 +34,6 @@ describe('Actions button component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - const findButton = () => wrapper.findComponent(GlButton); const findTooltip = () => wrapper.findComponent(GlTooltip); const findDropdown = () => wrapper.findComponent(GlDropdown); 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 8a9ee4699bd..8e7a10c4d77 100644 --- a/spec/frontend/vue_shared/components/alert_details_table_spec.js +++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js @@ -41,11 +41,6 @@ describe('AlertDetails', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findTableComponent = () => wrapper.findComponent(GlTable); const findTableKeys = () => findTableComponent().findAll('tbody td:first-child'); const findTableFieldValueByKey = (fieldKey) => diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js index c7f9d8fd8d5..da5516f8db1 100644 --- a/spec/frontend/vue_shared/components/awards_list_spec.js +++ b/spec/frontend/vue_shared/components/awards_list_spec.js @@ -64,16 +64,7 @@ const REACTION_CONTROL_CLASSES = [ describe('vue_shared/components/awards_list', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const createComponent = (props = {}) => { - if (wrapper) { - throw new Error('There should only be one wrapper created per test'); - } - wrapper = mount(AwardsList, { propsData: props }); }; const matchingEmojiTag = (name) => expect.stringMatching(`gl-emoji data-name="${name}"`); @@ -98,7 +89,6 @@ describe('vue_shared/components/awards_list', () => { addButtonClass: TEST_ADD_BUTTON_CLASS, }); }); - it('shows awards in correct order', () => { expect(findAwardsData()).toEqual([ { 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 ce7fd40937f..6acd1f51a86 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 @@ -24,10 +24,6 @@ describe('Blob Rich Viewer component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the passed content without transformations', () => { expect(wrapper.html()).toContain(content); }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js index 4b44311b253..a480e0869e8 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js @@ -23,10 +23,6 @@ describe('Blob Simple Viewer component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('does not fail if content is empty', () => { const spy = jest.spyOn(window.console, 'error'); createComponent(''); 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 ea708b6f3fe..d1b1e58f5d7 100644 --- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -21,10 +21,6 @@ describe('Changed file icon', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findIcon = () => wrapper.findComponent(GlIcon); const findIconName = () => findIcon().props('name'); const findIconClasses = () => findIcon().classes(); diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js index 6932a812287..2a40511affb 100644 --- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js +++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js @@ -10,8 +10,6 @@ describe('vue_shared/components/chronic_duration_input', () => { let hiddenElement; afterEach(() => { - wrapper.destroy(); - wrapper = null; textElement = null; hiddenElement = null; }); @@ -22,10 +20,6 @@ describe('vue_shared/components/chronic_duration_input', () => { }; const createComponent = (props = {}) => { - if (wrapper) { - throw new Error('There should only be one wrapper created per test'); - } - wrapper = mount(ChronicDurationInput, { propsData: props }); findComponents(); }; diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js index 4f24ec2d015..afb509b9fe6 100644 --- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js +++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js @@ -82,10 +82,6 @@ describe('CI Badge Link Component', () => { wrapper = shallowMount(CiBadgeLink, { propsData }); }; - afterEach(() => { - wrapper.destroy(); - }); - it.each(Object.keys(statuses))('should render badge for status: %s', (status) => { createComponent({ status: statuses[status] }); diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js index 2064bee9673..31d63654168 100644 --- a/spec/frontend/vue_shared/components/ci_icon_spec.js +++ b/spec/frontend/vue_shared/components/ci_icon_spec.js @@ -7,11 +7,6 @@ describe('CI Icon component', () => { const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render a span element with an svg', () => { wrapper = shallowMount(ciIcon, { propsData: { diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js index b18b00e70bb..08a9c2a42d8 100644 --- a/spec/frontend/vue_shared/components/clipboard_button_spec.js +++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js @@ -59,11 +59,6 @@ describe('clipboard button', () => { expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1'); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('without gfm', () => { beforeEach(() => { createWrapper({ diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js index 31c08260dd0..584e29d94c4 100644 --- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js @@ -21,11 +21,6 @@ describe('Clone Dropdown Button', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('rendering', () => { it('matches the snapshot', () => { createComponent(); diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js index 181692e61b5..25283eb1211 100644 --- a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js +++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js @@ -11,10 +11,6 @@ describe('Code Block Highlighted', () => { wrapper = shallowMount(CodeBlock, { propsData }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders highlighted code if language is supported', async () => { createComponent({ code, language: 'javascript' }); diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js index 9a4dbcc47ff..0fdfb96cb23 100644 --- a/spec/frontend/vue_shared/components/code_block_spec.js +++ b/spec/frontend/vue_shared/components/code_block_spec.js @@ -13,10 +13,6 @@ describe('Code Block', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('overwrites the default slot', () => { createComponent({}, { default: 'DEFAULT SLOT' }); 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 060048c4bbd..a839af3b709 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 @@ -34,10 +34,6 @@ describe('ColorPicker', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('label', () => { it('hides the label if the label is not passed', () => { createComponent(shallowMount); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js index fe614f03119..700556edfd5 100644 --- a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js +++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js @@ -20,10 +20,6 @@ describe('ColorItem', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the correct title', () => { expect(wrapper.text()).toBe(propsData.title); }); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js index 5b0772f6e34..61a9ab3225e 100644 --- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js +++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue'; import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue'; @@ -13,7 +13,7 @@ import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants'; import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); @@ -60,10 +60,6 @@ describe('LabelsSelectRoot', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { const defaultClasses = ['labels-select-wrapper', 'gl-relative']; @@ -145,7 +141,7 @@ describe('LabelsSelectRoot', () => { await waitForPromises(); }); - it('creates flash with error message', () => { + it('creates alert with error message', () => { expect(createAlert).toHaveBeenCalledWith({ captureError: true, message: 'Error fetching epic color.', diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js index 303824c77b3..914dfdbaab0 100644 --- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js @@ -22,10 +22,6 @@ describe('DropdownContentsColorView', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const findColors = () => wrapper.findAllComponents(ColorItem); const findColorList = () => wrapper.findComponent(GlDropdownForm); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js index ee4d3a2630a..c07faab20d0 100644 --- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js @@ -28,10 +28,6 @@ describe('DropdownContent', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findColorView = () => wrapper.findComponent(DropdownContentsColorView); const findDropdownHeader = () => wrapper.findComponent(DropdownHeader); const findDropdown = () => wrapper.findComponent(GlDropdown); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js index d203d78477f..6c8aabe1c7f 100644 --- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js @@ -15,10 +15,6 @@ describe('DropdownHeader', () => { const findButton = () => wrapper.findComponent(GlButton); - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { createComponent(); }); diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js index 5bbdb136353..825f37c97e0 100644 --- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js @@ -22,10 +22,6 @@ describe('DropdownValue', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when there is a color set', () => { it('renders the color', () => { expect(findColorItems()).toHaveLength(2); diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js index 1893e127f6f..62a2738d8df 100644 --- a/spec/frontend/vue_shared/components/commit_spec.js +++ b/spec/frontend/vue_shared/components/commit_spec.js @@ -24,10 +24,6 @@ describe('Commit component', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should render a fork icon if it does not represent a tag', () => { createComponent({ tag: false, diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js index 3f7ec156c19..92cd7597637 100644 --- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js +++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js @@ -1,14 +1,11 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants'; +import { TYPE_ISSUE, TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; -const createComponent = ({ - workspaceType = WorkspaceType.project, - issuableType = TYPE_ISSUE, -} = {}) => +const createComponent = ({ workspaceType = WORKSPACE_PROJECT, issuableType = TYPE_ISSUE } = {}) => shallowMount(ConfidentialityBadge, { propsData: { workspaceType, @@ -23,14 +20,10 @@ describe('ConfidentialityBadge', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it.each` - workspaceType | issuableType | expectedTooltip - ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'} - ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'} + workspaceType | issuableType | expectedTooltip + ${WORKSPACE_PROJECT} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'} + ${WORKSPACE_GROUP} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'} `( 'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType', ({ workspaceType, issuableType, expectedTooltip }) => { diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js index a660643d74f..d7f94c00d09 100644 --- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js @@ -24,7 +24,7 @@ describe('Confirm Danger Modal', () => { const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message'); const findPrimaryAction = () => findModal().props('actionPrimary'); const findCancelAction = () => findModal().props('actionCancel'); - const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr]; + const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr]; const createComponent = ({ provide = {} } = {}) => shallowMountExtended(ConfirmDangerModal, { @@ -42,10 +42,6 @@ describe('Confirm Danger Modal', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the default warning message', () => { expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING); }); diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js index a179afccae0..379b5cde4d5 100644 --- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js @@ -32,10 +32,6 @@ describe('Confirm Danger Modal', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the button', () => { expect(wrapper.html()).toContain(buttonText); }); diff --git a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js index 1cde92cf522..fbfef5cbe46 100644 --- a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js @@ -21,10 +21,6 @@ describe('vue_shared/components/confirm_fork_modal', () => { }, }); - afterEach(() => { - wrapper.destroy(); - }); - describe('visible = false', () => { beforeEach(() => { wrapper = createComponent(); diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js index c1e682a1aae..283ef52cee7 100644 --- a/spec/frontend/vue_shared/components/confirm_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js @@ -47,10 +47,6 @@ describe('vue_shared/components/confirm_modal', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findModal = () => wrapper.findComponent(GlModalStub); const findForm = () => wrapper.find('form'); const findFormData = () => diff --git a/spec/frontend/vue_shared/components/content_transition_spec.js b/spec/frontend/vue_shared/components/content_transition_spec.js index 8bb6d31cce7..5f2b1f096f3 100644 --- a/spec/frontend/vue_shared/components/content_transition_spec.js +++ b/spec/frontend/vue_shared/components/content_transition_spec.js @@ -13,11 +13,6 @@ const TEST_SLOTS = [ describe('~/vue_shared/components/content_transition.vue', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const createComponent = (props = {}, slots = {}) => { wrapper = shallowMount(ContentTransition, { propsData: { diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js index c1495e8264a..a3e5f187f9b 100644 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js +++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js @@ -18,10 +18,6 @@ describe('DateTimePickerInput', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders label above the input', () => { createComponent({ label: inputLabel, diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js index aa41df438d2..5620b569409 100644 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js @@ -26,10 +26,6 @@ describe('DateTimePicker', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders dropdown toggle button with selected text', async () => { createComponent(); await nextTick(); diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js index 79001b9282f..dde2540e121 100644 --- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js +++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js @@ -17,10 +17,6 @@ describe('Deploy Board Instance', () => { }); describe('as a non-canary deployment', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('should render a div with the correct css status and tooltip data', () => { wrapper = createComponent({ tooltipText: 'This is a pod', @@ -43,10 +39,6 @@ describe('Deploy Board Instance', () => { }); describe('as a canary deployment', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('should render a div with canary class when stable prop is provided as false', async () => { wrapper = createComponent({ stable: false, @@ -58,10 +50,6 @@ describe('Deploy Board Instance', () => { }); describe('as a legend item', () => { - afterEach(() => { - wrapper.destroy(); - }); - it('should not have a tooltip', () => { wrapper = createComponent(); diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js index 353d493add9..ca9c2b7d381 100644 --- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js +++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js @@ -16,10 +16,6 @@ describe('Design note pin component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('should match the snapshot of note without index', () => { createComponent(); expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js index 99c973bdd26..930d2fc8cfe 100644 --- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js @@ -110,10 +110,6 @@ describe('Diff Stats Dropdown', () => { createComponent({ changed, added, deleted }); }); - afterEach(() => { - wrapper.destroy(); - }); - it(`dropdown header should be '${expectedDropdownHeader}'`, () => { expect(findChanged().props('text')).toBe(expectedDropdownHeader); }); diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js index 6e0717c29d7..694c69fbe9f 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -18,10 +18,6 @@ describe('DiffViewer', () => { wrapper = mount(DiffViewer, { propsData }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders image diff', () => { window.gon = { relative_url_root: '', diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js index 16f924b44d8..7863ef45817 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js @@ -42,10 +42,6 @@ describe('ImageDiffViewer component', () => { triggerEvent('mouseup', doc.body); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders image diff for replaced', () => { createComponent(allProps); const metaInfoElements = wrapper.findAll('.image-info'); diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js index c4358f0d9cb..661db19ff0e 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js @@ -13,10 +13,6 @@ describe('Diff viewer mode changed component', () => { }); }); - afterEach(() => { - vm.destroy(); - }); - it('renders aMode & bMode', () => { expect(vm.text()).toContain('File mode changed from 123 to 321'); }); diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js index 8b1189f25d5..53e7d9fc7fc 100644 --- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js @@ -16,10 +16,6 @@ describe('vue_shared/components/dismissible_alert', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findAlert = () => wrapper.findComponent(GlAlert); describe('default', () => { diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js index 7d8581e11e9..6d179434d1d 100644 --- a/spec/frontend/vue_shared/components/dismissible_container_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js @@ -11,10 +11,6 @@ describe('DismissibleContainer', () => { featureId: 'some-feature-id', }; - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { const findBtn = () => wrapper.find('[data-testid="close"]'); let mockAxios; diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js index 4b32fbffebe..b184ec4ac54 100644 --- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js @@ -23,11 +23,6 @@ describe('Dismissible Feedback Alert', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const createFullComponent = () => createComponent({ mountFn: mount }); const findAlert = () => wrapper.findComponent(GlAlert); diff --git a/spec/frontend/vue_shared/components/dom_element_listener_spec.js b/spec/frontend/vue_shared/components/dom_element_listener_spec.js index a848c34b7ce..d31e9b867e4 100644 --- a/spec/frontend/vue_shared/components/dom_element_listener_spec.js +++ b/spec/frontend/vue_shared/components/dom_element_listener_spec.js @@ -42,10 +42,6 @@ describe('~/vue_shared/components/dom_element_listener.vue', () => { }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); 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 e34ed31b4bf..82130500458 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js @@ -11,10 +11,6 @@ describe('DropdownButton component', () => { wrapper = mount(DropdownButton, { propsData: props, slots }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('dropdownToggleText', () => { it('returns default toggle text', () => { 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 dd3e55c82bb..4dfee20764c 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -31,10 +31,6 @@ describe('DropdownWidget component', () => { }, }); - // We need to mock out `showDropdown` which - // invokes `show` method of BDropdown used inside GlDropdown. - // Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679 - jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); jest.spyOn(findDropdown().vm, 'hide').mockImplementation(); }; @@ -42,11 +38,6 @@ describe('DropdownWidget component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('passes default selectText prop to dropdown', () => { expect(findDropdown().props('text')).toBe('Select'); }); diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js index 119d6448507..ef42c17984a 100644 --- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js +++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js @@ -39,10 +39,6 @@ describe('DropdownKeyboardNavigation', () => { }, }; - afterEach(() => { - wrapper.destroy(); - }); - describe('onInit', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js index eef8b452f5f..217e795bc64 100644 --- a/spec/frontend/vue_shared/components/ensure_data_spec.js +++ b/spec/frontend/vue_shared/components/ensure_data_spec.js @@ -59,7 +59,6 @@ describe('EnsureData', () => { }); afterEach(() => { - wrapper.destroy(); Sentry.captureException.mockClear(); }); diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js index 57dce032d30..32ce2155494 100644 --- a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js +++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js @@ -63,11 +63,8 @@ describe('ProjectSelect', () => { }; const openListbox = () => findListbox().vm.$emit('shown'); - beforeAll(() => { - gon.api_version = apiVersion; - }); - beforeEach(() => { + gon.api_version = apiVersion; mock = new MockAdapter(axios); }); diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js index 170c947e520..ad2a57d90eb 100644 --- a/spec/frontend/vue_shared/components/expand_button_spec.js +++ b/spec/frontend/vue_shared/components/expand_button_spec.js @@ -27,10 +27,6 @@ describe('Expand button', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the prepended collapse button', () => { expect(expanderPrependEl().isVisible()).toBe(true); expect(expanderAppendEl().isVisible()).toBe(false); diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js index f0998b1b5c6..dce6c85b5b3 100644 --- a/spec/frontend/vue_shared/components/file_finder/item_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js @@ -22,10 +22,6 @@ describe('File finder item spec', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders file name & path', () => { createComponent(); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js index 0fcc0678c13..d95773f2218 100644 --- a/spec/frontend/vue_shared/components/file_icon_spec.js +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -16,10 +16,6 @@ describe('File Icon component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should render a span element and an icon', () => { createComponent({ fileName: 'test.js', diff --git a/spec/frontend/vue_shared/components/file_row_header_spec.js b/spec/frontend/vue_shared/components/file_row_header_spec.js index 80f4c275dcc..885a80f73b5 100644 --- a/spec/frontend/vue_shared/components/file_row_header_spec.js +++ b/spec/frontend/vue_shared/components/file_row_header_spec.js @@ -1,36 +1,24 @@ import { shallowMount } from '@vue/test-utils'; +import { GlTruncate } from '@gitlab/ui'; import FileRowHeader from '~/vue_shared/components/file_row_header.vue'; describe('File row header component', () => { - let vm; + let wrapper; function createComponent(path) { - vm = shallowMount(FileRowHeader, { + wrapper = shallowMount(FileRowHeader, { propsData: { path, }, }); } - afterEach(() => { - vm.destroy(); - }); - it('renders file path', () => { - createComponent('app/assets'); - - expect(vm.element).toMatchSnapshot(); - }); - - it('trucates path after 40 characters', () => { - createComponent('app/assets/javascripts/merge_requests'); - - expect(vm.element).toMatchSnapshot(); - }); - - it('adds multiple ellipsises after 40 characters', () => { - createComponent('app/assets/javascripts/merge_requests/widget/diffs/notes'); + const path = 'app/assets'; + createComponent(path); - expect(vm.element).toMatchSnapshot(); + const truncate = wrapper.findComponent(GlTruncate); + expect(truncate.exists()).toBe(true); + expect(truncate.props('text')).toBe(path); }); }); diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js index b70d4565f56..976866af27c 100644 --- a/spec/frontend/vue_shared/components/file_row_spec.js +++ b/spec/frontend/vue_shared/components/file_row_spec.js @@ -6,6 +6,9 @@ import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileHeader from '~/vue_shared/components/file_row_header.vue'; +const scrollIntoViewMock = jest.fn(); +HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + describe('File row component', () => { let wrapper; @@ -18,10 +21,6 @@ describe('File row component', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders name', () => { const fileName = 't4'; createComponent({ @@ -72,11 +71,10 @@ describe('File row component', () => { }, level: 0, }); - jest.spyOn(wrapper.vm, '$emit'); wrapper.element.click(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', fileName); + expect(wrapper.emitted('toggleTreeOpen')[0][0]).toEqual(fileName); }); it('calls scrollIntoView if made active', () => { @@ -89,14 +87,12 @@ describe('File row component', () => { level: 0, }); - jest.spyOn(wrapper.vm, 'scrollIntoView'); - wrapper.setProps({ file: { ...wrapper.props('file'), active: true }, }); return nextTick().then(() => { - expect(wrapper.vm.scrollIntoView).toHaveBeenCalled(); + expect(scrollIntoViewMock).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/vue_shared/components/file_tree_spec.js b/spec/frontend/vue_shared/components/file_tree_spec.js index e8818e09dc0..9d2fa369910 100644 --- a/spec/frontend/vue_shared/components/file_tree_spec.js +++ b/spec/frontend/vue_shared/components/file_tree_spec.js @@ -33,10 +33,6 @@ describe('File Tree component', () => { ...pick(x.attributes(), Object.keys(TEST_EXTA_ARGS)), })); - afterEach(() => { - wrapper.destroy(); - }); - describe('file row component', () => { beforeEach(() => { createComponent({ file: {} }); 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 b0e393bbf5e..123714353e2 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 @@ -82,10 +82,6 @@ describe('FilteredSearchBarRoot', () => { wrapper = createComponent({ sortOptions: mockSortOptions }); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('data', () => { it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => { expect(wrapper.vm.filterValue).toEqual([]); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js index 63c22aff3d5..dd0ec65c871 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data'; import Api from '~/api'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status'; import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions'; import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types'; @@ -15,7 +15,7 @@ const labelsEndpoint = 'fake_labels_endpoint'; const groupEndpoint = 'fake_group_endpoint'; const projectEndpoint = 'fake_project_endpoint'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Filters actions', () => { let state; @@ -165,16 +165,10 @@ describe('Filters actions', () => { }); describe('fetchAuthors', () => { - let restoreVersion; beforeEach(() => { - restoreVersion = gon.api_version; gon.api_version = 'v1'; }); - afterEach(() => { - gon.api_version = restoreVersion; - }); - describe('success', () => { beforeEach(() => { mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers); @@ -305,17 +299,11 @@ describe('Filters actions', () => { describe('fetchAssignees', () => { describe('success', () => { - let restoreVersion; beforeEach(() => { mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers); - restoreVersion = gon.api_version; gon.api_version = 'v1'; }); - afterEach(() => { - gon.api_version = restoreVersion; - }); - it('dispatches RECEIVE_ASSIGNEES_SUCCESS with received data and groupEndpoint set', () => { return testAction( actions.fetchAssignees, @@ -350,17 +338,11 @@ describe('Filters actions', () => { }); describe('error', () => { - let restoreVersion; beforeEach(() => { mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE); - restoreVersion = gon.api_version; gon.api_version = 'v1'; }); - afterEach(() => { - gon.api_version = restoreVersion; - }); - it('dispatches RECEIVE_ASSIGNEES_ERROR and groupEndpoint set', () => { return testAction( actions.fetchAssignees, diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index 164235e4bb9..9941abbfaea 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -120,10 +120,6 @@ describe('BaseToken', () => { const getMockSuggestionListSuggestions = () => JSON.parse(findMockSuggestionList().attributes('data-suggestions')); - afterEach(() => { - wrapper.destroy(); - }); - describe('data', () => { it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => { wrapper = createComponent(); 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 311d5a13280..a6bb32736db 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 @@ -9,14 +9,15 @@ import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { mockBranches, mockBranchToken } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const defaultStubs = { Portal: true, GlFilteredSearchSuggestionList: { @@ -54,58 +55,83 @@ describe('BranchToken', () => { let mock; let wrapper; + const findBaseToken = () => wrapper.findComponent(BaseToken); + const triggerFetchBranches = (searchTerm = null) => { + findBaseToken().vm.$emit('fetch-suggestions', searchTerm); + return waitForPromises(); + }; + beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('methods', () => { - beforeEach(() => { - wrapper = createComponent(); - }); - describe('fetchBranches', () => { - it('calls `config.fetchBranches` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchBranches'); - - wrapper.vm.fetchBranches('foo'); + it('sets loading state', async () => { + wrapper = createComponent({ + config: { + fetchBranches: jest.fn().mockResolvedValue(new Promise(() => {})), + }, + }); + await nextTick(); - expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo'); + expect(findBaseToken().props('suggestionsLoading')).toBe(true); }); - it('sets response to `branches` when request is succesful', () => { - jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches }); + describe('when request is successful', () => { + beforeEach(() => { + wrapper = createComponent({ + config: { + fetchBranches: jest.fn().mockResolvedValue({ data: mockBranches }), + }, + }); + }); + + it('calls `config.fetchBranches` with provided searchTerm param', async () => { + const searchTerm = 'foo'; + await triggerFetchBranches(searchTerm); - wrapper.vm.fetchBranches('foo'); + expect(findBaseToken().props('config').fetchBranches).toHaveBeenCalledWith(searchTerm); + }); + + it('sets response to `branches`', async () => { + await triggerFetchBranches(); - return waitForPromises().then(() => { - expect(wrapper.vm.branches).toEqual(mockBranches); + expect(findBaseToken().props('suggestions')).toEqual(mockBranches); + }); + + it('sets `loading` to false when request completes', async () => { + await triggerFetchBranches(); + + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); - it('calls `createAlert` with flash error message when request fails', () => { - jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); + describe('when request fails', () => { + beforeEach(() => { + wrapper = createComponent({ + config: { + fetchBranches: jest.fn().mockRejectedValue({}), + }, + }); + }); - wrapper.vm.fetchBranches('foo'); + it('calls `createAlert` with alert error message when request fails', async () => { + await triggerFetchBranches(); - return waitForPromises().then(() => { expect(createAlert).toHaveBeenCalledWith({ message: 'There was a problem fetching branches.', }); }); - }); - - it('sets `loading` to false when request completes', () => { - jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); - wrapper.vm.fetchBranches('foo'); + it('sets `loading` to false when request completes', async () => { + await triggerFetchBranches(); - return waitForPromises().then(() => { - expect(wrapper.vm.loading).toBe(false); + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); }); @@ -120,16 +146,13 @@ describe('BranchToken', () => { await nextTick(); } - beforeEach(async () => { - wrapper = createComponent({ value: { data: mockBranches[0].name } }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - branches: mockBranches, + beforeEach(() => { + wrapper = createComponent({ + value: { data: mockBranches[0].name }, + config: { + initialBranches: mockBranches, + }, }); - - await nextTick(); }); it('renders gl-filtered-search-token component', () => { 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 7be7035a0f2..ce134f7d24e 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 @@ -8,7 +8,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -22,7 +22,7 @@ import { mockProjectCrmContactsQueryResponse, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const defaultStubs = { Portal: true, @@ -79,7 +79,6 @@ describe('CrmContactToken', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -159,7 +158,7 @@ describe('CrmContactToken', () => { }); }); - it('calls `createAlert` with flash error message when request fails', async () => { + it('calls `createAlert` with alert error message when request fails', async () => { mountComponent(); jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({}); 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 ecd3e8a04f1..8526631c63d 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 @@ -8,7 +8,7 @@ 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -22,7 +22,7 @@ import { mockProjectCrmOrganizationsQueryResponse, } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const defaultStubs = { Portal: true, @@ -78,7 +78,6 @@ describe('CrmOrganizationToken', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -158,7 +157,7 @@ describe('CrmOrganizationToken', () => { }); }); - it('calls `createAlert` with flash error message when request fails', async () => { + it('calls `createAlert` with alert error message when request fails', async () => { mountComponent(); jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({}); 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 773df01ada7..4e00b6837a3 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 @@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { @@ -17,10 +17,11 @@ import { OPTIONS_NONE_ANY, } from '~/vue_shared/components/filtered_search_bar/constants'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { mockReactionEmojiToken, mockEmojis } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const GlEmoji = { template: '<img/>' }; const defaultStubs = { Portal: true, @@ -60,58 +61,72 @@ describe('EmojiToken', () => { let mock; let wrapper; + const findBaseToken = () => wrapper.findComponent(BaseToken); + const triggerFetchEmojis = (searchTerm = null) => { + findBaseToken().vm.$emit('fetch-suggestions', searchTerm); + return waitForPromises(); + }; + beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('methods', () => { - beforeEach(() => { - wrapper = createComponent(); - }); - describe('fetchEmojis', () => { - it('calls `config.fetchEmojis` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis'); - - wrapper.vm.fetchEmojis('foo'); + it('sets loading state', async () => { + wrapper = createComponent({ + config: { + fetchEmojis: jest.fn().mockResolvedValue(new Promise(() => {})), + }, + }); + await nextTick(); - expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo'); + expect(findBaseToken().props('suggestionsLoading')).toBe(true); }); - it('sets response to `emojis` when request is successful', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis); + describe('when request is successful', () => { + const searchTerm = 'foo'; - wrapper.vm.fetchEmojis('foo'); + beforeEach(async () => { + wrapper = createComponent({ + config: { + fetchEmojis: jest.fn().mockResolvedValue({ data: mockEmojis }), + }, + }); + return triggerFetchEmojis(searchTerm); + }); - return waitForPromises().then(() => { - expect(wrapper.vm.emojis).toEqual(mockEmojis); + it('calls `config.fetchEmojis` with provided searchTerm param', () => { + expect(findBaseToken().props('config').fetchEmojis).toHaveBeenCalledWith(searchTerm); }); - }); - it('calls `createAlert` with flash error message when request fails', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); + it('sets response to `emojis`', () => { + expect(findBaseToken().props('suggestions')).toEqual(mockEmojis); + }); + }); - wrapper.vm.fetchEmojis('foo'); + describe('when request fails', () => { + beforeEach(() => { + wrapper = createComponent({ + config: { + fetchEmojis: jest.fn().mockRejectedValue({}), + }, + }); + return triggerFetchEmojis(); + }); - return waitForPromises().then(() => { + it('calls `createAlert` with alert error message', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'There was a problem fetching emojis.', }); }); - }); - - it('sets `loading` to false when request completes', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); - - wrapper.vm.fetchEmojis('foo'); - return waitForPromises().then(() => { - expect(wrapper.vm.loading).toBe(false); + it('sets `loading` to false when request completes', () => { + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); }); @@ -123,15 +138,10 @@ describe('EmojiToken', () => { beforeEach(async () => { wrapper = createComponent({ value: { data: `"${mockEmojis[0].name}"` }, + config: { + initialEmojis: mockEmojis, + }, }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - emojis: mockEmojis, - }); - - await nextTick(); }); it('renders gl-filtered-search-token component', () => { 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 9d96123c17f..b9275409125 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 @@ -11,7 +11,7 @@ import { mockRegularLabel, mockLabels, } from 'jest/sidebar/components/labels/labels_select_vue/mock_data'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -20,7 +20,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label import { mockLabelToken } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const defaultStubs = { Portal: true, BaseToken, @@ -60,103 +60,125 @@ function createComponent(options = {}) { describe('LabelToken', () => { let mock; let wrapper; + const defaultLabels = OPTIONS_NONE_ANY; beforeEach(() => { mock = new MockAdapter(axios); }); + const findBaseToken = () => wrapper.findComponent(BaseToken); + const findSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion); + const findTokenSegments = () => wrapper.findAllComponents(GlFilteredSearchTokenSegment); + const triggerFetchLabels = (searchTerm = null) => { + findBaseToken().vm.$emit('fetch-suggestions', searchTerm); + return waitForPromises(); + }; + afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('methods', () => { - beforeEach(() => { - wrapper = createComponent(); - }); - describe('getActiveLabel', () => { it('returns label object from labels array based on provided `currentValue` param', () => { - expect(wrapper.vm.getActiveLabel(mockLabels, 'Foo Label')).toEqual(mockRegularLabel); + wrapper = createComponent(); + + expect(findBaseToken().props('getActiveTokenValue')(mockLabels, 'Foo Label')).toEqual( + mockRegularLabel, + ); }); }); describe('getLabelName', () => { - it('returns value of `name` or `title` property present in provided label param', () => { - let mockLabel = { - title: 'foo', - }; + it('returns value of `name` or `title` property present in provided label param', async () => { + const customMockLabels = [ + { title: 'Title with no name label' }, + { name: 'Name Label', title: 'Title with name label' }, + ]; + + wrapper = createComponent({ + active: true, + config: { + ...mockLabelToken, + fetchLabels: jest.fn().mockResolvedValue({ data: customMockLabels }), + }, + stubs: { Portal: true }, + }); - expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.title); + await waitForPromises(); - mockLabel = { - name: 'foo', - }; + const suggestions = findSuggestions(); + const indexWithTitle = defaultLabels.length; + const indexWithName = defaultLabels.length + 1; - expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.name); + expect(suggestions.at(indexWithTitle).text()).toBe(customMockLabels[0].title); + expect(suggestions.at(indexWithName).text()).toBe(customMockLabels[1].name); }); }); describe('fetchLabels', () => { - it('calls `config.fetchLabels` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchLabels'); - - wrapper.vm.fetchLabels('foo'); - - expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo'); - }); + describe('when request is successful', () => { + const searchTerm = 'foo'; + + beforeEach(async () => { + wrapper = createComponent({ + config: { + fetchLabels: jest.fn().mockResolvedValue({ data: mockLabels }), + }, + }); + await triggerFetchLabels(searchTerm); + }); - it('sets response to `labels` when request is succesful', () => { - jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels); + it('calls `config.fetchLabels` with provided searchTerm param', () => { + expect(findBaseToken().props('config').fetchLabels).toHaveBeenCalledWith(searchTerm); + }); - wrapper.vm.fetchLabels('foo'); + it('sets response to `labels`', () => { + expect(findBaseToken().props('suggestions')).toEqual(mockLabels); + }); - return waitForPromises().then(() => { - expect(wrapper.vm.labels).toEqual(mockLabels); + it('sets `loading` to false when request completes', () => { + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); - it('calls `createAlert` with flash error message when request fails', () => { - jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({}); - - wrapper.vm.fetchLabels('foo'); + describe('when request fails', () => { + beforeEach(async () => { + wrapper = createComponent({ + config: { + fetchLabels: jest.fn().mockRejectedValue({}), + }, + }); + await triggerFetchLabels(); + }); - return waitForPromises().then(() => { + it('calls `createAlert` with alert error message', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'There was a problem fetching labels.', }); }); - }); - - it('sets `loading` to false when request completes', () => { - jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({}); - - wrapper.vm.fetchLabels('foo'); - return waitForPromises().then(() => { - expect(wrapper.vm.loading).toBe(false); + it('sets `loading` to false when request completes', () => { + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); }); }); describe('template', () => { - const defaultLabels = OPTIONS_NONE_ANY; - beforeEach(async () => { - wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - labels: mockLabels, + wrapper = createComponent({ + value: { data: `"${mockRegularLabel.title}"` }, + config: { + initialLabels: mockLabels, + }, }); await nextTick(); }); it('renders base-token component', () => { - const baseTokenEl = wrapper.findComponent(BaseToken); + const baseTokenEl = findBaseToken(); expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ @@ -166,7 +188,7 @@ describe('LabelToken', () => { }); it('renders token item when value is selected', () => { - const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + const tokenSegments = findTokenSegments(); expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label" expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label" @@ -181,12 +203,12 @@ describe('LabelToken', () => { config: { ...mockLabelToken, defaultLabels }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + const tokenSegments = findTokenSegments(); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); - const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); + const suggestions = findSuggestions(); expect(suggestions).toHaveLength(defaultLabels.length); defaultLabels.forEach((label, index) => { @@ -200,7 +222,7 @@ describe('LabelToken', () => { config: { ...mockLabelToken, defaultLabels: [] }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + const tokenSegments = findTokenSegments(); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); await nextTick(); @@ -215,11 +237,10 @@ describe('LabelToken', () => { config: { ...mockLabelToken }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + const tokenSegments = findTokenSegments(); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - - const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); + const suggestions = findSuggestions(); expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length); OPTIONS_NONE_ANY.forEach((label, index) => { @@ -234,7 +255,7 @@ describe('LabelToken', () => { input: mockInput, }, }); - wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]); + findBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]); expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]); }); 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 589697fe542..fea1496a80b 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 @@ -8,16 +8,17 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { sortMilestonesByDueDate } from '~/milestones/utils'; import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/milestones/utils'); const defaultStubs = { @@ -57,6 +58,12 @@ describe('MilestoneToken', () => { let mock; let wrapper; + const findBaseToken = () => wrapper.findComponent(BaseToken); + const triggerFetchMilestones = (searchTerm = null) => { + findBaseToken().vm.$emit('fetch-suggestions', searchTerm); + return waitForPromises(); + }; + beforeEach(() => { mock = new MockAdapter(axios); wrapper = createComponent(); @@ -64,73 +71,77 @@ describe('MilestoneToken', () => { afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('methods', () => { describe('fetchMilestones', () => { - describe('when config.shouldSkipSort is true', () => { - beforeEach(() => { - wrapper.vm.config.shouldSkipSort = true; + it('sets loading state', async () => { + wrapper = createComponent({ + config: { + fetchMilestones: jest.fn().mockResolvedValue(new Promise(() => {})), + }, }); + await nextTick(); - afterEach(() => { - wrapper.vm.config.shouldSkipSort = false; - }); + expect(findBaseToken().props('suggestionsLoading')).toBe(true); + }); + + describe('when config.shouldSkipSort is true', () => { it('does not call sortMilestonesByDueDate', async () => { - jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({ - data: mockMilestones, + wrapper = createComponent({ + config: { + shouldSkipSort: true, + fetchMilestones: jest.fn().mockResolvedValue({ data: mockMilestones }), + }, }); - wrapper.vm.fetchMilestones(); - - await waitForPromises(); + await triggerFetchMilestones(); expect(sortMilestonesByDueDate).toHaveBeenCalledTimes(0); }); }); - it('calls `config.fetchMilestones` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchMilestones'); - - wrapper.vm.fetchMilestones('foo'); + describe('when request is successful', () => { + const searchTerm = 'foo'; - expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo'); - }); - - it('sets response to `milestones` when request is successful', () => { - wrapper.vm.config.shouldSkipSort = false; + beforeEach(() => { + wrapper = createComponent({ + config: { + shouldSkipSort: false, + fetchMilestones: jest.fn().mockResolvedValue({ data: mockMilestones }), + }, + }); + return triggerFetchMilestones(searchTerm); + }); - jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({ - data: mockMilestones, + it('calls `config.fetchMilestones` with provided searchTerm param', () => { + expect(findBaseToken().props('config').fetchMilestones).toHaveBeenCalledWith(searchTerm); }); - wrapper.vm.fetchMilestones(); - return waitForPromises().then(() => { - expect(wrapper.vm.milestones).toEqual(mockMilestones); + it('sets response to `milestones`', () => { expect(sortMilestonesByDueDate).toHaveBeenCalled(); + expect(findBaseToken().props('suggestions')).toEqual(mockMilestones); }); }); - it('calls `createAlert` with flash error message when request fails', () => { - jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); - - wrapper.vm.fetchMilestones('foo'); + describe('when request fails', () => { + beforeEach(() => { + wrapper = createComponent({ + config: { + fetchMilestones: jest.fn().mockRejectedValue({}), + }, + }); + return triggerFetchMilestones(); + }); - return waitForPromises().then(() => { + it('calls `createAlert` with alert error message', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'There was a problem fetching milestones.', }); }); - }); - it('sets `loading` to false when request completes', () => { - jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); - - wrapper.vm.fetchMilestones('foo'); - - return waitForPromises().then(() => { - expect(wrapper.vm.loading).toBe(false); + it('sets `loading` to false when request completes', () => { + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); }); @@ -143,15 +154,12 @@ describe('MilestoneToken', () => { ]; beforeEach(async () => { - wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - milestones: mockMilestones, + wrapper = createComponent({ + value: { data: `"${mockRegularMilestone.title}"` }, + config: { + initialMilestones: mockMilestones, + }, }); - - await nextTick(); }); it('renders gl-filtered-search-token component', () => { @@ -228,7 +236,7 @@ describe('MilestoneToken', () => { it('finds the correct value from the activeToken', () => { DEFAULT_MILESTONES.forEach(({ value, title }) => { - const activeToken = wrapper.vm.getActiveMilestone([], value); + const activeToken = findBaseToken().props('getActiveTokenValue')([], value); expect(activeToken.title).toEqual(title); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js index 0e5fa0f66d4..5190ab919b1 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js @@ -2,11 +2,11 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui' import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; import { mockReleaseToken } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('ReleaseToken', () => { const id = '123'; @@ -27,10 +27,6 @@ describe('ReleaseToken', () => { }, }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders release value', async () => { wrapper = createComponent({ value: { data: id } }); await nextTick(); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js index 32cb74d5f80..89003296854 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js @@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -17,7 +17,7 @@ import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_t import { mockAuthorToken, mockUsers } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const defaultStubs = { Portal: true, GlFilteredSearchSuggestionList: { @@ -67,99 +67,82 @@ function createComponent(options = {}) { } describe('UserToken', () => { - const originalGon = window.gon; const currentUserLength = 1; let mock; let wrapper; - const getBaseToken = () => wrapper.findComponent(BaseToken); + const findBaseToken = () => wrapper.findComponent(BaseToken); beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { - window.gon = originalGon; mock.restore(); - wrapper.destroy(); }); describe('methods', () => { describe('fetchUsers', () => { + const triggerFetchUsers = (searchTerm = null) => { + findBaseToken().vm.$emit('fetch-suggestions', searchTerm); + return waitForPromises(); + }; + beforeEach(() => { wrapper = createComponent(); }); - it('calls `config.fetchUsers` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchUsers'); - - getBaseToken().vm.$emit('fetch-suggestions', mockUsers[0].username); - - expect(wrapper.vm.config.fetchUsers).toHaveBeenCalledWith( - mockAuthorToken.fetchPath, - mockUsers[0].username, - ); - }); - - it('sets response to `users` when request is successful', () => { - jest.spyOn(wrapper.vm.config, 'fetchUsers').mockResolvedValue(mockUsers); - - getBaseToken().vm.$emit('fetch-suggestions', 'root'); - - return waitForPromises().then(() => { - expect(getBaseToken().props('suggestions')).toEqual(mockUsers); + it('sets loading state', async () => { + wrapper = createComponent({ + config: { + fetchUsers: jest.fn().mockResolvedValue(new Promise(() => {})), + }, }); + await nextTick(); + + expect(findBaseToken().props('suggestionsLoading')).toBe(true); }); - // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756 - describe('when there are null users presents', () => { - const mockUsersWithNullUser = mockUsers.concat([null]); + describe('when request is successful', () => { + const searchTerm = 'foo'; beforeEach(() => { - jest - .spyOn(wrapper.vm.config, 'fetchUsers') - .mockResolvedValue({ data: mockUsersWithNullUser }); - - getBaseToken().vm.$emit('fetch-suggestions', 'root'); + wrapper = createComponent({ + config: { + fetchUsers: jest.fn().mockResolvedValue({ data: mockUsers }), + }, + }); + return triggerFetchUsers(searchTerm); }); - describe('when res.data is present', () => { - it('filters the successful response when null values are present', () => { - return waitForPromises().then(() => { - expect(getBaseToken().props('suggestions')).toEqual(mockUsers); - }); - }); + it('calls `config.fetchUsers` with provided searchTerm param', () => { + expect(findBaseToken().props('config').fetchUsers).toHaveBeenCalledWith(searchTerm); }); - describe('when response is an array', () => { - it('filters the successful response when null values are present', () => { - return waitForPromises().then(() => { - expect(getBaseToken().props('suggestions')).toEqual(mockUsers); - }); - }); + it('sets response to `users` when request is successful', () => { + expect(findBaseToken().props('suggestions')).toEqual(mockUsers); }); }); - it('calls `createAlert` with flash error message when request fails', () => { - jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({}); - - getBaseToken().vm.$emit('fetch-suggestions', 'root'); + describe('when request fails', () => { + beforeEach(() => { + wrapper = createComponent({ + config: { + fetchUsers: jest.fn().mockRejectedValue({}), + }, + }); + return triggerFetchUsers(); + }); - return waitForPromises().then(() => { + it('calls `createAlert` with alert error message', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'There was a problem fetching users.', }); }); - }); - - it('sets `loading` to false when request completes', async () => { - jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({}); - - getBaseToken().vm.$emit('fetch-suggestions', 'root'); - await waitForPromises(); - - expect(getBaseToken().props('suggestionsLoading')).toBe(false); + it('sets `loading` to false when request completes', async () => { + expect(findBaseToken().props('suggestionsLoading')).toBe(false); + }); }); }); }); @@ -178,12 +161,12 @@ describe('UserToken', () => { data: { users: mockUsers }, }); - const baseTokenEl = getBaseToken(); + const baseTokenEl = findBaseToken(); expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ suggestions: mockUsers, - getActiveTokenValue: wrapper.vm.getActiveUser, + getActiveTokenValue: baseTokenEl.props('getActiveTokenValue'), }); }); @@ -191,7 +174,6 @@ describe('UserToken', () => { wrapper = createComponent({ value: { data: mockUsers[0].username }, data: { users: mockUsers }, - stubs: { Portal: true }, }); await nextTick(); @@ -215,30 +197,13 @@ describe('UserToken', () => { users: [ { ...mockUsers[0], + avatarUrl: mockUsers[0].avatar_url, + avatar_url: undefined, }, ], }, - stubs: { Portal: true }, }); - await nextTick(); - - expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - users: [ - { - ...mockUsers[0], - avatarUrl: mockUsers[0].avatar_url, - avatar_url: undefined, - }, - ], - }); - - await nextTick(); - expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url); }); @@ -264,7 +229,6 @@ describe('UserToken', () => { wrapper = createComponent({ active: true, config: { ...mockAuthorToken, defaultUsers: [] }, - stubs: { Portal: true }, }); const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); diff --git a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js index 361b162b6a0..eee8a0c4532 100644 --- a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js +++ b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js @@ -10,10 +10,6 @@ describe('Form Footer Actions', () => { }); } - afterEach(() => { - wrapper.destroy(); - }); - it('renders content properly', () => { const defaultSlot = 'Foo'; const prepend = 'Bar'; diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js index e1da8b690af..4f1603f93ba 100644 --- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js +++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js @@ -10,10 +10,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; describe('InputCopyToggleVisibility', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - const valueProp = 'hR8x1fuJbzwu5uFKLf9e'; const createComponent = (options = {}) => { @@ -21,7 +17,7 @@ describe('InputCopyToggleVisibility', () => { InputCopyToggleVisibility, merge({}, options, { directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }), ); diff --git a/spec/frontend/vue_shared/components/form/title_spec.js b/spec/frontend/vue_shared/components/form/title_spec.js index 452f3723e76..d499f847c72 100644 --- a/spec/frontend/vue_shared/components/form/title_spec.js +++ b/spec/frontend/vue_shared/components/form/title_spec.js @@ -12,10 +12,6 @@ describe('Title edit field', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('matches the snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js index af53d256236..1de206123fe 100644 --- a/spec/frontend/vue_shared/components/gl_countdown_spec.js +++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js @@ -10,10 +10,6 @@ describe('GlCountdown', () => { jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime()); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when there is time remaining', () => { beforeEach(async () => { wrapper = mount(GlCountdown, { diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js index 458f2cc5374..da9bc0f8a2f 100644 --- a/spec/frontend/vue_shared/components/header_ci_component_spec.js +++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js @@ -48,11 +48,6 @@ describe('Header CI Component', () => { ); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('render', () => { beforeEach(() => { createComponent({ itemName: 'Pipeline' }); diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js index 77c03dc0c3c..76e66d07fa0 100644 --- a/spec/frontend/vue_shared/components/help_popover_spec.js +++ b/spec/frontend/vue_shared/components/help_popover_spec.js @@ -23,10 +23,6 @@ describe('HelpPopover', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('with title and content', () => { beforeEach(() => { createComponent(); 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 c63e46313b3..dd20b09f176 100644 --- a/spec/frontend/vue_shared/components/integration_help_text_spec.js +++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js @@ -22,11 +22,6 @@ describe('IntegrationHelpText component', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should use the gl components', () => { wrapper = createComponent(); diff --git a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js index 10c6cbe6d94..f69a883ee4d 100644 --- a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js +++ b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js @@ -37,10 +37,6 @@ describe('~/vue_shared/components/keep_alive_slots.vue', () => { isVisible: x.isVisible(), })); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js index 7ed6a59c844..4e83f3e1c06 100644 --- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js +++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlFormGroup, GlListbox } from '@gitlab/ui'; +import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui'; import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue'; describe('ListboxInput', () => { @@ -27,7 +27,7 @@ describe('ListboxInput', () => { // Finders const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); - const findGlListbox = () => wrapper.findComponent(GlListbox); + const findGlListbox = () => wrapper.findComponent(GlCollapsibleListbox); const findInput = () => wrapper.find('input'); const createComponent = (propsData) => { @@ -153,7 +153,7 @@ describe('ListboxInput', () => { expect(findGlListbox().props('searchable')).toBe(true); }); - it('passes all items to GlListbox by default', () => { + it('passes all items to GlCollapsibleListbox by default', () => { createComponent(); expect(findGlListbox().props('items')).toStrictEqual(items); diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js index a80717a1aea..1c7f419b118 100644 --- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js +++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js @@ -17,7 +17,6 @@ describe('Local Storage Sync', () => { const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value); afterEach(() => { - wrapper.destroy(); localStorage.clear(); }); diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js index ecb2b37c3a5..8aab867f32a 100644 --- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js +++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js @@ -17,11 +17,6 @@ describe('Apply Suggestion component', () => { beforeEach(() => createWrapper()); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('initial template', () => { it('renders a dropdown with the correct props', () => { const dropdown = findDropdown(); diff --git a/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js new file mode 100644 index 00000000000..67f296b1bf0 --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js @@ -0,0 +1,66 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue'; +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { create } from '~/drawio/markdown_field_editor_facade'; + +jest.mock('~/drawio/drawio_editor'); +jest.mock('~/drawio/markdown_field_editor_facade'); + +describe('vue_shared/components/markdown/drawio_toolbar_button', () => { + let wrapper; + let textArea; + const uploadsPath = '/uploads'; + const markdownPreviewPath = '/markdown/preview'; + + const buildWrapper = (props = { uploadsPath, markdownPreviewPath }) => { + wrapper = shallowMount(DrawioToolbarButton, { + propsData: { + ...props, + }, + }); + }; + + beforeEach(() => { + textArea = document.createElement('textarea'); + textArea.classList.add('js-gfm-input'); + + document.body.appendChild(textArea); + }); + + afterEach(() => { + textArea.remove(); + }); + + describe('default', () => { + it('renders button that launches draw.io editor', () => { + buildWrapper(); + + expect(wrapper.findComponent(GlButton).props()).toMatchObject({ + icon: 'diagram', + category: 'tertiary', + }); + }); + }); + + describe('when clicking button', () => { + it('launches draw.io editor', async () => { + const editorFacadeStub = {}; + + create.mockReturnValueOnce(editorFacadeStub); + + buildWrapper(); + + await wrapper.findComponent(GlButton).vm.$emit('click'); + + expect(create).toHaveBeenCalledWith({ + markdownPreviewPath, + textArea, + uploadsPath, + }); + expect(launchDrawioEditor).toHaveBeenCalledWith({ + editorFacade: editorFacadeStub, + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js index 34071775b9c..becd4257cbe 100644 --- a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js @@ -21,14 +21,10 @@ describe('vue_shared/component/markdown/editor_mode_dropdown', () => { .filter((item) => item.text().startsWith(text)) .at(0); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` - modeText | value | dropdownText | otherMode - ${'Rich text'} | ${'richText'} | ${'View markdown'} | ${'Markdown'} - ${'Markdown'} | ${'markdown'} | ${'View rich text'} | ${'Rich text'} + modeText | value | dropdownText | otherMode + ${'Rich text'} | ${'richText'} | ${'Viewing rich text'} | ${'Markdown'} + ${'Markdown'} | ${'markdown'} | ${'Viewing markdown'} | ${'Rich text'} `('$modeText', ({ modeText, value, dropdownText, otherMode }) => { beforeEach(() => { createComponent({ value }); diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js index 176ccfc5a69..1bbbe0896f2 100644 --- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js @@ -6,20 +6,14 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm'; jest.mock('~/behaviors/markdown/render_gfm'); describe('Markdown Field View component', () => { - let wrapper; - function createComponent() { - wrapper = shallowMount(MarkdownFieldView); + shallowMount(MarkdownFieldView); } beforeEach(() => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('processes rendering with GFM', () => { expect(renderGFM).toHaveBeenCalledTimes(1); }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index ed417097e1e..68f05e5119d 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -3,6 +3,7 @@ import { nextTick } from 'vue'; import { GlTabs } from '@gitlab/ui'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; +import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; describe('Markdown field header component', () => { @@ -26,6 +27,7 @@ describe('Markdown field header component', () => { findToolbarButtons() .filter((button) => button.props(prop) === value) .at(0); + const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton); beforeEach(() => { window.gl = { @@ -37,10 +39,6 @@ describe('Markdown field header component', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('markdown header buttons', () => { it('renders the buttons with the correct title', () => { const buttons = [ @@ -197,4 +195,24 @@ describe('Markdown field header component', () => { expect(findToolbarButtons().length).toBe(defaultCount); }); }); + + describe('when drawIOEnabled is true', () => { + const uploadsPath = '/uploads'; + const markdownPreviewPath = '/preview'; + + beforeEach(() => { + createWrapper({ + drawioEnabled: true, + uploadsPath, + markdownPreviewPath, + }); + }); + + it('renders drawio toolbar button', () => { + expect(findDrawioToolbarButton().props()).toEqual({ + uploadsPath, + markdownPreviewPath, + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index 26b536984ff..681ff6c8dd3 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import Autosize from 'autosize'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; @@ -9,10 +10,15 @@ import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { stubComponent } from 'helpers/stub_component'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/emoji'); +jest.mock('autosize'); describe('vue_shared/component/markdown/markdown_editor', () => { + useLocalStorageSpy(); + let wrapper; const value = 'test markdown'; const renderMarkdownPath = '/api/markdown'; @@ -57,14 +63,27 @@ describe('vue_shared/component/markdown/markdown_editor', () => { const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const findContentEditor = () => wrapper.findComponent(ContentEditor); + const enableContentEditor = async () => { + findMarkdownField().vm.$emit('enableContentEditor'); + await nextTick(); + await waitForPromises(); + }; + + const enableMarkdownEditor = async () => { + findContentEditor().vm.$emit('enableMarkdownEditor'); + await nextTick(); + await waitForPromises(); + }; + beforeEach(() => { window.uploads_path = 'uploads'; mock = new MockAdapter(axios); }); afterEach(() => { - wrapper.destroy(); mock.restore(); + + localStorage.clear(); }); it('displays markdown field by default', () => { @@ -83,8 +102,133 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }); }); + it('enables content editor switcher when contentEditorEnabled prop is true', () => { + buildWrapper({ propsData: { enableContentEditor: true } }); + + expect(findMarkdownField().text()).toContain('Rich text'); + }); + + it('hides content editor switcher when contentEditorEnabled prop is false', () => { + buildWrapper({ propsData: { enableContentEditor: false } }); + + expect(findMarkdownField().text()).not.toContain('Rich text'); + }); + + it('passes down any additional props to markdown field component', () => { + const propsData = { + line: { text: 'hello world', richText: 'hello world' }, + lines: [{ text: 'hello world', richText: 'hello world' }], + canSuggest: true, + }; + + buildWrapper({ + propsData: { ...propsData, myCustomProp: 'myCustomValue', 'data-testid': 'custom id' }, + }); + + expect(findMarkdownField().props()).toMatchObject(propsData); + expect(findMarkdownField().vm.$attrs).toMatchObject({ + myCustomProp: 'myCustomValue', + + // data-testid isn't copied over + 'data-testid': 'markdown-field', + }); + }); + + describe('disabled', () => { + it('disables markdown field when disabled prop is true', () => { + buildWrapper({ propsData: { disabled: true } }); + + expect(findMarkdownField().find('textarea').attributes('disabled')).toBe('disabled'); + }); + + it('enables markdown field when disabled prop is false', () => { + buildWrapper({ propsData: { disabled: false } }); + + expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined); + }); + + it('disables content editor when disabled prop is true', async () => { + buildWrapper({ propsData: { disabled: true } }); + + await enableContentEditor(); + + expect(findContentEditor().props('editable')).toBe(false); + }); + + it('enables content editor when disabled prop is false', async () => { + buildWrapper({ propsData: { disabled: false } }); + + await enableContentEditor(); + + expect(findContentEditor().props('editable')).toBe(true); + }); + }); + + describe('autosize', () => { + it('autosizes the textarea when the value changes', async () => { + buildWrapper(); + await findTextarea().setValue('Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines'); + + expect(Autosize.update).toHaveBeenCalled(); + }); + + it('autosizes the textarea when the value changes from outside the component', async () => { + buildWrapper(); + wrapper.setProps({ value: 'Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines' }); + + await nextTick(); + await waitForPromises(); + expect(Autosize.update).toHaveBeenCalled(); + }); + + it('does not autosize the textarea if markdown editor is disabled', async () => { + buildWrapper(); + await enableContentEditor(); + + wrapper.setProps({ value: 'Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines' }); + + expect(Autosize.update).not.toHaveBeenCalled(); + }); + }); + + describe('autosave', () => { + it('automatically saves the textarea value to local storage if autosaveKey is defined', () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: 'This is **markdown**' } }); + + expect(localStorage.getItem('autosave/issue/1234')).toBe('This is **markdown**'); + }); + + it("loads value from local storage if autosaveKey is defined, and value isn't", () => { + localStorage.setItem('autosave/issue/1234', 'This is **markdown**'); + + buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } }); + + expect(findTextarea().element.value).toBe('This is **markdown**'); + }); + + it("doesn't load value from local storage if autosaveKey is defined, and value is", () => { + localStorage.setItem('autosave/issue/1234', 'This is **markdown**'); + + buildWrapper({ propsData: { autosaveKey: 'issue/1234' } }); + + expect(findTextarea().element.value).toBe('test markdown'); + }); + + it('does not save the textarea value to local storage if autosaveKey is not defined', () => { + buildWrapper({ propsData: { value: 'This is **markdown**' } }); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('does not save the textarea value to local storage if value is empty', () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } }); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + }); + it('renders markdown field textarea', () => { - buildWrapper(); + buildWrapper({ propsData: { supportsQuickActions: true } }); expect(findTextarea().attributes()).toEqual( expect.objectContaining({ @@ -92,6 +236,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { name: formFieldName, placeholder: formFieldPlaceholder, 'aria-label': formFieldAriaLabel, + 'data-supports-quick-actions': 'true', }), ); @@ -107,9 +252,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => { buildWrapper(); - findMarkdownField().vm.$emit('enableContentEditor'); - - await nextTick(); + await enableContentEditor(); expect(wrapper.emitted(EDITING_MODE_CONTENT_EDITOR)).toHaveLength(1); }); @@ -119,11 +262,8 @@ describe('vue_shared/component/markdown/markdown_editor', () => { stubs: { ContentEditor: stubComponent(ContentEditor) }, }); - findMarkdownField().vm.$emit('enableContentEditor'); - - await nextTick(); - - findContentEditor().vm.$emit('enableMarkdownEditor'); + await enableContentEditor(); + await enableMarkdownEditor(); expect(wrapper.emitted(EDITING_MODE_MARKDOWN_FIELD)).toHaveLength(1); }); @@ -138,6 +278,16 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(wrapper.emitted('input')).toEqual([[newValue]]); }); + it('autosaves the markdown value to local storage', async () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234' } }); + + const newValue = 'new value'; + + await findTextarea().setValue(newValue); + + expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue); + }); + describe('when autofocus is true', () => { beforeEach(async () => { buildWrapper({ attachTo: document.body, propsData: { autofocus: true } }); @@ -159,9 +309,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }); describe(`when markdown field triggers enableContentEditor event`, () => { - beforeEach(() => { + beforeEach(async () => { buildWrapper(); - findMarkdownField().vm.$emit('enableContentEditor'); + await enableContentEditor(); }); it('displays the content editor', () => { @@ -169,7 +319,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect.objectContaining({ renderMarkdown: expect.any(Function), uploadsPath: window.uploads_path, - useBottomToolbar: false, markdown: value, }), ); @@ -198,9 +347,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }); describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => { - beforeEach(() => { - buildWrapper(); - findMarkdownField().vm.$emit('enableContentEditor'); + beforeEach(async () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234' } }); + await enableContentEditor(); }); describe('when autofocus is true', () => { @@ -224,6 +373,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(wrapper.emitted('input')).toEqual([[newValue]]); }); + it('autosaves the content editor value to local storage', async () => { + const newValue = 'new value'; + + await findContentEditor().vm.$emit('change', { markdown: newValue }); + + expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue); + }); + it('bubbles up keydown event', () => { const event = new Event('keydown'); @@ -233,9 +390,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }); describe(`when richText editor triggers enableMarkdownEditor event`, () => { - beforeEach(() => { - findContentEditor().vm.$emit('enableMarkdownEditor'); - }); + beforeEach(enableMarkdownEditor); it('hides the content editor', () => { expect(findContentEditor().exists()).toBe(false); diff --git a/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js new file mode 100644 index 00000000000..8ad9ad30c1d --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import SavedRepliesDropdown from '~/vue_shared/components/markdown/saved_replies_dropdown.vue'; +import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql'; + +let wrapper; +let savedRepliesResp; + +function createMockApolloProvider(response) { + Vue.use(VueApollo); + + savedRepliesResp = jest.fn().mockResolvedValue(response); + + const requestHandlers = [[savedRepliesQuery, savedRepliesResp]]; + + return createMockApollo(requestHandlers); +} + +function createComponent(options = {}) { + const { mockApollo } = options; + + return mountExtended(SavedRepliesDropdown, { + propsData: { + newSavedRepliesPath: '/new', + }, + apolloProvider: mockApollo, + }); +} + +describe('Saved replies dropdown', () => { + it('fetches data when dropdown gets opened', async () => { + const mockApollo = createMockApolloProvider(savedRepliesResponse); + wrapper = createComponent({ mockApollo }); + + wrapper.findByTestId('saved-replies-dropdown-toggle').trigger('click'); + + await waitForPromises(); + + expect(savedRepliesResp).toHaveBeenCalled(); + }); + + it('adds markdown toolbar attributes to dropdown items', async () => { + const mockApollo = createMockApolloProvider(savedRepliesResponse); + wrapper = createComponent({ mockApollo }); + + wrapper.findByTestId('saved-replies-dropdown-toggle').trigger('click'); + + await waitForPromises(); + + expect(wrapper.findByTestId('saved-reply-dropdown-item').attributes()).toEqual( + expect.objectContaining({ + 'data-md-cursor-offset': '0', + 'data-md-prepend': 'true', + 'data-md-tag': 'Saved Reply Content', + }), + ); + }); +}); 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 9db1b779a04..9768bc7a6dd 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 @@ -25,7 +25,7 @@ describe('Suggestion Diff component', () => { ...props, }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; @@ -34,10 +34,6 @@ describe('Suggestion Diff component', () => { window.gon.current_user_id = 1; }); - afterEach(() => { - wrapper.destroy(); - }); - const findApplyButton = () => wrapper.findComponent(ApplySuggestion); const findApplyBatchButton = () => wrapper.find('.js-apply-batch-btn'); const findAddToBatchButton = () => wrapper.find('.js-add-to-batch-btn'); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js index f9a8b64f89b..c46a2d3e117 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js @@ -36,10 +36,6 @@ describe('SuggestionDiffRow', () => { const findNewLineWrapper = () => wrapper.find('.new_line'); const findSuggestionContent = () => wrapper.find('[data-testid="suggestion-diff-content"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('renders correctly', () => { it('renders the correct base suggestion markup', () => { factory({ 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 d84483c1663..8c7f51664ad 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js @@ -61,11 +61,6 @@ describe('Suggestion Diff component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('matches snapshot', () => { expect(wrapper.element).toMatchSnapshot(); }); 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 82210e79799..33e9d6add99 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js @@ -20,11 +20,6 @@ describe('toolbar_button', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const getButtonShortcutsAttr = () => { return wrapper.findComponent(GlButton).attributes('data-md-shortcuts'); }; diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js index b1a1dbbeb7a..fea14f80496 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js @@ -11,10 +11,6 @@ describe('toolbar', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('user can attach file', () => { beforeEach(() => { createMountedWrapper(); diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js index 2b311b75f85..37b0767616a 100644 --- a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js +++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js @@ -36,8 +36,6 @@ describe('MarkdownDrawer', () => { }; afterEach(() => { - wrapper.destroy(); - wrapper = null; Object.keys(cache).forEach((key) => delete cache[key]); }); diff --git a/spec/frontend/vue_shared/components/memory_graph_spec.js b/spec/frontend/vue_shared/components/memory_graph_spec.js index ae8d5ff78ba..81325fb3269 100644 --- a/spec/frontend/vue_shared/components/memory_graph_spec.js +++ b/spec/frontend/vue_shared/components/memory_graph_spec.js @@ -1,10 +1,8 @@ import { GlSparklineChart } from '@gitlab/ui/dist/charts'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; import MemoryGraph from '~/vue_shared/components/memory_graph.vue'; describe('MemoryGraph', () => { - const Component = Vue.extend(MemoryGraph); let wrapper; const metrics = [ [1573586253.853, '2.87'], @@ -13,12 +11,10 @@ describe('MemoryGraph', () => { [1573586433.853, '3.0066964285714284'], ]; - afterEach(() => { - wrapper.destroy(); - }); + const findGlSparklineChart = () => wrapper.findComponent(GlSparklineChart); beforeEach(() => { - wrapper = shallowMount(Component, { + wrapper = shallowMount(MemoryGraph, { propsData: { metrics, width: 100, @@ -27,19 +23,15 @@ describe('MemoryGraph', () => { }); }); - describe('chartData', () => { - it('should calculate chartData', () => { - expect(wrapper.vm.chartData.length).toEqual(metrics.length); - }); - - it('should format date & MB values', () => { + describe('Chart data', () => { + it('should have formatted date & MB values', () => { const formattedData = [ ['Nov 12 2019 19:17:33', '2.87'], ['Nov 12 2019 19:18:33', '2.78'], ['Nov 12 2019 19:19:33', '2.78'], ['Nov 12 2019 19:20:33', '3.01'], ]; - expect(wrapper.vm.chartData).toEqual(formattedData); + expect(findGlSparklineChart().props('data')).toEqual(formattedData); }); }); @@ -47,7 +39,7 @@ describe('MemoryGraph', () => { it('should draw container with chart', () => { expect(wrapper.element).toMatchSnapshot(); expect(wrapper.find('.memory-graph-container').exists()).toBe(true); - expect(wrapper.findComponent(GlSparklineChart).exists()).toBe(true); + expect(findGlSparklineChart().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js index 537367940e0..626f6fc735e 100644 --- a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js @@ -4,11 +4,11 @@ import actionsFactory from '~/vue_shared/components/metric_images/store/actions' import * as types from '~/vue_shared/components/metric_images/store/mutation_types'; import createStore from '~/vue_shared/components/metric_images/store'; import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { fileList, initialData } from '../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); const service = { getMetricImages: jest.fn(), uploadMetricImage: jest.fn(), diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index 61e4e774420..a649b06c50e 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -6,10 +6,6 @@ import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; describe('modal copy button', () => { let wrapper; - afterEach(() => { - wrapper.destroy(); - }); - beforeEach(() => { wrapper = shallowMount(ModalCopyButton, { propsData: { diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js index b1bec28bffb..947ee756259 100644 --- a/spec/frontend/vue_shared/components/navigation_tabs_spec.js +++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js @@ -38,11 +38,6 @@ describe('navigation tabs component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should render tabs', () => { expect(wrapper.findAllComponents(GlTab)).toHaveLength(data.length); }); diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js index 31320b1d2a6..a116233a065 100644 --- a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js @@ -21,7 +21,7 @@ import { searchProjectsWithinGroupQueryResponse, } from './mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('NewResourceDropdown component', () => { useLocalStorageSpy(); 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 17a62ae8a33..f87674246d1 100644 --- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js +++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js @@ -132,12 +132,6 @@ describe('Issue Warning Component', () => { }); }); - afterEach(() => { - wrapperLocked.destroy(); - wrapperConfidential.destroy(); - wrapperLockedAndConfidential.destroy(); - }); - it('renders confidential & locked messages with noteable "issue"', () => { expect(findLockedBlock(wrapperLocked).text()).toContain('This issue is locked.'); expect(findConfidentialBlock(wrapperConfidential).text()).toContain( 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 8f9f1bb336f..7e669fb7c71 100644 --- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js @@ -30,11 +30,6 @@ describe('Issue placeholder note component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('matches snapshot', () => { createComponent(); diff --git a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js index de6ab43bc41..5897b9e0ffc 100644 --- a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js @@ -12,11 +12,6 @@ describe('Placeholder system note component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('matches snapshot', () => { createComponent(); diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js index bcfd7a8ec70..29e1a9ccf4d 100644 --- a/spec/frontend/vue_shared/components/notes/system_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js @@ -46,7 +46,6 @@ describe('system note component', () => { }); afterEach(() => { - vm.destroy(); mock.restore(); }); diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js index bd4b6a463ab..fa9d3cd28a9 100644 --- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js +++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js @@ -10,10 +10,6 @@ describe(`TimelineEntryItem`, () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders correctly', () => { factory(); diff --git a/spec/frontend/vue_shared/components/ordered_layout_spec.js b/spec/frontend/vue_shared/components/ordered_layout_spec.js index 21588569d6a..b6c8c467028 100644 --- a/spec/frontend/vue_shared/components/ordered_layout_spec.js +++ b/spec/frontend/vue_shared/components/ordered_layout_spec.js @@ -37,10 +37,6 @@ describe('Ordered Layout', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when slotKeys are in initial slot order', () => { beforeEach(() => { createComponent({ slotKeys: regularSlotOrder }); diff --git a/spec/frontend/vue_shared/components/page_size_selector_spec.js b/spec/frontend/vue_shared/components/page_size_selector_spec.js index 5ec0b863afd..fce7ceee2fe 100644 --- a/spec/frontend/vue_shared/components/page_size_selector_spec.js +++ b/spec/frontend/vue_shared/components/page_size_selector_spec.js @@ -14,10 +14,6 @@ describe('Page size selector component', () => { const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - afterEach(() => { - wrapper.destroy(); - }); - it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => { createWrapper({ pageSize }); diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js index ae9c920ebd2..fc9adab2e2b 100644 --- a/spec/frontend/vue_shared/components/paginated_list_spec.js +++ b/spec/frontend/vue_shared/components/paginated_list_spec.js @@ -33,10 +33,6 @@ describe('Pagination links component', () => { [glPaginatedList] = wrapper.vm.$children; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('Paginated List Component', () => { describe('props', () => { // We test attrs and not props because we pass through to child component using v-bind:"$attrs" 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 86a63db0d9e..25bfa688e5b 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 @@ -108,16 +108,23 @@ describe('AlertManagementEmptyState', () => { const findStatusTabs = () => wrapper.findComponent(GlTabs); const findStatusFilterBadge = () => wrapper.findAllComponents(GlBadge); + const handleFilterItems = (filters) => { + Filters().vm.$emit('onFilter', filters); + return nextTick(); + }; + describe('Snowplow tracking', () => { + const category = 'category'; + const action = 'action'; + beforeEach(() => { jest.spyOn(Tracking, 'event'); mountComponent({ - props: { trackViewsOptions: { category: 'category', action: 'action' } }, + props: { trackViewsOptions: { category, action } }, }); }); it('should track the items list page views', () => { - const { category, action } = wrapper.vm.trackViewsOptions; expect(Tracking.event).toHaveBeenCalledWith(category, action); }); }); @@ -234,14 +241,14 @@ describe('AlertManagementEmptyState', () => { findPagination().vm.$emit('input', 3); await nextTick(); - expect(wrapper.vm.previousPage).toBe(2); + expect(findPagination().props('prevPage')).toBe(2); }); it('returns 0 when it is the first page', async () => { findPagination().vm.$emit('input', 1); await nextTick(); - expect(wrapper.vm.previousPage).toBe(0); + expect(findPagination().props('prevPage')).toBe(0); }); }); @@ -265,14 +272,14 @@ describe('AlertManagementEmptyState', () => { findPagination().vm.$emit('input', 1); await nextTick(); - expect(wrapper.vm.nextPage).toBe(2); + expect(findPagination().props('nextPage')).toBe(2); }); it('returns `null` when currentPage is already last page', async () => { findStatusTabs().vm.$emit('input', 1); findPagination().vm.$emit('input', 1); await nextTick(); - expect(wrapper.vm.nextPage).toBeNull(); + expect(findPagination().props('nextPage')).toBeNull(); }); }); }); @@ -320,36 +327,32 @@ describe('AlertManagementEmptyState', () => { it('returns correctly applied filter search values', async () => { const searchTerm = 'foo'; - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - searchTerm, - }); - + await handleFilterItems([{ type: 'filtered-search-term', value: { data: searchTerm } }]); await nextTick(); - expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]); + expect(Filters().props('initialFilterValue')).toEqual([searchTerm]); }); - it('updates props tied to getIncidents GraphQL query', () => { - wrapper.vm.handleFilterItems(mockFilters); - - expect(wrapper.vm.authorUsername).toBe('root'); - expect(wrapper.vm.assigneeUsername).toEqual('root2'); - expect(wrapper.vm.searchTerm).toBe(mockFilters[2].value.data); - }); + it('updates props tied to getIncidents GraphQL query', async () => { + await handleFilterItems(mockFilters); - it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - authorUsername: 'foo', - searchTerm: 'bar', - }); + const [ + { + value: { data: authorUsername }, + }, + { + value: { data: assigneeUsername }, + }, + searchTerm, + ] = Filters().props('initialFilterValue'); - wrapper.vm.handleFilterItems([]); + expect(authorUsername).toBe('root'); + expect(assigneeUsername).toEqual('root2'); + expect(searchTerm).toBe(mockFilters[2].value.data); + }); - expect(wrapper.vm.authorUsername).toBe(''); - expect(wrapper.vm.searchTerm).toBe(''); + it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', async () => { + await handleFilterItems([]); + expect(Filters().props('initialFilterValue')).toEqual([]); }); }); }); 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 112cdaf74c6..2a1a6342c38 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 @@ -25,10 +25,6 @@ describe('Pagination bar', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('events', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js index d444ad7a733..99a4f776305 100644 --- a/spec/frontend/vue_shared/components/pagination_links_spec.js +++ b/spec/frontend/vue_shared/components/pagination_links_spec.js @@ -44,10 +44,6 @@ describe('Pagination links component', () => { glPagination = wrapper.findComponent(GlPagination); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should provide translated text to GitLab UI pagination', () => { Object.entries(translations).forEach((entry) => { expect(glPagination.vm[entry[0]]).toBe(entry[1]); diff --git a/spec/frontend/vue_shared/components/panel_resizer_spec.js b/spec/frontend/vue_shared/components/panel_resizer_spec.js index 0e261124cbf..a535fe4939c 100644 --- a/spec/frontend/vue_shared/components/panel_resizer_spec.js +++ b/spec/frontend/vue_shared/components/panel_resizer_spec.js @@ -27,10 +27,6 @@ describe('Panel Resizer component', () => { el.dispatchEvent(event); }; - afterEach(() => { - wrapper.destroy(); - }); - it('should render a div element with the correct classes and styles', () => { wrapper = mount(PanelResizer, { propsData: { diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js index ff4febd647e..a44a1aba8c0 100644 --- a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js +++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js @@ -16,10 +16,6 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () const findAlert = () => wrapper.findComponent(GlAlert); - afterEach(() => { - wrapper.destroy(); - }); - it('should render alert with correct props', async () => { createComponent({ errorMessages: [{ code: 'MissingQuotes' }] }); await nextTick(); diff --git a/spec/frontend/vue_shared/components/project_avatar_spec.js b/spec/frontend/vue_shared/components/project_avatar_spec.js index af828fbca51..9378f6e3f1b 100644 --- a/spec/frontend/vue_shared/components/project_avatar_spec.js +++ b/spec/frontend/vue_shared/components/project_avatar_spec.js @@ -15,10 +15,6 @@ describe('ProjectAvatar', () => { wrapper = shallowMount(ProjectAvatar, { propsData: { ...defaultProps, ...props }, attrs }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders GlAvatar with correct props', () => { createComponent(); 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 4e0c318c84e..d704fcc0e7b 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 @@ -1,57 +1,49 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import { GlButton } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; describe('ProjectListItem component', () => { - const Component = Vue.extend(ProjectListItem); let wrapper; - let vm; - let options; const project = JSON.parse(JSON.stringify(mockProjects))[0]; - beforeEach(() => { - options = { + const createWrapper = ({ propsData } = {}) => { + wrapper = shallowMountExtended(ProjectListItem, { propsData: { project, selected: false, + ...propsData, }, - }; - }); - - afterEach(() => { - wrapper.vm.$destroy(); - }); - - it('does not render a check mark icon if selected === false', () => { - wrapper = shallowMount(Component, options); - - expect(wrapper.find('.js-selected-icon').exists()).toBe(false); - }); + }); + }; - it('renders a check mark icon if selected === true', () => { - options.propsData.selected = true; + const findProjectNamespace = () => wrapper.findByTestId('project-namespace'); + const findProjectName = () => wrapper.findByTestId('project-name'); - wrapper = shallowMount(Component, options); + it.each([true, false])('renders a checkmark correctly when selected === "%s"', (selected) => { + createWrapper({ + propsData: { + selected, + }, + }); - expect(wrapper.find('.js-selected-icon').exists()).toBe(true); + expect(wrapper.findByTestId('selected-icon').exists()).toBe(selected); }); - it(`emits a "clicked" event when clicked`, () => { - wrapper = shallowMount(Component, options); - ({ vm } = wrapper); + it(`emits a "clicked" event when the button is clicked`, () => { + createWrapper(); - jest.spyOn(vm, '$emit').mockImplementation(() => {}); - wrapper.vm.onClick(); + expect(wrapper.emitted('click')).toBeUndefined(); + wrapper.findComponent(GlButton).vm.$emit('click'); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); + expect(wrapper.emitted('click')).toHaveLength(1); }); it(`renders the project avatar`, () => { - wrapper = shallowMount(Component, options); + createWrapper(); const avatar = wrapper.findComponent(ProjectAvatar); expect(avatar.exists()).toBe(true); @@ -63,48 +55,73 @@ describe('ProjectListItem component', () => { }); it(`renders a simple namespace name with a trailing slash`, () => { - options.propsData.project.name_with_namespace = 'a / b'; - - wrapper = shallowMount(Component, options); - const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + createWrapper({ + propsData: { + project: { + ...project, + name_with_namespace: 'a / b', + }, + }, + }); + const renderedNamespace = trimText(findProjectNamespace().text()); expect(renderedNamespace).toBe('a /'); }); it(`renders a properly truncated namespace with a trailing slash`, () => { - options.propsData.project.name_with_namespace = 'a / b / c / d / e / f'; - - wrapper = shallowMount(Component, options); - const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + createWrapper({ + propsData: { + project: { + ...project, + name_with_namespace: 'a / b / c / d / e / f', + }, + }, + }); + const renderedNamespace = trimText(findProjectNamespace().text()); expect(renderedNamespace).toBe('a / ... / e /'); }); it(`renders a simple namespace name of a GraphQL project`, () => { - options.propsData.project.name_with_namespace = undefined; - options.propsData.project.nameWithNamespace = 'test'; - - wrapper = shallowMount(Component, options); - const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + createWrapper({ + propsData: { + project: { + ...project, + name_with_namespace: undefined, + nameWithNamespace: 'test', + }, + }, + }); + const renderedNamespace = trimText(findProjectNamespace().text()); expect(renderedNamespace).toBe('test /'); }); it(`renders the project name`, () => { - options.propsData.project.name = 'my-test-project'; - - wrapper = shallowMount(Component, options); - const renderedName = trimText(wrapper.find('.js-project-name').text()); + createWrapper({ + propsData: { + project: { + ...project, + name: 'my-test-project', + }, + }, + }); + const renderedName = trimText(findProjectName().text()); expect(renderedName).toBe('my-test-project'); }); it(`renders the project name with highlighting in the case of a search query match`, () => { - options.propsData.project.name = 'my-test-project'; - options.propsData.matcher = 'pro'; - - wrapper = shallowMount(Component, options); - const renderedName = trimText(wrapper.find('.js-project-name').html()); + createWrapper({ + propsData: { + project: { + ...project, + name: 'my-test-project', + }, + matcher: 'pro', + }, + }); + const renderedName = trimText(findProjectName().html()); const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject'; expect(renderedName).toContain(expected); @@ -112,11 +129,16 @@ describe('ProjectListItem component', () => { it('prevents search query and project name XSS', () => { const alertSpy = jest.spyOn(window, 'alert'); - options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject"; - options.propsData.matcher = "pro<script>alert('XSS');</script>"; - - wrapper = shallowMount(Component, options); - const renderedName = trimText(wrapper.find('.js-project-name').html()); + createWrapper({ + propsData: { + project: { + ...project, + name: "my-xss-pro<script>alert('XSS');</script>ject", + }, + matcher: "pro<script>alert('XSS');</script>", + }, + }); + const renderedName = trimText(findProjectName().html()); const expected = 'my-xss-project'; expect(renderedName).toContain(expected); 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 a0832dd7030..5e304f1c118 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 @@ -1,7 +1,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { head } from 'lodash'; -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; @@ -25,7 +25,7 @@ describe('ProjectSelector component', () => { }; beforeEach(() => { - wrapper = mount(Vue.extend(ProjectSelector), { + wrapper = mount(ProjectSelector, { propsData: { projectSearchResults: searchResults, selectedProjects: selected, 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 8f19f0ea14d..60c1293b7c1 100644 --- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js +++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js @@ -26,10 +26,6 @@ describe('Package code instruction', () => { const findInputElement = () => wrapper.find('[data-testid="instruction-input"]'); const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]'); - afterEach(() => { - wrapper.destroy(); - }); - describe('single line', () => { beforeEach(() => createComponent({ 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 ebc9816f983..9ef1ce5647d 100644 --- a/spec/frontend/vue_shared/components/registry/details_row_spec.js +++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js @@ -20,11 +20,6 @@ describe('DetailsRow', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('has a default slot', () => { mountComponent(); expect(findDefaultSlot().exists()).toBe(true); 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 947520567e6..17abe06dbee 100644 --- a/spec/frontend/vue_shared/components/registry/history_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js @@ -22,11 +22,6 @@ describe('History Item', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem); const findGlIcon = () => wrapper.findComponent(GlIcon); const findDefaultSlot = () => wrapper.find('[data-testid="default-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 b941eb77c32..298fa163d59 100644 --- a/spec/frontend/vue_shared/components/registry/list_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -30,11 +30,6 @@ describe('list item', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each` slotName | finderFunction ${'left-primary'} | ${findLeftPrimarySlot} 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 a04e1e237d4..278b09d80b2 100644 --- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js @@ -14,16 +14,11 @@ describe('Metadata Item', () => { wrapper = shallowMount(component, { propsData, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findIcon = () => wrapper.findComponent(GlIcon); const findLink = (w = wrapper) => w.findComponent(GlLink); const findText = () => wrapper.find('[data-testid="metadata-item-text"]'); diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js index 616fefe847e..b93fa37546f 100644 --- a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js +++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js @@ -31,10 +31,6 @@ describe('Persisted dropdown selection', () => { const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - afterEach(() => { - wrapper.destroy(); - }); - describe('local storage sync', () => { it('uses the local storage sync component with the correct props', () => { createComponent(); 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 591447a37c2..59bb0646350 100644 --- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js +++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js @@ -36,11 +36,6 @@ describe('Registry Search', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('searching', () => { it('has a filtered-search component', () => { mountComponent(); 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 efb57ddd310..ec1451de470 100644 --- a/spec/frontend/vue_shared/components/registry/title_area_spec.js +++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js @@ -36,11 +36,6 @@ describe('title area', () => { return acc; }, {}); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('title', () => { it('if slot is not present defaults to prop', () => { mountComponent(); diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap deleted file mode 100644 index cdfe311acd9..00000000000 --- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Resizable Chart Container renders the component 1`] = ` -<div> - <template> - <div - class="slot" - > - <span - class="width" - > - 0 - </span> - - <span - class="height" - > - 0 - </span> - </div> - </template> -</div> -`; diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js deleted file mode 100644 index 7536df24ac6..00000000000 --- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { mount } from '@vue/test-utils'; -import $ from 'jquery'; -import { nextTick } from 'vue'; -import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue'; - -jest.mock('~/lib/utils/common_utils', () => ({ - debounceByAnimationFrame(callback) { - return jest.spyOn({ callback }, 'callback'); - }, -})); - -describe('Resizable Chart Container', () => { - let wrapper; - - beforeEach(() => { - wrapper = mount(ResizableChartContainer, { - scopedSlots: { - default: ` - <template #default="{ width, height }"> - <div class="slot"> - <span class="width">{{width}}</span> - <span class="height">{{height}}</span> - </div> - </template> - `, - }, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders the component', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - - it('updates the slot width and height props', async () => { - const width = 1920; - const height = 1080; - - // JSDOM mocks and sets clientWidth/clientHeight to 0 so we set manually - wrapper.vm.$refs.chartWrapper = { clientWidth: width, clientHeight: height }; - - $(document).trigger('content.resize'); - - await nextTick(); - const widthNode = wrapper.find('.slot > .width'); - const heightNode = wrapper.find('.slot > .height'); - - expect(parseInt(widthNode.text(), 10)).toEqual(width); - expect(parseInt(heightNode.text(), 10)).toEqual(height); - }); - - it('calls onResize on manual resize', () => { - $(document).trigger('content.resize'); - expect(wrapper.vm.debouncedResize).toHaveBeenCalled(); - }); - - it('calls onResize on page resize', () => { - window.dispatchEvent(new Event('resize')); - expect(wrapper.vm.debouncedResize).toHaveBeenCalled(); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js index 5d96fe27676..11ee2e56c14 100644 --- a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js @@ -27,10 +27,6 @@ describe('RichTimestampTooltip', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the tooltip text header', () => { expect(wrapper.findByTestId('header-text').text()).toBe('Created just now'); }); diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js index f9d700fe67f..c4d4f80c573 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js @@ -59,10 +59,6 @@ describe('RunnerCliInstructions component', () => { runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockInstructions); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when the instructions are shown', () => { beforeEach(async () => { createComponent(); 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 8f593b6aa1b..cb35cbd35ad 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 @@ -74,10 +74,6 @@ describe('RunnerInstructionsModal component', () => { runnerPlatformsHandler = jest.fn().mockResolvedValue(mockRunnerPlatforms); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('when the modal is shown', () => { beforeEach(async () => { createComponent(); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js index 986d76d2b95..260eddbb37d 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js @@ -12,7 +12,7 @@ describe('RunnerInstructions component', () => { const createComponent = () => { wrapper = shallowMountExtended(RunnerInstructions, { directives: { - GlModal: createMockDirective(), + GlModal: createMockDirective('gl-tooltip'), }, }); }; @@ -21,10 +21,6 @@ describe('RunnerInstructions component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should show the "Show runner installation instructions" button', () => { expect(findModalButton().text()).toBe('Show runner installation instructions'); }); 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 09b0b3d43ad..6eebd129beb 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 @@ -6,7 +6,7 @@ import { expectedDownloadDropdownPropsWithTitle, securityReportMergeRequestDownloadPathsQueryResponse, } from 'jest/vue_shared/security_reports/mock_data'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue'; import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; import { @@ -15,7 +15,7 @@ import { } from '~/vue_shared/security_reports/constants'; import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('Merge request artifact Download', () => { let wrapper; @@ -52,10 +52,6 @@ describe('Merge request artifact Download', () => { const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); - afterEach(() => { - wrapper.destroy(); - }); - describe('given the query is loading', () => { beforeEach(() => { createWrapper({ 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 08d3d5b19d4..2f6e633fb34 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 @@ -21,11 +21,6 @@ describe('HelpIcon component', () => { const findPopover = () => wrapper.findComponent(GlPopover); const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('given a help path only', () => { beforeEach(() => { createWrapper(); diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js index f186eb848f2..61cdc329220 100644 --- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js +++ b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js @@ -15,11 +15,6 @@ describe('SecuritySummary component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe.each([ { message: '' }, { message: 'foo' }, diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js index 88445b6684c..c1feb64dacb 100644 --- a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js +++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js @@ -40,10 +40,6 @@ describe('~/vue_shared/components/segmented_control_button_group.vue', () => { disabled, })); - afterEach(() => { - wrapper.destroy(); - }); - describe('default', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js index 5e829653c13..94d634f79bd 100644 --- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js +++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js @@ -16,10 +16,6 @@ describe('Settings Block', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findDefaultSlot = () => wrapper.findByTestId('default-slot'); const findTitleSlot = () => wrapper.findByTestId('title-slot'); const findDescriptionSlot = () => wrapper.findByTestId('description-slot'); diff --git a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js index 8802a832781..e5d988f75f5 100644 --- a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js +++ b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js @@ -1,5 +1,4 @@ import { mount } from '@vue/test-utils'; -import Vue from 'vue'; import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue'; describe('Toggle Button', () => { @@ -16,7 +15,7 @@ describe('Toggle Button', () => { remain, }; - const Component = Vue.extend({ + const Component = { components: { SmartVirtualScrollList, }, @@ -26,7 +25,7 @@ describe('Toggle Button', () => { <smart-virtual-scroll-list v-bind="$options.smartListProperties"> <li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li> </smart-virtual-scroll-list>`, - }); + }; return mount(Component).vm; }; diff --git a/spec/frontend/vue_shared/components/source_editor_spec.js b/spec/frontend/vue_shared/components/source_editor_spec.js index ca5b990bc29..5b155207029 100644 --- a/spec/frontend/vue_shared/components/source_editor_spec.js +++ b/spec/frontend/vue_shared/components/source_editor_spec.js @@ -47,10 +47,6 @@ describe('Source Editor component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - const triggerChangeContent = (val) => { mockInstance.getValue.mockReturnValue(val); const [cb] = mockInstance.onDidChangeModelContent.mock.calls[0]; diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js index da9067a8ddc..395ba92d4c6 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js @@ -45,8 +45,6 @@ describe('Chunk component', () => { createComponent(); }); - afterEach(() => wrapper.destroy()); - describe('Intersection observer', () => { it('renders an Intersection observer component', () => { expect(findIntersectionObserver().exists()).toBe(true); 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 f661bd6747a..6c8fc244fa0 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 @@ -31,8 +31,6 @@ describe('Chunk Line component', () => { createComponent(); }); - afterEach(() => wrapper.destroy()); - describe('rendering', () => { it('renders a blame link', () => { expect(findBlameLink().attributes()).toMatchObject({ 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 95ef11d776a..59880496d74 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 @@ -24,8 +24,6 @@ describe('Chunk component', () => { createComponent(); }); - afterEach(() => wrapper.destroy()); - describe('Intersection observer', () => { it('renders an Intersection observer component', () => { expect(findIntersectionObserver().exists()).toBe(true); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js index 0beec8e9d3e..c911e3d308b 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js @@ -68,8 +68,6 @@ describe('Source Viewer component', () => { return createComponent(); }); - afterEach(() => wrapper.destroy()); - describe('event tracking', () => { it('fires a tracking event when the component is created', () => { const eventData = { label: EVENT_LABEL_VIEWER, property: language }; 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 1c75442b4a8..46b582c3668 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 @@ -25,8 +25,6 @@ describe('Source Viewer component', () => { return createComponent(); }); - afterEach(() => wrapper.destroy()); - describe('event tracking', () => { it('fires a tracking event when the component is created', () => { const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK }; diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js index 79b1f17afa0..13911d487f2 100644 --- a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js +++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js @@ -18,10 +18,6 @@ describe('StackedProgressBarComponent', () => { wrapper = mount(StackedProgressBarComponent, { propsData: defaultConfig }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSuccessBar = () => wrapper.find('.status-green'); const findNeutralBar = () => wrapper.find('.status-neutral'); const findFailureBar = () => wrapper.find('.status-red'); diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js index 99de26ce2ae..79aba1b2516 100644 --- a/spec/frontend/vue_shared/components/table_pagination_spec.js +++ b/spec/frontend/vue_shared/components/table_pagination_spec.js @@ -16,10 +16,6 @@ describe('Pagination component', () => { spy = jest.fn(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('render', () => { it('should not render anything', () => { mountComponent({ diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js index 28c5acc8110..a1757952dc0 100644 --- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js @@ -25,7 +25,6 @@ describe('Time ago with tooltip component', () => { }; afterEach(() => { - vm.destroy(); timezoneMock.unregister(); }); diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js index c8351ed61d7..d8dedd8240b 100644 --- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js @@ -1,4 +1,4 @@ -import { GlDropdownItem, GlDropdown } from '@gitlab/ui'; +import { GlDropdownItem, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; @@ -9,7 +9,9 @@ describe('Deploy freeze timezone dropdown', () => { let wrapper; let store; - const createComponent = (searchTerm, selectedTimezone) => { + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = async (searchTerm, selectedTimezone) => { wrapper = shallowMountExtended(TimezoneDropdown, { store, propsData: { @@ -19,9 +21,8 @@ describe('Deploy freeze timezone dropdown', () => { }, }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ searchTerm }); + findSearchBox().vm.$emit('input', searchTerm); + await nextTick(); }; const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); @@ -29,14 +30,9 @@ describe('Deploy freeze timezone dropdown', () => { const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults'); const findHiddenInput = () => wrapper.find('input'); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('No time zones found', () => { - beforeEach(() => { - createComponent('UTC timezone'); + beforeEach(async () => { + await createComponent('UTC timezone'); }); it('renders empty results message', () => { @@ -45,8 +41,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Search term is empty', () => { - beforeEach(() => { - createComponent(''); + beforeEach(async () => { + await createComponent(''); }); it('renders all timezones when search term is empty', () => { @@ -55,8 +51,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Time zones found', () => { - beforeEach(() => { - createComponent('Alaska'); + beforeEach(async () => { + await createComponent('Alaska'); }); it('renders only the time zone searched for', () => { @@ -87,8 +83,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Selected time zone not found', () => { - beforeEach(() => { - createComponent('', 'Berlin'); + beforeEach(async () => { + await createComponent('', 'Berlin'); }); it('renders empty selections', () => { @@ -101,8 +97,8 @@ describe('Deploy freeze timezone dropdown', () => { }); describe('Selected time zone found', () => { - beforeEach(() => { - createComponent('', 'Europe/Berlin'); + beforeEach(async () => { + await createComponent('', 'Europe/Berlin'); }); it('renders selected time zone as dropdown label', () => { 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 ca1f7996ad6..3807bb4cc63 100644 --- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js +++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js @@ -30,8 +30,8 @@ describe('TooltipOnTruncate component', () => { default: [MOCK_TITLE], }, directives: { - GlTooltip: createMockDirective(), - GlResizeObserver: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), + GlResizeObserver: createMockDirective('gl-resize-observer'), }, ...options, }); @@ -42,8 +42,8 @@ describe('TooltipOnTruncate component', () => { ...TooltipOnTruncate, directives: { ...TooltipOnTruncate.directives, - GlTooltip: createMockDirective(), - GlResizeObserver: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), + GlResizeObserver: createMockDirective('gl-resize-observer'), }, }; @@ -78,10 +78,6 @@ describe('TooltipOnTruncate component', () => { await nextTick(); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when truncated', () => { beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(true); @@ -90,7 +86,7 @@ describe('TooltipOnTruncate component', () => { it('renders tooltip', async () => { expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); - expect(getTooltipValue()).toMatchObject({ + expect(getTooltipValue()).toStrictEqual({ title: MOCK_TITLE, placement: 'top', disabled: false, @@ -144,7 +140,7 @@ describe('TooltipOnTruncate component', () => { await nextTick(); - expect(getTooltipValue()).toMatchObject({ + expect(getTooltipValue()).toStrictEqual({ title: MOCK_TITLE, placement: 'top', disabled: false, @@ -194,20 +190,22 @@ describe('TooltipOnTruncate component', () => { }); }); - describe('placement', () => { - it('sets placement when tooltip is rendered', () => { - const mockPlacement = 'bottom'; - + describe('tooltip customization', () => { + it.each` + property | mockValue + ${'placement'} | ${'bottom'} + ${'boundary'} | ${'viewport'} + `('sets $property when the tooltip is rendered', ({ property, mockValue }) => { hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { - placement: mockPlacement, + [property]: mockValue, }, }); expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); expect(getTooltipValue()).toMatchObject({ - placement: mockPlacement, + [property]: mockValue, }); }); }); diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap index f9d615d4f68..c816fe790a8 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap +++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap @@ -5,7 +5,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div @@ -86,7 +86,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div @@ -171,7 +171,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div @@ -256,7 +256,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div @@ -342,7 +342,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div @@ -428,7 +428,7 @@ exports[`Upload dropzone component when dragging renders correct template when d class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div @@ -514,7 +514,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon class="gl-w-full gl-relative" > <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" > <div 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 a063a5591e3..24f96195e05 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 @@ -3,8 +3,6 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; -jest.mock('~/flash'); - describe('Upload dropzone component', () => { let wrapper; @@ -34,11 +32,6 @@ describe('Upload dropzone component', () => { }); } - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when slot provided', () => { it('renders dropzone with slot content', () => { createComponent({ diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js index 30a7439579f..2718be74111 100644 --- a/spec/frontend/vue_shared/components/url_sync_spec.js +++ b/spec/frontend/vue_shared/components/url_sync_spec.js @@ -33,10 +33,6 @@ describe('url sync component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const expectUrlSyncWithMergeUrlParams = ( query, times, diff --git a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js index 662c09d02bf..ba55df5512f 100644 --- a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js +++ b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js @@ -24,10 +24,6 @@ describe('usage banner', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe.each` slotName | finderFunction ${'left-primary-text'} | ${findLeftPrimaryTextSlot} 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 d63b13981ac..3ae3d89af27 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 @@ -20,10 +20,6 @@ describe('User Avatar Image Component', () => { const findAvatar = () => wrapper.findComponent(GlAvatar); - afterEach(() => { - wrapper.destroy(); - }); - describe('Initialization', () => { beforeEach(() => { wrapper = shallowMount(UserAvatarImage, { 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 df7ce449678..90f9156af38 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 @@ -34,10 +34,6 @@ describe('User Avatar Link Component', () => { createWrapper(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('should render GlLink with correct props', () => { const link = wrapper.findComponent(GlAvatarLink); expect(link.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 63371b1492b..1754292cb63 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 @@ -50,10 +50,6 @@ describe('UserAvatarList', () => { props = { imgSize: TEST_IMAGE_SIZE }; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('empty text', () => { it('shows when items are empty', () => { factory({ propsData: { items: [] } }); diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js index 521744154ba..b04e578c931 100644 --- a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js +++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js @@ -28,8 +28,6 @@ const initialSlotProps = (changes = {}) => ({ }); describe('UserCalloutDismisser', () => { - let wrapper; - const MOCK_FEATURE_NAME = 'mock_feature_name'; // Query handlers @@ -52,7 +50,7 @@ describe('UserCalloutDismisser', () => { const callDismissSlotProp = () => defaultScopedSlotSpy.mock.calls[0][0].dismiss(); const createComponent = ({ queryHandler, mutationHandler, ...options }) => { - wrapper = mount( + mount( UserCalloutDismisser, merge( { @@ -72,10 +70,6 @@ describe('UserCalloutDismisser', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when loading', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js index 78abb89e7b8..6491e5a66cd 100644 --- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js +++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js @@ -51,10 +51,6 @@ describe('User deletion obstacles list', () => { ); } - afterEach(() => { - wrapper.destroy(); - }); - const findLinks = () => wrapper.findAllComponents(GlLink); const findTitle = () => wrapper.findByTestId('title'); const findFooter = () => wrapper.findByTestId('footer'); 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 f6316af6ad8..8ecab5cc043 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 @@ -13,11 +13,11 @@ import { I18N_ERROR_UNFOLLOW, } from '~/vue_shared/components/user_popover/constants'; import axios from '~/lib/utils/axios_utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { followUser, unfollowUser } from '~/api/user_api'; import { mockTracking } from 'helpers/tracking_helper'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/api/user_api', () => ({ followUser: jest.fn(), unfollowUser: jest.fn(), @@ -51,7 +51,6 @@ describe('User Popover Component', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index b0e9584a15b..e881bfed35e 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql'; import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; -import { IssuableType } from '~/issues/constants'; +import { TYPE_MERGE_REQUEST } 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 '~/sidebar/queries/get_issue_participants.query.graphql'; @@ -105,7 +105,6 @@ describe('User select dropdown', () => { }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -409,7 +408,7 @@ describe('User select dropdown', () => { describe('when on merge request sidebar', () => { beforeEach(() => { - createComponent({ props: { issuableType: IssuableType.MergeRequest, issuableId: 1 } }); + createComponent({ props: { issuableType: TYPE_MERGE_REQUEST, issuableId: 1 } }); return waitForPromises(); }); diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js index c136c2054ac..acbb931b7b6 100644 --- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js +++ b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js @@ -27,10 +27,6 @@ describe('~/vue_shared/components/vuex_module_provider', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('provides "vuexModule" set from prop', () => { createComponent(); expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE); 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 18afe049149..f6eb11aaddf 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -137,10 +137,6 @@ describe('Web IDE link component', () => { localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, 'true'); }); - afterEach(() => { - wrapper.destroy(); - }); - const findActionsButton = () => wrapper.findComponent(ActionsButton); const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const findModal = () => wrapper.findComponent(GlModal); diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js index dcd3a44a6fc..72a348c1a79 100644 --- a/spec/frontend/vue_shared/directives/validation_spec.js +++ b/spec/frontend/vue_shared/directives/validation_spec.js @@ -80,11 +80,6 @@ describe('validation directive', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const getFormData = () => wrapper.vm.form; const findForm = () => wrapper.find('form'); const findInput = () => wrapper.find('input'); 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 7b0f0f7e344..e983519d9fc 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 @@ -34,10 +34,6 @@ describe('IssuableCreateRoot', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { it('renders component container element with class "issuable-create-container"', () => { expect(wrapper.classes()).toContain('issuable-create-container'); 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 ff21b3bc356..ae2fd5ebffa 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 @@ -36,10 +36,6 @@ describe('IssuableForm', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('methods', () => { describe('handleUpdateSelectedLabels', () => { it('sets provided `labels` param to prop `selectedLabels`', () => { diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js index 76b6efa15b6..ce9e23d9a00 100644 --- a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js +++ b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js @@ -6,11 +6,8 @@ import { } from 'jest/sidebar/components/labels/labels_select_widget/mock_data'; import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue'; import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue'; -import { - DropdownVariant, - LabelType, -} from '~/sidebar/components/labels/labels_select_widget/constants'; -import { WorkspaceType } from '~/issues/constants'; +import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants'; +import { WORKSPACE_PROJECT } from '~/issues/constants'; import { __ } from '~/locale'; const allowLabelRemove = true; @@ -20,9 +17,9 @@ const fullPath = '/full-path'; const labelsFilterBasePath = '/labels-filter-base-path'; const initialLabels = []; const issuableType = 'issue'; -const labelType = LabelType.project; +const labelType = WORKSPACE_PROJECT; const variant = DropdownVariant.Embedded; -const workspaceType = WorkspaceType.project; +const workspaceType = WORKSPACE_PROJECT; describe('IssuableLabelSelector', () => { let wrapper; @@ -50,10 +47,6 @@ describe('IssuableLabelSelector', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const expectTitleWithCount = (count) => { const title = findTitle(); diff --git a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js index a0b1d64b97c..61e6d2a420a 100644 --- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js +++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js @@ -7,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue'; import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants'; -import { issuableTypes } from '~/boards/constants'; -import { TYPE_ISSUE } from '~/issues/constants'; +import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import { truncate } from '~/lib/utils/text_utility'; import { mockIssue, @@ -49,11 +48,6 @@ describe('IssuableBlockedIcon', () => { await waitForApollo(); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const createWrapperWithApollo = ({ item = mockBlockedIssue1, blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1), @@ -121,9 +115,9 @@ describe('IssuableBlockedIcon', () => { }; it.each` - mockIssuable | issuableType | expectedIcon - ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'} - ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'} + mockIssuable | issuableType | expectedIcon + ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'} + ${mockEpic} | ${TYPE_EPIC} | ${'entity-blocked'} `( 'should render blocked icon for $issuableType', ({ mockIssuable, issuableType, expectedIcon }) => { @@ -153,9 +147,9 @@ describe('IssuableBlockedIcon', () => { describe('on mouseenter on blocked icon', () => { it.each` - item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy - ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)} - ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)} + item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy + ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)} + ${mockBlockedEpic1} | ${TYPE_EPIC} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)} `( 'should query for blocking issuables and render the result for $issuableType', async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => { diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js index a25f92c9cf2..c23bd002ee5 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js @@ -28,7 +28,6 @@ describe('IssuableBulkEditSidebar', () => { }); afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); 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 2fac004875a..45daf0dc34b 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 @@ -39,7 +39,6 @@ describe('IssuableItem', () => { const mockLabels = mockIssuable.labels.nodes; const mockAuthor = mockIssuable.author; - const originalUrl = gon.gitlab_url; let wrapper; const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]'); @@ -49,11 +48,6 @@ describe('IssuableItem', () => { gon.gitlab_url = MOCK_GITLAB_URL; }); - afterEach(() => { - wrapper.destroy(); - gon.gitlab_url = originalUrl; - }); - describe('computed', () => { describe('author', () => { it('returns `issuable.author` reference', () => { 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 371844e66f4..9a4636e0f4d 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 @@ -47,10 +47,6 @@ describe('IssuableListRoot', () => { const findVueDraggable = () => wrapper.findComponent(VueDraggable); const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector); - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { beforeEach(() => { wrapper = createComponent(); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js index 27985895c62..9cdd4d75c42 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js @@ -35,7 +35,6 @@ describe('IssuableTabs', () => { afterEach(() => { setLanguage(null); - wrapper.destroy(); }); const findAllGlBadges = () => wrapper.findAllComponents(GlBadge); 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 6b20f0c77a3..7e665b7c76e 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 @@ -13,7 +13,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { mockIssuableShowProps, mockIssuable } from '../mock_data'; jest.mock('~/autosave'); -jest.mock('~/flash'); +jest.mock('~/alert'); const issuableBodyProps = { ...mockIssuableShowProps, @@ -48,10 +48,6 @@ describe('IssuableBody', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('computed', () => { describe('isUpdated', () => { it.each` diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js index ea58cc2baf5..b4f1c286158 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js @@ -24,10 +24,6 @@ describe('IssuableDescription', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('mounted', () => { it('calls `renderGFM`', () => { expect(renderGFM).toHaveBeenCalledTimes(1); 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 159be4cd1ef..0d6cd1ad00b 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 @@ -43,6 +43,9 @@ describe('IssuableEditForm', () => { }); afterEach(() => { + // note: the order of wrapper.destroy() and jest.resetAllMocks() matters. + // maybe it'll help with investigation on how to remove this wrapper.destroy() call + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy wrapper.destroy(); jest.resetAllMocks(); }); 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 6a8b9ef77a9..d9f1b6c15a8 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 @@ -33,7 +33,6 @@ describe('IssuableHeader', () => { }; afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); 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 edfd55c8bb4..f976e0499f0 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 @@ -41,10 +41,6 @@ describe('IssuableShowRoot', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('template', () => { const { statusIcon, diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js index 6f62fb77353..39316dfa249 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js @@ -22,35 +22,35 @@ const createComponent = (propsData = issuableTitleProps) => 'status-badge': 'Open', }, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); describe('IssuableTitle', () => { let wrapper; + const findStickyHeader = () => wrapper.findComponent('[data-testid="header"]'); + beforeEach(() => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('methods', () => { describe('handleTitleAppear', () => { - it('sets value of `stickyTitleVisible` prop to false', () => { + it('sets value of `stickyTitleVisible` prop to false', async () => { wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); + await nextTick(); - expect(wrapper.vm.stickyTitleVisible).toBe(false); + expect(findStickyHeader().exists()).toBe(false); }); }); describe('handleTitleDisappear', () => { - it('sets value of `stickyTitleVisible` prop to true', () => { + it('sets value of `stickyTitleVisible` prop to true', async () => { wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); + await nextTick(); - expect(wrapper.vm.stickyTitleVisible).toBe(true); + expect(findStickyHeader().exists()).toBe(true); }); }); }); @@ -87,14 +87,10 @@ describe('IssuableTitle', () => { }); it('renders sticky header when `stickyTitleVisible` prop is true', 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({ - stickyTitleVisible: true, - }); - + wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); await nextTick(); - const stickyHeaderEl = wrapper.find('[data-testid="header"]'); + + const stickyHeaderEl = findStickyHeader(); expect(stickyHeaderEl.exists()).toBe(true); expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success'); diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js index 6c9e5f85fa0..f2509aead77 100644 --- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js +++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js @@ -38,7 +38,6 @@ describe('IssuableSidebarRoot', () => { }; afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); }); diff --git a/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js index 52f36aa0e77..052ff518468 100644 --- a/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js +++ b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js @@ -11,9 +11,7 @@ describe('Legacy container component', () => { }; afterEach(() => { - wrapper.destroy(); resetHTMLFixture(); - wrapper = null; }); describe('when selector targets real node', () => { diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js index c90131fea9a..cc8a8d86d19 100644 --- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js +++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js @@ -27,9 +27,7 @@ describe('Welcome page', () => { }); afterEach(() => { - wrapper.destroy(); window.location.hash = ''; - wrapper = null; }); it('tracks link clicks', async () => { diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js index 6115dc6e61b..5ff7b9f390a 100644 --- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js +++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js @@ -14,7 +14,7 @@ describe('Experimental new project creation app', () => { const DEFAULT_PROPS = { title: 'Create something', - initialBreadcrumb: 'Something', + initialBreadcrumbs: [{ text: 'Something', href: '#' }], panels: [ { name: 'panel1', selector: '#some-selector1' }, { name: 'panel2', selector: '#some-selector2' }, @@ -33,7 +33,6 @@ describe('Experimental new project creation app', () => { }; afterEach(() => { - wrapper.destroy(); window.location.hash = ''; }); @@ -46,8 +45,8 @@ describe('Experimental new project creation app', () => { expect(findWelcomePage().exists()).toBe(true); }); - it('does not render breadcrumbs', () => { - expect(findBreadcrumb().exists()).toBe(false); + it('renders breadcrumbs', () => { + expect(findBreadcrumb().exists()).toBe(true); }); }); @@ -75,7 +74,7 @@ describe('Experimental new project creation app', () => { it('renders breadcrumbs', () => { const breadcrumb = findBreadcrumb(); expect(breadcrumb.exists()).toBe(true); - expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumb); + expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumbs[0].text); }); }); diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js index 322586a772c..0bf2737fb2b 100644 --- a/spec/frontend/vue_shared/plugins/global_toast_spec.js +++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js @@ -1,14 +1,16 @@ -import toast, { instance } from '~/vue_shared/plugins/global_toast'; +import toast from '~/vue_shared/plugins/global_toast'; -describe('Global toast', () => { - let spyFunc; - - beforeEach(() => { - spyFunc = jest.spyOn(instance.$toast, 'show').mockImplementation(() => {}); - }); +const mockSpy = jest.fn(); +jest.mock('@gitlab/ui', () => ({ + GlToast: (Vue) => { + // eslint-disable-next-line no-param-reassign + Vue.prototype.$toast = { show: (...args) => mockSpy(...args) }; + }, +})); +describe('Global toast', () => { afterEach(() => { - spyFunc.mockRestore(); + mockSpy.mockRestore(); }); it("should call GitLab UI's toast method", () => { @@ -17,7 +19,7 @@ describe('Global toast', () => { toast(arg1, arg2); - expect(instance.$toast.show).toHaveBeenCalledTimes(1); - expect(instance.$toast.show).toHaveBeenCalledWith(arg1, arg2); + expect(mockSpy).toHaveBeenCalledTimes(1); + expect(mockSpy).toHaveBeenCalledWith(arg1, arg2); }); }); diff --git a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js index 136fe74b0d6..d258658d5e2 100644 --- a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js +++ b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js @@ -21,10 +21,6 @@ describe('Section Layout component', () => { const findHeading = () => wrapper.find('h2'); const findLoader = () => wrapper.findComponent(SectionLoader); - afterEach(() => { - wrapper.destroy(); - }); - describe('basic structure', () => { beforeEach(() => { createComponent({ heading: 'testheading' }); diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js index 0a5e46d9263..6345393951c 100644 --- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js +++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js @@ -56,10 +56,6 @@ describe('ManageViaMr component', () => { ); } - afterEach(() => { - wrapper.destroy(); - }); - // This component supports different report types/mutations depending on // whether it's in a CE or EE context. This makes sure we are only testing // the ones available in the current test context. 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 5f2b13a79c9..299a3d62421 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 @@ -15,11 +15,6 @@ describe('SecurityReportDownloadDropdown component', () => { const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('given report artifacts', () => { beforeEach(() => { artifacts = [ 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 221da35de3d..257f59612e8 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 @@ -14,7 +14,7 @@ import { sastDiffSuccessMock, secretDetectionDiffSuccessMock, } from 'jest/vue_shared/security_reports/mock_data'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue'; @@ -26,7 +26,7 @@ import { import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue'; -jest.mock('~/flash'); +jest.mock('~/alert'); Vue.use(VueApollo); Vue.use(Vuex); @@ -74,10 +74,6 @@ describe('Security reports app', () => { const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); const findHelpIconComponent = () => wrapper.findComponent(HelpIcon); - afterEach(() => { - wrapper.destroy(); - }); - describe('given the artifacts query is loading', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js index 45a39d2dd58..cbeff184e9d 100644 --- a/spec/frontend/webhooks/components/form_url_app_spec.js +++ b/spec/frontend/webhooks/components/form_url_app_spec.js @@ -19,10 +19,6 @@ describe('FormUrlApp', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findAllRadioButtons = () => wrapper.findAllComponents(GlFormRadio); const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); const findUrlMaskDisable = () => findAllRadioButtons().at(0); diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js index ee15034daff..000b07f4dfd 100644 --- a/spec/frontend/whats_new/components/app_spec.js +++ b/spec/frontend/whats_new/components/app_spec.js @@ -49,7 +49,7 @@ describe('App', () => { store, propsData: buildProps(), directives: { - GlResizeObserver: createMockDirective(), + GlResizeObserver: createMockDirective('gl-resize-observer'), }, }); }; @@ -71,7 +71,6 @@ describe('App', () => { }; afterEach(() => { - wrapper.destroy(); unmockTracking(); }); diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js index 099054bf8ca..d69ac2803df 100644 --- a/spec/frontend/whats_new/components/feature_spec.js +++ b/spec/frontend/whats_new/components/feature_spec.js @@ -30,11 +30,6 @@ describe("What's new single feature", () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('renders the date', () => { createWrapper({ feature: exampleFeature }); diff --git a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js index b199f4f0c49..79717b8767e 100644 --- a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js +++ b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js @@ -11,10 +11,6 @@ describe('~/whats_new/utils/get_drawer_body_height', () => { }); }); - afterEach(() => { - drawerWrapper.destroy(); - }); - const setClientHeight = (el, height) => { Object.defineProperty(el, 'clientHeight', { get() { diff --git a/spec/frontend/work_items/components/app_spec.js b/spec/frontend/work_items/components/app_spec.js index 95034085493..d799e8042b1 100644 --- a/spec/frontend/work_items/components/app_spec.js +++ b/spec/frontend/work_items/components/app_spec.js @@ -12,10 +12,6 @@ describe('Work Items Application', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders a component', () => { createComponent(); diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js index c3cc2fbc556..c3bdbfe030e 100644 --- a/spec/frontend/work_items/components/item_state_spec.js +++ b/spec/frontend/work_items/components/item_state_spec.js @@ -21,10 +21,6 @@ describe('ItemState', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders label and dropdown', () => { createComponent(); diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index 6361f8dafc4..aef310319ab 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -19,10 +19,6 @@ describe('ItemTitle', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders title contents', () => { expect(findInputEl().attributes()).toMatchObject({ 'data-placeholder': 'Add a title...', diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap index 5901642b8a1..1c01451f047 100644 --- a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap +++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\"></note-header-stub>"`; +exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\" noteurl=\\"\\"></note-header-stub>"`; diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js deleted file mode 100644 index eb4bcbf942b..00000000000 --- a/spec/frontend/work_items/components/notes/activity_filter_spec.js +++ /dev/null @@ -1,74 +0,0 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { ASC, DESC } from '~/notes/constants'; - -import { mockTracking } from 'helpers/tracking_helper'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; - -describe('Activity Filter', () => { - let wrapper; - - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first'); - - const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => { - wrapper = shallowMountExtended(ActivityFilter, { - propsData: { - sortOrder, - loading, - workItemType, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - describe('default', () => { - it('has a dropdown with 2 options', () => { - expect(findDropdown().exists()).toBe(true); - expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length); - }); - - it('has local storage sync with the correct props', () => { - expect(findLocalStorageSync().props('asString')).toBe(true); - }); - - it('emits `updateSavedSortOrder` event when update is emitted', async () => { - findLocalStorageSync().vm.$emit('input', ASC); - - await nextTick(); - expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1); - expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]); - }); - }); - - describe('when asc', () => { - describe('when the dropdown is clicked', () => { - it('calls the right actions', async () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - findNewestFirstItem().vm.$emit('click'); - await nextTick(); - - expect(wrapper.emitted('changeSortOrder')).toHaveLength(1); - expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]); - - expect(trackingSpy).toHaveBeenCalledWith( - TRACKING_CATEGORY_SHOW, - 'notes_sort_order_changed', - { - category: TRACKING_CATEGORY_SHOW, - label: 'item_track_notes_sorting', - property: 'type_Task', - }, - ); - }); - }); - }); -}); diff --git a/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js new file mode 100644 index 00000000000..5ed9d581446 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js @@ -0,0 +1,109 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemActivitySortFilter from '~/work_items/components/notes/work_item_activity_sort_filter.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { ASC, DESC } from '~/notes/constants'; +import { + WORK_ITEM_ACTIVITY_SORT_OPTIONS, + WORK_ITEM_NOTES_SORT_ORDER_KEY, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + WORK_ITEM_NOTES_FILTER_KEY, + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_ACTIVITY_FILTER_OPTIONS, + TRACKING_CATEGORY_SHOW, +} from '~/work_items/constants'; + +import { mockTracking } from 'helpers/tracking_helper'; + +describe('Work Item Activity/Discussions Filtering', () => { + let wrapper; + + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findByDataTestId = (dataTestId) => wrapper.findByTestId(dataTestId); + + const createComponent = ({ + loading = false, + workItemType = 'Task', + sortFilterProp = ASC, + filterOptions = WORK_ITEM_ACTIVITY_SORT_OPTIONS, + trackingLabel = 'item_track_notes_sorting', + trackingAction = 'work_item_notes_sort_order_changed', + filterEvent = 'changeSort', + defaultSortFilterProp = ASC, + storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY, + } = {}) => { + wrapper = shallowMountExtended(WorkItemActivitySortFilter, { + propsData: { + loading, + workItemType, + sortFilterProp, + filterOptions, + trackingLabel, + trackingAction, + filterEvent, + defaultSortFilterProp, + storageKey, + }, + }); + }; + + describe.each` + usedFor | filterOptions | storageKey | filterEvent | newInputOption | trackingLabel | trackingAction | defaultSortFilterProp | sortFilterProp | nonDefaultDataTestId + ${'Sorting'} | ${WORK_ITEM_ACTIVITY_SORT_OPTIONS} | ${WORK_ITEM_NOTES_SORT_ORDER_KEY} | ${'changeSort'} | ${DESC} | ${'item_track_notes_sorting'} | ${'work_item_notes_sort_order_changed'} | ${ASC} | ${ASC} | ${'newest-first'} + ${'Filtering'} | ${WORK_ITEM_ACTIVITY_FILTER_OPTIONS} | ${WORK_ITEM_NOTES_FILTER_KEY} | ${'changeFilter'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${'item_track_notes_sorting'} | ${'work_item_notes_filter_changed'} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${'comments-activity'} + `( + 'When used for $usedFor', + ({ + filterOptions, + storageKey, + filterEvent, + trackingLabel, + trackingAction, + newInputOption, + defaultSortFilterProp, + sortFilterProp, + nonDefaultDataTestId, + }) => { + beforeEach(() => { + createComponent({ + sortFilterProp, + filterOptions, + trackingLabel, + trackingAction, + filterEvent, + defaultSortFilterProp, + storageKey, + }); + }); + + it('has a dropdown with options equal to the length of `filterOptions`', () => { + expect(findDropdown().exists()).toBe(true); + expect(findAllDropdownItems()).toHaveLength(filterOptions.length); + }); + + it('has local storage sync with the correct props', () => { + expect(findLocalStorageSync().props('asString')).toBe(true); + expect(findLocalStorageSync().props('storageKey')).toBe(storageKey); + }); + + it(`emits ${filterEvent} event when local storage input is emitted`, () => { + findLocalStorageSync().vm.$emit('input', newInputOption); + + expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]); + }); + + it('emits tracking event when the a non default dropdown item is clicked', () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + findByDataTestId(nonDefaultDataTestId).vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, trackingAction, { + category: TRACKING_CATEGORY_SHOW, + label: trackingLabel, + property: 'type_Task', + }); + }); + }, + ); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js index bb65b75c4d8..6b95da0910b 100644 --- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js @@ -1,7 +1,5 @@ -import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; import WorkItemNote from '~/work_items/components/notes/work_item_note.vue'; @@ -21,9 +19,6 @@ describe('Work Item Discussion', () => { let wrapper; const mockWorkItemId = 'gid://gitlab/WorkItem/625'; - const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem); - const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); - const findAvatar = () => wrapper.findComponent(GlAvatar); const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget); const findAllThreads = () => wrapper.findAllComponents(WorkItemNote); const findThreadAtIndex = (index) => findAllThreads().at(index); @@ -55,19 +50,6 @@ describe('Work Item Discussion', () => { createComponent(); }); - it('Should be wrapped inside the timeline entry item', () => { - expect(findTimelineEntryItem().exists()).toBe(true); - }); - - it('should have the author avatar of the work item note', () => { - expect(findAvatarLink().exists()).toBe(true); - expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl); - - expect(findAvatar().exists()).toBe(true); - expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl); - expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username); - }); - it('should not show the the toggle replies widget wrapper when no replies', () => { expect(findToggleRepliesWidget().exists()).toBe(false); }); @@ -89,13 +71,17 @@ describe('Work Item Discussion', () => { }); it('the number of threads should be equal to the response length', async () => { - findToggleRepliesWidget().vm.$emit('toggle'); - await nextTick(); expect(findAllThreads()).toHaveLength( mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length, ); }); + it('should collapse when we click on toggle replies widget', async () => { + findToggleRepliesWidget().vm.$emit('toggle'); + await nextTick(); + expect(findAllThreads()).toHaveLength(1); + }); + it('should autofocus when we click expand replies', async () => { const mainComment = findThreadAtIndex(0); diff --git a/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js new file mode 100644 index 00000000000..339efad0608 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js @@ -0,0 +1,44 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, +} from '~/work_items/constants'; + +describe('Work Item History Filter note', () => { + let wrapper; + + const findShowAllActivityButton = () => wrapper.findByTestId('show-all-activity'); + const findShowCommentsButton = () => wrapper.findByTestId('show-comments-only'); + + const createComponent = () => { + wrapper = shallowMountExtended(WorkItemHistoryOnlyFilterNote, { + stubs: { + GlSprintf, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('timelineContent renders a string containing instruction for switching feed type', () => { + expect(wrapper.text()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('emits `changeFilter` event with 0 parameter on clicking Show all activity button', () => { + findShowAllActivityButton().vm.$emit('click'); + + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ALL_NOTES]]); + }); + + it('emits `changeFilter` event with 1 parameter on clicking Show comments only button', () => { + findShowCommentsButton().vm.$emit('click'); + + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS]]); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js index d85cd46c1c3..b293127b6af 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js @@ -1,52 +1,116 @@ import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import EmojiPicker from '~/emoji/components/picker.vue'; +import waitForPromises from 'helpers/wait_for_promises'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue'; +import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; + +Vue.use(VueApollo); describe('Work Item Note Actions', () => { let wrapper; + const noteId = '1'; const findReplyButton = () => wrapper.findComponent(ReplyButton); const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]'); + const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]'); + + const addEmojiMutationResolver = jest.fn().mockResolvedValue({ + data: { + errors: [], + }, + }); + + const EmojiPickerStub = { + props: EmojiPicker.props, + template: '<div></div>', + }; - const createComponent = ({ showReply = true, showEdit = true } = {}) => { + const createComponent = ({ showReply = true, showEdit = true, showAwardEmoji = true } = {}) => { wrapper = shallowMount(WorkItemNoteActions, { propsData: { showReply, showEdit, + noteId, + showAwardEmoji, + }, + provide: { + glFeatures: { + workItemsMvc2: true, + }, }, + stubs: { + EmojiPicker: EmojiPickerStub, + }, + apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]), }); }; - describe('Default', () => { - it('Should show the reply button by default', () => { + describe('reply button', () => { + it('is visible by default', () => { createComponent(); + expect(findReplyButton().exists()).toBe(true); }); - }); - describe('When the reply button needs to be hidden', () => { - it('Should show the reply button by default', () => { + it('is hidden when showReply false', () => { createComponent({ showReply: false }); + expect(findReplyButton().exists()).toBe(false); }); }); - it('shows edit button when `showEdit` prop is true', () => { - createComponent(); + describe('edit button', () => { + it('is visible when `showEdit` prop is true', () => { + createComponent(); - expect(findEditButton().exists()).toBe(true); - }); + expect(findEditButton().exists()).toBe(true); + }); + + it('is hidden when `showEdit` prop is false', () => { + createComponent({ showEdit: false }); + + expect(findEditButton().exists()).toBe(false); + }); - it('does not show edit button when `showEdit` prop is false', () => { - createComponent({ showEdit: false }); + it('emits `startEditing` event when clicked', () => { + createComponent(); + findEditButton().vm.$emit('click'); - expect(findEditButton().exists()).toBe(false); + expect(wrapper.emitted('startEditing')).toEqual([[]]); + }); }); - it('emits `startEditing` event when edit button is clicked', () => { - createComponent(); - findEditButton().vm.$emit('click'); + describe('emoji picker', () => { + it('is visible when `showAwardEmoji` prop is true', () => { + createComponent(); + + expect(findEmojiButton().exists()).toBe(true); + }); + + it('is hidden when `showAwardEmoji` prop is false', () => { + createComponent({ showAwardEmoji: false }); - expect(wrapper.emitted('startEditing')).toEqual([[]]); + expect(findEmojiButton().exists()).toBe(false); + }); + + it('commits mutation on click', async () => { + const awardName = 'carrot'; + + createComponent(); + + findEmojiButton().vm.$emit('click', awardName); + + await waitForPromises(); + + expect(findEmojiButton().emitted('errors')).toEqual(undefined); + expect(addEmojiMutationResolver).toHaveBeenCalledWith({ + awardableId: noteId, + name: awardName, + }); + }); }); }); diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index 9b87419cee7..8e574dc1a81 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -198,10 +198,6 @@ describe('Work Item Note', () => { expect(findNoteActions().exists()).toBe(true); }); - it('should not have the Avatar link for main thread inside the timeline-entry', () => { - expect(findAuthorAvatarLink().exists()).toBe(false); - }); - it('should have the reply button props', () => { expect(findNoteActions().props('showReply')).toBe(true); }); @@ -228,7 +224,7 @@ describe('Work Item Note', () => { }); }); - it('should display a dropdown if user has a permission to delete a note', () => { + it('should display the `Delete comment` dropdown item if user has a permission to delete a note', () => { createComponent({ note: { ...mockWorkItemCommentNote, @@ -237,12 +233,14 @@ describe('Work Item Note', () => { }); expect(findDropdown().exists()).toBe(true); + expect(findDeleteNoteButton().exists()).toBe(true); }); - it('should not display a dropdown if user has no permission to delete a note', () => { + it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => { createComponent(); - expect(findDropdown().exists()).toBe(false); + expect(findDropdown().exists()).toBe(true); + expect(findDeleteNoteButton().exists()).toBe(false); }); it('should emit `deleteNote` event when delete note action is clicked', () => { diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js new file mode 100644 index 00000000000..daf74f7a93b --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js @@ -0,0 +1,63 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; +import { ASC } from '~/notes/constants'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, +} from '~/work_items/constants'; + +describe('Work Item Note Activity Header', () => { + let wrapper; + + const findActivityLabelHeading = () => wrapper.find('h3'); + const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter'); + const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort'); + + const createComponent = ({ + disableActivityFilterSort = false, + sortOrder = ASC, + workItemType = 'Task', + discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES, + } = {}) => { + wrapper = shallowMountExtended(WorkItemNotesActivityHeader, { + propsData: { + disableActivityFilterSort, + sortOrder, + workItemType, + discussionFilter, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('Should have the Activity label', () => { + expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel); + }); + + it('Should have Activity filtering dropdown', () => { + expect(findActivityFilterDropdown().exists()).toBe(true); + }); + + it('Should have Activity sorting dropdown', () => { + expect(findActivitySortDropdown().exists()).toBe(true); + }); + + describe('Activity Filter', () => { + it('emits `changeFilter` when filtering discussions', () => { + findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY); + + expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]); + }); + }); + + describe('Activity Sorting', () => { + it('emits `changeSort` when sorting discussions/activity', () => { + findActivitySortDropdown().vm.$emit('changeSort', ASC); + + expect(wrapper.emitted('changeSort')).toEqual([[ASC]]); + }); + }); +}); 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 3c312fb4552..a0db8172bf6 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -52,10 +52,6 @@ describe('WorkItemActions component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders modal', () => { createComponent(); 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 e85f62b881d..2a8159f7294 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -113,10 +113,6 @@ describe('WorkItemAssignees component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('passes the correct data-user-id attribute', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js index 0ab2546440b..4f1d49ee2e5 100644 --- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js +++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js @@ -29,10 +29,6 @@ describe('WorkItemDescription', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders gfm', async () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index a12ec23c15a..b4b7b8989ea 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -99,10 +99,6 @@ describe('WorkItemDescription', () => { } }; - afterEach(() => { - wrapper.destroy(); - }); - describe('editing description with workItemsMvc FF enabled', () => { beforeEach(() => { workItemsMvc = true; @@ -117,11 +113,14 @@ describe('WorkItemDescription', () => { await createComponent({ isEditing: true }); expect(findMarkdownEditor().props()).toMatchObject({ - autocompleteDataSources: autocompleteDataSources(fullPath, iid), supportsQuickActions: true, renderMarkdownPath: markdownPreviewPath(fullPath, iid), quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath, }); + + expect(findMarkdownEditor().vm.$attrs).toMatchObject({ + 'autocomplete-data-sources': autocompleteDataSources(fullPath, iid), + }); }); }); 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 938cf6e6f51..1bdf5d1c840 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 @@ -82,10 +82,6 @@ describe('WorkItemDetailModal component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders WorkItemDetail', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 64a7502671e..fe7556f8ec6 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -99,9 +99,7 @@ describe('WorkItemDetail component', () => { subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], error = undefined, - workItemsMvcEnabled = false, workItemsMvc2Enabled = false, - fetchByIid = false, } = {}) => { const handlers = [ [workItemQuery, handler], @@ -124,9 +122,7 @@ describe('WorkItemDetail component', () => { }, provide: { glFeatures: { - workItemsMvc: workItemsMvcEnabled, workItemsMvc2: workItemsMvc2Enabled, - useIidInWorkItemsPath: fetchByIid, }, hasIssueWeightsFeature: true, hasIterationsFeature: true, @@ -149,7 +145,6 @@ describe('WorkItemDetail component', () => { }; afterEach(() => { - wrapper.destroy(); setWindowLocation(''); }); @@ -420,6 +415,12 @@ describe('WorkItemDetail component', () => { expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName); }); + it('shows parent title and iid', () => { + expect(findParentButton().text()).toBe( + `${mockParent.parent.title} #${mockParent.parent.iid}`, + ); + }); + it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => { expect(findParentButton().attributes().href).toBe('../../issues/5'); }); @@ -441,6 +442,11 @@ describe('WorkItemDetail component', () => { expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl); }); + + it('shows work item type and iid', () => { + const { iid, workItemType } = workItemQueryResponse.data.workItem; + expect(findParent().text()).toContain(`${workItemType.name} #${iid}`); + }); }); }); @@ -626,7 +632,7 @@ describe('WorkItemDetail component', () => { }); }); - it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => { + it('calls the global ID work item query when there is no `iid_path` parameter in URL', async () => { createComponent(); await waitForPromises(); @@ -636,20 +642,10 @@ describe('WorkItemDetail component', () => { expect(successByIidHandler).not.toHaveBeenCalled(); }); - it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => { - createComponent({ fetchByIid: true }); - await waitForPromises(); - - expect(successHandler).toHaveBeenCalledWith({ - id: workItemQueryResponse.data.workItem.id, - }); - expect(successByIidHandler).not.toHaveBeenCalled(); - }); - - it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => { + it('calls the IID work item query when `iid_path` route parameter is present', async () => { setWindowLocation(`?iid_path=true`); - createComponent({ fetchByIid: true, iidPathQueryParam: 'true' }); + createComponent(); await waitForPromises(); expect(successHandler).not.toHaveBeenCalled(); @@ -659,10 +655,10 @@ describe('WorkItemDetail component', () => { }); }); - it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present and is a modal', async () => { + it('calls the IID work item query when `iid_path` route parameter is present and is a modal', async () => { setWindowLocation(`?iid_path=true`); - createComponent({ fetchByIid: true, iidPathQueryParam: 'true', isModal: true }); + createComponent({ isModal: true }); await waitForPromises(); expect(successHandler).not.toHaveBeenCalled(); @@ -748,21 +744,10 @@ describe('WorkItemDetail component', () => { }); describe('notes widget', () => { - it('does not render notes by default', async () => { + it('renders notes by default', async () => { createComponent(); await waitForPromises(); - expect(findNotesWidget().exists()).toBe(false); - }); - - it('renders notes when the work_items_mvc flag is on', async () => { - const notesWorkItem = workItemResponseFactory({ - notesWidgetPresent: true, - }); - const handler = jest.fn().mockResolvedValue(notesWorkItem); - createComponent({ workItemsMvcEnabled: true, handler }); - await waitForPromises(); - expect(findNotesWidget().exists()).toBe(true); }); }); diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js index 7ebaf8209c7..b4811db8bed 100644 --- a/spec/frontend/work_items/components/work_item_due_date_spec.js +++ b/spec/frontend/work_items/components/work_item_due_date_spec.js @@ -46,10 +46,6 @@ describe('WorkItemDueDate component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('when can update', () => { describe('start date', () => { describe('`Add start date` button', () => { diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index 0b6ab5c3290..6d51448194b 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -72,10 +72,6 @@ describe('WorkItemLabels component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('has a label', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js index 5fbd8e7e1a7..688dccbda79 100644 --- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js +++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js @@ -15,10 +15,6 @@ describe('RelatedItemsTree', () => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('OkrActionsSplitButton', () => { describe('template', () => { it('renders objective and key results sections', () => { diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index 0470249d7ce..721436e217e 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql'; @@ -31,7 +31,7 @@ import { workItemObjectiveMetadataWidgets, } from '../../mock_data'; -jest.mock('~/flash'); +jest.mock('~/alert'); describe('WorkItemLinkChild', () => { const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; @@ -67,10 +67,6 @@ describe('WorkItemLinkChild', () => { createAlert.mockClear(); }); - afterEach(() => { - wrapper.destroy(); - }); - it.each` status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'} @@ -109,10 +105,30 @@ describe('WorkItemLinkChild', () => { }); it('renders item title', () => { - expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4'); + expect(titleEl.attributes('href')).toBe( + '/gitlab-org/gitlab-test/-/work_items/4?iid_path=true', + ); expect(titleEl.text()).toBe(workItemTask.title); }); + describe('renders item title correctly for relative instance', () => { + beforeEach(() => { + window.gon = { relative_url_root: '/test' }; + createComponent(); + titleEl = wrapper.findByTestId('item-title'); + }); + + it('renders item title with correct href', () => { + expect(titleEl.attributes('href')).toBe( + '/test/gitlab-org/gitlab-test/-/work_items/4?iid_path=true', + ); + }); + + it('renders item title with correct text', () => { + expect(titleEl.text()).toBe(workItemTask.title); + }); + }); + it.each` action | event | emittedEvent ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'} @@ -149,6 +165,8 @@ describe('WorkItemLinkChild', () => { expect(metadataEl.props()).toMatchObject({ metadataWidgets: workItemObjectiveMetadataWidgets, }); + + expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-3'); }); it('does not render item metadata component when item has no metadata present', () => { @@ -158,6 +176,8 @@ describe('WorkItemLinkChild', () => { }); expect(findMetadataComponent().exists()).toBe(false); + + expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-0'); }); }); 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 480f8fbcc58..5184b24d202 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 @@ -75,10 +75,6 @@ describe('WorkItemLinksForm', () => { const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findAddChildButton = () => wrapper.findByTestId('add-child-button'); - afterEach(() => { - wrapper.destroy(); - }); - describe('creating a new work item', () => { beforeEach(async () => { await createComponent(); 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 e3f3b74f296..4e53fc2987b 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 @@ -17,10 +17,6 @@ describe('WorkItemLinksMenu', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders dropdown and dropdown items', () => { expect(findDropdown().exists()).toBe(true); expect(findRemoveDropdownItem().exists()).toBe(true); 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 ec51f92b578..99e44b4d89c 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 @@ -54,7 +54,6 @@ describe('WorkItemLinks', () => { mutationHandler = mutationChangeParentHandler, issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()), hasIterationsFeature = false, - fetchByIid = false, } = {}) => { mockApollo = createMockApollo( [ @@ -76,11 +75,7 @@ describe('WorkItemLinks', () => { }, provide: { projectPath: 'project/path', - iid: '1', hasIterationsFeature, - glFeatures: { - useIidInWorkItemsPath: fetchByIid, - }, }, propsData: { issuableId: 1 }, apolloProvider: mockApollo, @@ -351,7 +346,7 @@ describe('WorkItemLinks', () => { beforeEach(async () => { setWindowLocation('?iid_path=true'); - await createComponent({ fetchByIid: true }); + await createComponent(); firstChild = findFirstWorkItemLinkChild(); }); @@ -391,7 +386,7 @@ describe('WorkItemLinks', () => { it('starts prefetching work item by iid if URL contains work item id', async () => { setWindowLocation('?work_item_iid=5&iid_path=true'); - await createComponent({ fetchByIid: true }); + await createComponent(); expect(childWorkItemByIidHandler).toHaveBeenCalledWith({ iid: '5', @@ -402,7 +397,7 @@ describe('WorkItemLinks', () => { it('does not open the modal if work item iid URL parameter is not found in child items', async () => { setWindowLocation('?work_item_iid=555&iid_path=true'); - await createComponent({ fetchByIid: true }); + await createComponent(); expect(showModal).not.toHaveBeenCalled(); expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null); @@ -410,7 +405,7 @@ describe('WorkItemLinks', () => { it('opens the modal if work item iid URL parameter is found in child items', async () => { setWindowLocation('?work_item_iid=2&iid_path=true'); - await createComponent({ fetchByIid: true }); + await createComponent(); expect(showModal).toHaveBeenCalled(); expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2'); diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index 3db848a0ad2..a067923b9fc 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -9,10 +9,13 @@ import SystemNote from '~/work_items/components/notes/system_note.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; +import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql'; +import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql'; +import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql'; +import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql'; import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants'; import { ASC, DESC } from '~/notes/constants'; import { @@ -21,6 +24,9 @@ import { mockWorkItemNotesByIidResponse, mockMoreWorkItemNotesResponse, mockWorkItemNotesResponseWithComments, + workItemNotesCreateSubscriptionResponse, + workItemNotesUpdateSubscriptionResponse, + workItemNotesDeleteSubscriptionResponse, } from '../mock_data'; const mockWorkItemId = workItemQueryResponse.data.workItem.id; @@ -53,10 +59,9 @@ describe('WorkItemNotes component', () => { const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote); const findAllListItems = () => wrapper.findAll('ul.timeline > *'); - const findActivityLabel = () => wrapper.find('label'); const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - const findSortingFilter = () => wrapper.findComponent(ActivityFilter); + const findActivityHeader = () => wrapper.findComponent(WorkItemNotesActivityHeader); const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index); const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion); const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index); @@ -73,6 +78,15 @@ describe('WorkItemNotes component', () => { const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({ data: { destroyNote: { note: null, __typename: 'DestroyNote' } }, }); + const notesCreateSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemNotesCreateSubscriptionResponse); + const notesUpdateSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemNotesUpdateSubscriptionResponse); + const notesDeleteSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemNotesDeleteSubscriptionResponse); const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const createComponent = ({ @@ -86,6 +100,9 @@ describe('WorkItemNotes component', () => { [workItemNotesQuery, defaultWorkItemNotesQueryHandler], [workItemNotesByIidQuery, workItemNotesByIidQueryHandler], [deleteWorkItemNoteMutation, deleteWINoteMutationHandler], + [workItemNoteCreatedSubscription, notesCreateSubscriptionHandler], + [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler], + [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler], ]), propsData: { workItemId, @@ -96,11 +113,6 @@ describe('WorkItemNotes component', () => { fetchByIid, workItemType: 'task', }, - provide: { - glFeatures: { - useIidInWorkItemsPath: fetchByIid, - }, - }, stubs: { GlModal: stubComponent(GlModal, { methods: { show: showModal } }), }, @@ -111,8 +123,8 @@ describe('WorkItemNotes component', () => { createComponent(); }); - it('renders activity label', () => { - expect(findActivityLabel().exists()).toBe(true); + it('has the work item note activity header', () => { + expect(findActivityHeader().exists()).toBe(true); }); it('passes correct props to comment form component', async () => { @@ -203,26 +215,22 @@ describe('WorkItemNotes component', () => { await waitForPromises(); }); - it('filter exists', () => { - expect(findSortingFilter().exists()).toBe(true); - }); - - it('sorts the list when the `changeSortOrder` event is emitted', async () => { + it('sorts the list when the `changeSort` event is emitted', async () => { expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId); - await findSortingFilter().vm.$emit('changeSortOrder', DESC); + await findActivityHeader().vm.$emit('changeSort', DESC); expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId); }); it('puts form at start of list in when sorting by newest first', async () => { - await findSortingFilter().vm.$emit('changeSortOrder', DESC); + await findActivityHeader().vm.$emit('changeSort', DESC); expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true); }); it('puts form at end of list in when sorting by oldest first', async () => { - await findSortingFilter().vm.$emit('changeSortOrder', ASC); + await findActivityHeader().vm.$emit('changeSort', ASC); expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true); }); @@ -334,4 +342,31 @@ describe('WorkItemNotes component', () => { ['Something went wrong when deleting a comment. Please try again'], ]); }); + + describe('Notes subscriptions', () => { + beforeEach(async () => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + await waitForPromises(); + }); + + it('has create notes subscription', () => { + expect(notesCreateSubscriptionHandler).toHaveBeenCalledWith({ + noteableId: mockWorkItemId, + }); + }); + + it('has delete notes subscription', () => { + expect(notesDeleteSubscriptionHandler).toHaveBeenCalledWith({ + noteableId: mockWorkItemId, + }); + }); + + it('has update notes subscription', () => { + expect(notesUpdateSubscriptionHandler).toHaveBeenCalledWith({ + noteableId: mockWorkItemId, + }); + }); + }); }); 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 b24d940d56a..d1262057c73 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -44,10 +44,6 @@ describe('WorkItemState component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders state', () => { 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 a549aad5cd8..34391b74cf7 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -41,10 +41,6 @@ describe('WorkItemTitle component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders title', () => { createComponent(); 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 index 182fb0f8cb6..a5e955c4dbf 100644 --- a/spec/frontend/work_items/components/work_item_type_icon_spec.js +++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js @@ -9,7 +9,7 @@ function createComponent(propsData) { wrapper = shallowMount(WorkItemTypeIcon, { propsData, directives: { - GlTooltip: createMockDirective(), + GlTooltip: createMockDirective('gl-tooltip'), }, }); } @@ -17,10 +17,6 @@ function createComponent(propsData) { describe('Work Item type component', () => { const findIcon = () => wrapper.findComponent(GlIcon); - afterEach(() => { - wrapper.destroy(); - }); - describe.each` workItemType | workItemIconName | iconName | text | showTooltipOnHover ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false} diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index d4832fe376d..fecf98b2651 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -82,6 +82,7 @@ export const workItemQueryResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -182,6 +183,7 @@ export const updateWorkItemMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -330,6 +332,7 @@ export const workItemResponseFactory = ({ userPermissions: { deleteWorkItem: canDelete, updateWorkItem: canUpdate, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -473,23 +476,20 @@ export const workItemResponseFactory = ({ export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({ data: { - workspace: { - id: 'gid://gitlab/Project/1', - issuable: { - id: 'gid://gitlab/Issue/4', - confidential, - iteration: { - id: 'gid://gitlab/Iteration/1124', - __typename: 'Iteration', - }, - milestone: { - id: 'gid://gitlab/Milestone/28', - __typename: 'Milestone', - }, - __typename: 'Issue', + issue: { + id: 'gid://gitlab/Issue/4', + confidential, + iteration: { + id: 'gid://gitlab/Iteration/1124', + __typename: 'Iteration', }, - __typename: 'Project', + milestone: { + id: 'gid://gitlab/Milestone/28', + __typename: 'Milestone', + }, + __typename: 'Issue', }, + __typename: 'Project', }, }); @@ -542,6 +542,7 @@ export const createWorkItemMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [], }, @@ -590,6 +591,7 @@ export const createWorkItemFromTaskMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -630,6 +632,7 @@ export const createWorkItemFromTaskMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [], }, @@ -831,15 +834,20 @@ export const workItemHierarchyEmptyResponse = { data: { workItem: { id: 'gid://gitlab/WorkItem/1', + iid: 1, + state: 'OPEN', workItemType: { - id: 'gid://gitlab/WorkItems::Type/6', + id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue', iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', + description: '', createdAt: '2022-08-03T12:41:54Z', + updatedAt: null, closedAt: null, + author: mockAssignees[0], project: { __typename: 'Project', id: '1', @@ -849,14 +857,11 @@ export const workItemHierarchyEmptyResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, confidential: false, widgets: [ { - type: 'DESCRIPTION', - __typename: 'WorkItemWidgetDescription', - }, - { type: 'HIERARCHY', parent: null, hasChildren: false, @@ -876,6 +881,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = { data: { workItem: { id: 'gid://gitlab/WorkItem/1', + iid: 1, + state: 'OPEN', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', name: 'Issue', @@ -883,9 +890,15 @@ export const workItemHierarchyNoUpdatePermissionResponse = { __typename: 'WorkItemType', }, title: 'New title', + description: '', + createdAt: '2022-08-03T12:41:54Z', + updatedAt: null, + closedAt: null, + author: mockAssignees[0], userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, project: { __typename: 'Project', @@ -896,10 +909,6 @@ export const workItemHierarchyNoUpdatePermissionResponse = { confidential: false, widgets: [ { - type: 'DESCRIPTION', - __typename: 'WorkItemWidgetDescription', - }, - { type: 'HIERARCHY', parent: null, hasChildren: true, @@ -952,6 +961,7 @@ export const workItemTask = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [], __typename: 'WorkItem', }; @@ -969,6 +979,7 @@ export const confidentialWorkItemTask = { confidential: true, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [], __typename: 'WorkItem', }; @@ -986,6 +997,7 @@ export const closedWorkItemTask = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: '2022-08-12T13:07:52Z', + widgets: [], __typename: 'WorkItem', }; @@ -1007,6 +1019,7 @@ export const childrenWorkItems = [ confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [], __typename: 'WorkItem', }, ]; @@ -1017,15 +1030,19 @@ export const workItemHierarchyResponse = { id: 'gid://gitlab/WorkItem/1', iid: '1', workItemType: { - id: 'gid://gitlab/WorkItems::Type/6', - name: 'Objective', - iconName: 'issue-type-objective', + id: 'gid://gitlab/WorkItems::Type/1', + name: 'Issue', + iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', + }, + author: { + ...mockAssignees[0], }, confidential: false, project: { @@ -1034,12 +1051,13 @@ export const workItemHierarchyResponse = { fullPath: 'test-project-path', archived: false, }, + description: 'Issue description', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + updatedAt: null, + closedAt: null, widgets: [ { - type: 'DESCRIPTION', - __typename: 'WorkItemWidgetDescription', - }, - { type: 'HIERARCHY', parent: null, hasChildren: true, @@ -1110,6 +1128,7 @@ export const workItemObjectiveWithChild = { userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', }, author: { ...mockAssignees[0], @@ -1176,6 +1195,7 @@ export const workItemHierarchyTreeResponse = { userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', }, confidential: false, project: { @@ -1252,6 +1272,7 @@ export const changeWorkItemParentMutationResponse = { userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', }, description: null, id: 'gid://gitlab/WorkItem/2', @@ -1624,6 +1645,7 @@ export const projectWorkItemResponse = { workItems: { nodes: [workItemQueryResponse.data.workItem], }, + __typename: 'Project', }, }, }; @@ -1681,6 +1703,8 @@ export const mockWorkItemNotesResponse = { systemNoteIconName: 'link', createdAt: '2022-11-14T04:18:59Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_199', lastEditedBy: null, system: true, internal: false, @@ -1724,6 +1748,8 @@ export const mockWorkItemNotesResponse = { systemNoteIconName: 'clock', createdAt: '2022-11-14T04:18:59Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_201', lastEditedBy: null, system: true, internal: false, @@ -1766,6 +1792,8 @@ export const mockWorkItemNotesResponse = { systemNoteIconName: 'weight', createdAt: '2022-11-25T07:16:20Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_202', lastEditedBy: null, system: true, internal: false, @@ -1868,6 +1896,8 @@ export const mockWorkItemNotesByIidResponse = { systemNoteIconName: 'link', createdAt: '2022-11-14T04:18:59Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: true, internal: false, @@ -1913,6 +1943,8 @@ export const mockWorkItemNotesByIidResponse = { systemNoteIconName: 'clock', createdAt: '2022-11-14T04:18:59Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: true, internal: false, @@ -1959,6 +1991,8 @@ export const mockWorkItemNotesByIidResponse = { systemNoteIconName: 'iteration', createdAt: '2022-11-14T04:19:00Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: true, internal: false, @@ -2059,6 +2093,8 @@ export const mockMoreWorkItemNotesResponse = { systemNoteIconName: 'link', createdAt: '2022-11-14T04:18:59Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: true, internal: false, @@ -2102,6 +2138,8 @@ export const mockMoreWorkItemNotesResponse = { systemNoteIconName: 'clock', createdAt: '2022-11-14T04:18:59Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: true, internal: false, @@ -2144,6 +2182,8 @@ export const mockMoreWorkItemNotesResponse = { systemNoteIconName: 'weight', createdAt: '2022-11-25T07:16:20Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: true, internal: false, @@ -2205,6 +2245,7 @@ export const createWorkItemNoteResponse = { systemNoteIconName: null, createdAt: '2023-01-25T04:49:46Z', lastEditedAt: null, + url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, discussion: { id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122', @@ -2252,6 +2293,7 @@ export const mockWorkItemCommentNote = { systemNoteIconName: false, createdAt: '2022-11-25T07:16:20Z', lastEditedAt: null, + url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: false, internal: false, @@ -2331,6 +2373,8 @@ export const mockWorkItemNotesResponseWithComments = { systemNoteIconName: null, createdAt: '2023-01-12T07:47:40Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, discussion: { id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3', @@ -2365,6 +2409,8 @@ export const mockWorkItemNotesResponseWithComments = { systemNoteIconName: null, createdAt: '2023-01-18T09:09:54Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, discussion: { id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3', @@ -2406,6 +2452,8 @@ export const mockWorkItemNotesResponseWithComments = { systemNoteIconName: 'weight', createdAt: '2022-11-25T07:16:20Z', lastEditedAt: null, + url: + 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', lastEditedBy: null, system: false, internal: false, @@ -2447,3 +2495,129 @@ export const mockWorkItemNotesResponseWithComments = { }, }, }; + +export const workItemNotesCreateSubscriptionResponse = { + data: { + workItemNoteCreated: { + id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d81864', + body: 'changed weight to **89**', + bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>', + systemNoteIconName: 'weight', + createdAt: '2022-11-25T07:16:20Z', + lastEditedAt: null, + url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', + lastEditedBy: null, + system: true, + internal: false, + discussion: { + id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e', + notes: { + nodes: [ + { + id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9881864', + body: 'changed weight to **89**', + bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>', + systemNoteIconName: 'weight', + createdAt: '2022-11-25T07:16:20Z', + lastEditedAt: null, + url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', + lastEditedBy: null, + system: true, + internal: false, + discussion: { + id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987', + }, + userPermissions: { + adminNote: false, + awardEmoji: true, + readNote: true, + createNote: true, + resolveNote: true, + repositionNote: true, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + }, + }, + userPermissions: { + adminNote: false, + awardEmoji: true, + readNote: true, + createNote: true, + resolveNote: true, + repositionNote: true, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + }, +}; + +export const workItemNotesUpdateSubscriptionResponse = { + data: { + workItemNoteUpdated: { + id: 'gid://gitlab/Note/0f2f195ec0d1ef95ee9d5b10446b8e96a9883894', + body: 'changed title', + bodyHtml: '<p dir="auto">changed title<strong>89</strong></p>', + systemNoteIconName: 'pencil', + createdAt: '2022-11-25T07:16:20Z', + lastEditedAt: null, + url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', + lastEditedBy: null, + system: true, + internal: false, + discussion: { + id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987', + }, + userPermissions: { + adminNote: false, + awardEmoji: true, + readNote: true, + createNote: true, + resolveNote: true, + repositionNote: true, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + }, +}; + +export const workItemNotesDeleteSubscriptionResponse = { + data: { + workItemNoteDeleted: { + id: 'gid://gitlab/DiscussionNote/235', + discussionId: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3', + lastDiscussionNote: false, + }, + }, +}; diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index 387c8a355fa..b963f041dd9 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -37,7 +37,6 @@ describe('Create work item component', () => { props = {}, queryHandler = querySuccessHandler, mutationHandler = createWorkItemSuccessHandler, - fetchByIid = false, } = {}) => { fakeApollo = createMockApollo( [ @@ -66,15 +65,11 @@ describe('Create work item component', () => { }, provide: { fullPath: 'full-path', - glFeatures: { - useIidInWorkItemsPath: fetchByIid, - }, }, }); }; afterEach(() => { - wrapper.destroy(); fakeApollo = null; }); @@ -109,9 +104,8 @@ describe('Create work item component', () => { expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ name: 'workItem', - params: { - id: '1', - }, + params: { id: '1' }, + query: { iid_path: 'true' }, }); }); @@ -210,18 +204,4 @@ describe('Create work item component', () => { 'Something went wrong when creating work item. Please try again.', ); }); - - it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => { - createComponent({ fetchByIid: true }); - findTitleInput().vm.$emit('title-input', 'Test title'); - - wrapper.find('form').trigger('submit'); - await waitForPromises(); - - expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ - name: 'workItem', - params: { id: '1' }, - query: { iid_path: 'true' }, - }); - }); }); diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js index a766962771a..37326910e13 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -44,10 +44,6 @@ describe('Work items root component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('renders WorkItemDetail', () => { createComponent(); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index ef9ae4a2eab..5dad7f7c43f 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -75,6 +75,7 @@ describe('Work items router', () => { WorkItemWeight: true, WorkItemIteration: true, WorkItemHealthStatus: true, + WorkItemNotes: true, }, }); }; @@ -88,6 +89,7 @@ describe('Work items router', () => { }); afterEach(() => { + // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy wrapper.destroy(); window.location.hash = ''; }); diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js index 124ff5f1608..22fd7d5f48a 100644 --- a/spec/frontend/work_items_hierarchy/components/app_spec.js +++ b/spec/frontend/work_items_hierarchy/components/app_spec.js @@ -24,10 +24,6 @@ describe('WorkItemsHierarchy App', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('survey banner', () => { it('shows when the banner is visible', () => { createComponent({}, { bannerVisible: true }); diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js index 084aaa754ab..dfdef7915dd 100644 --- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js +++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js @@ -40,10 +40,6 @@ describe('WorkItemsHierarchy Hierarchy', () => { ); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('available structure', () => { let items = []; diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js index 85f1dbdc305..025a92464f1 100644 --- a/spec/frontend/zen_mode_spec.js +++ b/spec/frontend/zen_mode_spec.js @@ -15,6 +15,8 @@ describe('ZenMode', () => { let dropzoneForElementSpy; const fixtureName = 'snippets/show.html'; + const getTextarea = () => $('.notes-form textarea'); + function enterZen() { $('.notes-form .js-zen-enter').click(); } @@ -24,7 +26,7 @@ describe('ZenMode', () => { } function escapeKeydown() { - $('.notes-form textarea').trigger( + getTextarea().trigger( $.Event('keydown', { keyCode: 27, }), @@ -50,6 +52,12 @@ describe('ZenMode', () => { }); afterEach(() => { + $(document).off('click', '.js-zen-enter'); + $(document).off('click', '.js-zen-leave'); + $(document).off('zen_mode:enter'); + $(document).off('zen_mode:leave'); + $(document).off('keydown'); + resetHTMLFixture(); }); @@ -62,14 +70,14 @@ describe('ZenMode', () => { $('.div-dropzone').addClass('js-invalid-dropzone'); exitZen(); - expect(dropzoneForElementSpy.mock.calls.length).toEqual(0); + expect(dropzoneForElementSpy).not.toHaveBeenCalled(); }); it('should call dropzone if element is dropzone valid', () => { $('.div-dropzone').removeClass('js-invalid-dropzone'); exitZen(); - expect(dropzoneForElementSpy.mock.calls.length).toEqual(2); + expect(dropzoneForElementSpy).toHaveBeenCalledTimes(1); }); }); @@ -82,10 +90,10 @@ describe('ZenMode', () => { }); it('removes textarea styling', () => { - $('.notes-form textarea').attr('style', 'height: 400px'); + getTextarea().attr('style', 'height: 400px'); enterZen(); - expect($('.notes-form textarea')).not.toHaveAttr('style'); + expect(getTextarea()).not.toHaveAttr('style'); }); }); @@ -116,4 +124,15 @@ describe('ZenMode', () => { expect(utils.scrollToElement).toHaveBeenCalled(); }); }); + + it('restores textarea style', () => { + const style = 'color: red; overflow-y: hidden;'; + getTextarea().attr('style', style); + expect(getTextarea()).toHaveAttr('style', style); + + enterZen(); + exitZen(); + + expect(getTextarea()).toHaveAttr('style', style); + }); }); |