diff options
Diffstat (limited to 'spec/frontend/vue_shared')
32 files changed, 650 insertions, 458 deletions
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js index 6c2b21053f0..d2dfb6ee1bf 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -1,16 +1,19 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { mount, 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 createMockApollo from 'helpers/mock_apollo_helper'; import { joinPaths } from '~/lib/utils/url_utility'; import Tracking from '~/tracking'; import AlertDetails from '~/vue_shared/alert_details/components/alert_details.vue'; import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue'; import { PAGE_CONFIG, SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants'; import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql'; +import alertQuery from '~/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue'; import createStore from '~/vue_shared/components/metric_images/store/'; @@ -27,20 +30,57 @@ describe('AlertDetails', () => { let environmentData = { name: environmentName, path: environmentPath }; let mock; let wrapper; + let requestHandlers; const projectPath = 'root/alerts'; const projectIssuesPath = 'root/alerts/-/issues'; const projectId = '1'; const $router = { push: jest.fn() }; + const defaultHandlers = { + createIssueMutationMock: jest.fn().mockResolvedValue({ + data: { + createAlertIssue: { + errors: [], + issue: { + id: 'id', + iid: 'iid', + webUrl: 'webUrl', + }, + }, + }, + }), + alertQueryMock: jest.fn().mockResolvedValue({ + data: { + project: { + id: '1', + alertManagementAlerts: { + nodes: [], + }, + }, + }, + }), + }; + + const createMockApolloProvider = (handlers) => { + Vue.use(VueApollo); + requestHandlers = handlers; + + return createMockApollo([ + [alertQuery, handlers.alertQueryMock], + [createIssueMutation, handlers.createIssueMutationMock], + ]); + }; + function mountComponent({ data, - loading = false, mountMethod = shallowMount, provide = {}, stubs = {}, + handlers = defaultHandlers, } = {}) { wrapper = extendedWrapper( mountMethod(AlertDetails, { + apolloProvider: createMockApolloProvider(handlers), provide: { alertId: 'alertId', projectPath, @@ -59,15 +99,6 @@ describe('AlertDetails', () => { }; }, mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - sidebarStatus: {}, - }, - }, $router, $route: { params: {} }, }, @@ -139,7 +170,6 @@ describe('AlertDetails', () => { describe('Metrics tab', () => { it('should mount without errors', () => { mountComponent({ - mountMethod: mount, provide: { canUpdate: true, iid: '1', @@ -216,7 +246,6 @@ describe('AlertDetails', () => { it('should display "Create incident" button when incident doesn\'t exist yet', async () => { const issue = null; mountComponent({ - mountMethod: mount, data: { alert: { ...mockAlert, issue } }, }); @@ -226,23 +255,16 @@ describe('AlertDetails', () => { }); it('calls `$apollo.mutate` with `createIssueQuery`', () => { - const issueIid = '10'; mountComponent({ mountMethod: mount, data: { alert: { ...mockAlert } }, }); - jest - .spyOn(wrapper.vm.$apollo, 'mutate') - .mockResolvedValue({ data: { createAlertIssue: { issue: { iid: issueIid } } } }); findCreateIncidentBtn().trigger('click'); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: createIssueMutation, - variables: { - iid: mockAlert.iid, - projectPath, - }, + expect(requestHandlers.createIssueMutationMock).toHaveBeenCalledWith({ + iid: mockAlert.iid, + projectPath, }); }); @@ -251,25 +273,44 @@ describe('AlertDetails', () => { mountComponent({ mountMethod: mount, data: { alert: { ...mockAlert, alertIid: 1 } }, + handlers: { + ...defaultHandlers, + createIssueMutationMock: jest.fn().mockRejectedValue(new Error(errorMsg)), + }, }); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); findCreateIncidentBtn().trigger('click'); await waitForPromises(); - expect(findIncidentCreationAlert().text()).toBe(errorMsg); + expect(findIncidentCreationAlert().text()).toBe(`Error: ${errorMsg}`); }); }); describe('View full alert details', () => { - beforeEach(() => { - mountComponent({ data: { alert: mockAlert } }); + beforeEach(async () => { + mountComponent({ + data: { alert: mockAlert }, + handlers: { + ...defaultHandlers, + alertQueryMock: jest.fn().mockResolvedValue({ + data: { + project: { + id: '1', + alertManagementAlerts: { + nodes: [{ id: '1' }], + }, + }, + }, + }), + }, + }); + await waitForPromises(); }); it('should display a table of raw alert details data', () => { - const details = findDetailsTable(); - expect(details.exists()).toBe(true); - expect(details.props()).toStrictEqual({ + expect(findDetailsTable().exists()).toBe(true); + + expect(findDetailsTable().props()).toStrictEqual({ alert: mockAlert, statuses: PAGE_CONFIG.OPERATIONS.STATUSES, loading: false, @@ -279,7 +320,7 @@ describe('AlertDetails', () => { describe('loading state', () => { beforeEach(() => { - mountComponent({ loading: true }); + mountComponent(); }); it('displays a loading state when loading', () => { diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon/ci_icon_spec.js index cbb725bf9e6..792470c8e89 100644 --- a/spec/frontend/vue_shared/components/ci_icon_spec.js +++ b/spec/frontend/vue_shared/components/ci_icon/ci_icon_spec.js @@ -1,6 +1,6 @@ import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; const mockStatus = { group: 'success', 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 53218d794c7..b825a578cee 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 @@ -19,7 +19,7 @@ describe('Confirm Danger Modal', () => { const findModal = () => wrapper.findComponent(GlModal); const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase'); - const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input'); + const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-field'); const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning'); const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message'); const findPrimaryAction = () => findModal().props('actionPrimary'); 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 810269257b6..e2c3fc89525 100644 --- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js @@ -152,7 +152,8 @@ describe('Diff Stats Dropdown', () => { }); it('focuses the first item when pressing the down key within the search box', () => { - const spy = jest.spyOn(wrapper.vm, 'focusFirstItem'); + const { element } = wrapper.find('.gl-new-dropdown-item'); + const spy = jest.spyOn(element, 'focus'); findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ARROW_DOWN_KEY })); expect(spy).toHaveBeenCalled(); 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 dd5a05a40c6..1a9a08a9656 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -1,8 +1,8 @@ import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; - import { nextTick } from 'vue'; import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue'; +import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; describe('DropdownWidget component', () => { let wrapper; @@ -27,11 +27,14 @@ describe('DropdownWidget component', () => { ...props, }, stubs: { - GlDropdown, + GlDropdown: stubComponent(GlDropdown, { + methods: { + hide: jest.fn(), + }, + template: RENDER_ALL_SLOTS_TEMPLATE, + }), }, }); - - jest.spyOn(findDropdown().vm, 'hide').mockImplementation(); }; beforeEach(() => { diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js index 1376133ec37..02da6079466 100644 --- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js +++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js @@ -8,7 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises'; describe('EntitySelect', () => { let wrapper; let fetchItemsMock; - let fetchInitialSelectionTextMock; + let fetchInitialSelectionMock; // Mocks const itemMock = { @@ -96,16 +96,16 @@ describe('EntitySelect', () => { }); it("fetches the initially selected value's name", async () => { - fetchInitialSelectionTextMock = jest.fn().mockImplementation(() => itemMock.text); + fetchInitialSelectionMock = jest.fn().mockImplementation(() => itemMock); createComponent({ props: { - fetchInitialSelectionText: fetchInitialSelectionTextMock, + fetchInitialSelection: fetchInitialSelectionMock, initialSelection: itemMock.value, }, }); await nextTick(); - expect(fetchInitialSelectionTextMock).toHaveBeenCalledTimes(1); + expect(fetchInitialSelectionMock).toHaveBeenCalledTimes(1); expect(findListbox().props('toggleText')).toBe(itemMock.text); }); }); @@ -188,7 +188,7 @@ describe('EntitySelect', () => { findListbox().vm.$emit('reset'); await nextTick(); - expect(Object.keys(wrapper.emitted('input')[2][0]).length).toBe(0); + expect(wrapper.emitted('input')[2][0]).toEqual({}); }); }); }); diff --git a/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js b/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js index ea029ba4f27..6dc38bbd0c6 100644 --- a/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js +++ b/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js @@ -1,35 +1,35 @@ import VueApollo from 'vue-apollo'; -import Vue, { nextTick } from 'vue'; -import { GlCollapsibleListbox } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Vue from 'vue'; +import { GlCollapsibleListbox, GlAlert } from '@gitlab/ui'; +import { chunk } from 'lodash'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue'; import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue'; +import { DEFAULT_PER_PAGE } from '~/api'; import { ORGANIZATION_TOGGLE_TEXT, ORGANIZATION_HEADER_TEXT, FETCH_ORGANIZATIONS_ERROR, FETCH_ORGANIZATION_ERROR, } from '~/vue_shared/components/entity_select/constants'; -import resolvers from '~/organizations/shared/graphql/resolvers'; -import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql'; -import { organizations as organizationsMock } from '~/organizations/mock_data'; +import getCurrentUserOrganizationsQuery from '~/organizations/shared/graphql/queries/organizations.query.graphql'; +import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql'; +import { organizations as nodes, pageInfo, pageInfoEmpty } from '~/organizations/mock_data'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; Vue.use(VueApollo); -jest.useFakeTimers(); - describe('OrganizationSelect', () => { let wrapper; let mockApollo; // Mocks - const [organizationMock] = organizationsMock; - - // Stubs - const GlAlert = { - template: '<div><slot /></div>', + const [organization] = nodes; + const organizations = { + nodes, + pageInfo, }; // Props @@ -44,23 +44,26 @@ describe('OrganizationSelect', () => { const findEntitySelect = () => wrapper.findComponent(EntitySelect); const findAlert = () => wrapper.findComponent(GlAlert); + // Mock handlers const handleInput = jest.fn(); + const getCurrentUserOrganizationsQueryHandler = jest.fn().mockResolvedValue({ + data: { currentUser: { id: 'gid://gitlab/User/1', __typename: 'CurrentUser', organizations } }, + }); + const getOrganizationQueryHandler = jest.fn().mockResolvedValue({ + data: { organization }, + }); // Helpers - const createComponent = ({ props = {}, mockResolvers = resolvers, handlers } = {}) => { - mockApollo = createMockApollo( - handlers || [ - [ - organizationsQuery, - jest.fn().mockResolvedValueOnce({ - data: { currentUser: { id: 1, organizations: { nodes: organizationsMock } } }, - }), - ], - ], - mockResolvers, - ); - - wrapper = shallowMountExtended(OrganizationSelect, { + const createComponent = ({ + props = {}, + handlers = [ + [getCurrentUserOrganizationsQuery, getCurrentUserOrganizationsQueryHandler], + [getOrganizationQuery, getOrganizationQueryHandler], + ], + } = {}) => { + mockApollo = createMockApollo(handlers); + + wrapper = mountExtended(OrganizationSelect, { apolloProvider: mockApollo, propsData: { label, @@ -70,10 +73,6 @@ describe('OrganizationSelect', () => { toggleClass, ...props, }, - stubs: { - GlAlert, - EntitySelect, - }, listeners: { input: handleInput, }, @@ -81,10 +80,6 @@ describe('OrganizationSelect', () => { }; const openListbox = () => findListbox().vm.$emit('shown'); - afterEach(() => { - mockApollo = null; - }); - describe('entity_select props', () => { beforeEach(() => { createComponent(); @@ -107,40 +102,31 @@ describe('OrganizationSelect', () => { describe('on mount', () => { it('fetches organizations when the listbox is opened', async () => { createComponent(); - await nextTick(); - jest.runAllTimers(); - await waitForPromises(); - openListbox(); - jest.runAllTimers(); await waitForPromises(); - expect(findListbox().props('items')).toEqual([ - { text: organizationsMock[0].name, value: 1 }, - { text: organizationsMock[1].name, value: 2 }, - { text: organizationsMock[2].name, value: 3 }, - ]); + + const expectedItems = nodes.map((node) => ({ + ...node, + text: node.name, + value: getIdFromGraphQLId(node.id), + })); + + expect(findListbox().props('items')).toEqual(expectedItems); }); describe('with an initial selection', () => { it("fetches the initially selected value's name", async () => { - createComponent({ props: { initialSelection: organizationMock.id } }); - await nextTick(); - jest.runAllTimers(); + createComponent({ props: { initialSelection: organization.id } }); await waitForPromises(); - expect(findListbox().props('toggleText')).toBe(organizationMock.name); + expect(findListbox().props('toggleText')).toBe(organization.name); }); it('show an error if fetching initially selected fails', async () => { - const mockResolvers = { - Query: { - organization: jest.fn().mockRejectedValueOnce(new Error()), - }, - }; - - createComponent({ props: { initialSelection: organizationMock.id }, mockResolvers }); - await nextTick(); - jest.runAllTimers(); + createComponent({ + props: { initialSelection: organization.id }, + handlers: [[getOrganizationQuery, jest.fn().mockRejectedValueOnce()]], + }); expect(findAlert().exists()).toBe(false); @@ -152,18 +138,59 @@ describe('OrganizationSelect', () => { }); }); + describe('when listbox bottom is reached and there are more organizations to load', () => { + const [firstPage, secondPage] = chunk(nodes, Math.ceil(nodes.length / 2)); + const getCurrentUserOrganizationsQueryMultiplePagesHandler = jest + .fn() + .mockResolvedValueOnce({ + data: { + currentUser: { + id: 'gid://gitlab/User/1', + __typename: 'CurrentUser', + organizations: { nodes: firstPage, pageInfo }, + }, + }, + }) + .mockResolvedValueOnce({ + data: { + currentUser: { + id: 'gid://gitlab/User/1', + __typename: 'CurrentUser', + organizations: { nodes: secondPage, pageInfo: pageInfoEmpty }, + }, + }, + }); + + beforeEach(async () => { + createComponent({ + handlers: [ + [getCurrentUserOrganizationsQuery, getCurrentUserOrganizationsQueryMultiplePagesHandler], + [getOrganizationQuery, getOrganizationQueryHandler], + ], + }); + openListbox(); + await waitForPromises(); + + findListbox().vm.$emit('bottom-reached'); + await waitForPromises(); + }); + + it('calls graphQL query correct `after` variable', () => { + expect(getCurrentUserOrganizationsQueryMultiplePagesHandler).toHaveBeenCalledWith({ + after: pageInfo.endCursor, + first: DEFAULT_PER_PAGE, + }); + expect(findListbox().props('infiniteScroll')).toBe(false); + }); + }); + it('shows an error when fetching organizations fails', async () => { createComponent({ - handlers: [[organizationsQuery, jest.fn().mockRejectedValueOnce(new Error())]], + handlers: [[getCurrentUserOrganizationsQuery, jest.fn().mockRejectedValueOnce()]], }); - await nextTick(); - jest.runAllTimers(); - await waitForPromises(); - openListbox(); expect(findAlert().exists()).toBe(false); - jest.runAllTimers(); await waitForPromises(); expect(findAlert().exists()).toBe(true); 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 bb612a13209..3a5c7d7729f 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 @@ -1,11 +1,4 @@ -import { - GlFilteredSearch, - GlButtonGroup, - GlButton, - GlDropdown, - GlDropdownItem, - GlFormCheckbox, -} from '@gitlab/ui'; +import { GlDropdownItem, GlSorting, GlFilteredSearch, GlFormCheckbox } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; import { nextTick } from 'vue'; @@ -13,7 +6,6 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; import { FILTERED_SEARCH_TERM, - SORT_DIRECTION, TOKEN_TYPE_AUTHOR, TOKEN_TYPE_LABEL, TOKEN_TYPE_MILESTONE, @@ -48,6 +40,7 @@ const createComponent = ({ recentSearchesStorageKey = 'requirements', tokens = mockAvailableTokens, sortOptions, + initialSortBy, initialFilterValue = [], showCheckbox = false, checkboxChecked = false, @@ -61,6 +54,7 @@ const createComponent = ({ recentSearchesStorageKey, tokens, sortOptions, + initialSortBy, initialFilterValue, showCheckbox, checkboxChecked, @@ -72,34 +66,38 @@ const createComponent = ({ describe('FilteredSearchBarRoot', () => { let wrapper; - const findGlButton = () => wrapper.findComponent(GlButton); - const findGlDropdown = () => wrapper.findComponent(GlDropdown); + const findGlSorting = () => wrapper.findComponent(GlSorting); const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); - beforeEach(() => { - wrapper = createComponent({ sortOptions: mockSortOptions }); - }); - describe('data', () => { - it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => { - expect(wrapper.vm.filterValue).toEqual([]); - expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]); - expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending); - expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true); - expect(wrapper.findComponent(GlButton).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); + describe('when `sortOptions` are provided', () => { + beforeEach(() => { + wrapper = createComponent({ sortOptions: mockSortOptions }); + }); + + it('sets a correct initial value for GlFilteredSearch', () => { + expect(findGlFilteredSearch().props('value')).toEqual([]); + }); + + it('emits an event with the selectedSortOption provided by default', async () => { + findGlSorting().vm.$emit('sortByChange', mockSortOptions[1].id); + await nextTick(); + + expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[1].sortDirection.descending]); + }); + + it('emits an event with the selectedSortDirection provided by default', async () => { + findGlSorting().vm.$emit('sortDirectionChange', true); + await nextTick(); + + expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[0].sortDirection.ascending]); + }); }); - it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => { - const wrapperNoSort = createComponent(); + it('does not initialize the sort dropdown when `sortOptions` are not provided', () => { + wrapper = createComponent(); - expect(wrapperNoSort.vm.filterValue).toEqual([]); - expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined); - expect(wrapperNoSort.findComponent(GlButtonGroup).exists()).toBe(false); - expect(wrapperNoSort.findComponent(GlButton).exists()).toBe(false); - expect(wrapperNoSort.findComponent(GlDropdown).exists()).toBe(false); - expect(wrapperNoSort.findComponent(GlDropdownItem).exists()).toBe(false); + expect(findGlSorting().exists()).toBe(false); }); }); @@ -125,27 +123,27 @@ describe('FilteredSearchBarRoot', () => { }); describe('sortDirectionIcon', () => { - it('renders `sort-highest` descending icon by default', () => { - expect(findGlButton().props('icon')).toBe('sort-highest'); - expect(findGlButton().attributes()).toMatchObject({ - 'aria-label': 'Sort direction: Descending', - title: 'Sort direction: Descending', - }); + beforeEach(() => { + wrapper = createComponent({ sortOptions: mockSortOptions }); + }); + + it('passes isAscending=false to GlSorting by default', () => { + expect(findGlSorting().props('isAscending')).toBe(false); }); it('renders `sort-lowest` ascending icon when the sort button is clicked', async () => { - findGlButton().vm.$emit('click'); + findGlSorting().vm.$emit('sortDirectionChange', true); await nextTick(); - expect(findGlButton().props('icon')).toBe('sort-lowest'); - expect(findGlButton().attributes()).toMatchObject({ - 'aria-label': 'Sort direction: Ascending', - title: 'Sort direction: Ascending', - }); + expect(findGlSorting().props('isAscending')).toBe(true); }); }); describe('filteredRecentSearches', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + it('returns array of recent searches filtering out any string type (unsupported) items', async () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -227,34 +225,37 @@ describe('FilteredSearchBarRoot', () => { }); }); - describe('handleSortOptionClick', () => { - it('emits component event `onSort` with selected sort by value', () => { - wrapper.vm.handleSortOptionClick(mockSortOptions[1]); + describe('handleSortOptionChange', () => { + it('emits component event `onSort` with selected sort by value', async () => { + wrapper = createComponent({ sortOptions: mockSortOptions }); + + findGlSorting().vm.$emit('sortByChange', mockSortOptions[1].id); + await nextTick(); expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[1]); expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[1].sortDirection.descending]); }); }); - describe('handleSortDirectionClick', () => { + describe('handleSortDirectionChange', () => { 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({ - selectedSortOption: mockSortOptions[0], + wrapper = createComponent({ + sortOptions: mockSortOptions, + initialSortBy: mockSortOptions[0].sortDirection.descending, }); }); - it('sets `selectedSortDirection` to be opposite of its current value', () => { - expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending); + it('sets sort direction to be opposite of its current value', async () => { + expect(findGlSorting().props('isAscending')).toBe(false); - wrapper.vm.handleSortDirectionClick(); + findGlSorting().vm.$emit('sortDirectionChange', true); + await nextTick(); - expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.ascending); + expect(findGlSorting().props('isAscending')).toBe(true); }); it('emits component event `onSort` with opposite of currently selected sort by value', () => { - wrapper.vm.handleSortDirectionClick(); + findGlSorting().vm.$emit('sortDirectionChange', true); expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[0].sortDirection.ascending]); }); @@ -288,6 +289,8 @@ describe('FilteredSearchBarRoot', () => { const mockFilters = [tokenValueAuthor, 'foo']; beforeEach(async () => { + 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({ @@ -358,19 +361,14 @@ describe('FilteredSearchBarRoot', () => { }); describe('template', () => { - beforeEach(async () => { + it('renders gl-filtered-search component', async () => { + 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({ - selectedSortOption: mockSortOptions[0], - selectedSortDirection: SORT_DIRECTION.descending, + await wrapper.setData({ recentSearches: mockHistoryItems, }); - await nextTick(); - }); - - it('renders gl-filtered-search component', () => { const glFilteredSearchEl = wrapper.findComponent(GlFilteredSearch); expect(glFilteredSearchEl.props('placeholder')).toBe('Filter requirements'); @@ -454,25 +452,28 @@ describe('FilteredSearchBarRoot', () => { }); it('renders sort dropdown component', () => { - expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdown).props('text')).toBe(mockSortOptions[0].title); - }); - - it('renders sort dropdown items', () => { - const dropdownItemsEl = wrapper.findAllComponents(GlDropdownItem); + wrapper = createComponent({ sortOptions: mockSortOptions }); - expect(dropdownItemsEl).toHaveLength(mockSortOptions.length); - expect(dropdownItemsEl.at(0).text()).toBe(mockSortOptions[0].title); - expect(dropdownItemsEl.at(0).props('isChecked')).toBe(true); - expect(dropdownItemsEl.at(1).text()).toBe(mockSortOptions[1].title); + expect(findGlSorting().exists()).toBe(true); }); - it('renders sort direction button', () => { - const sortButtonEl = wrapper.findComponent(GlButton); - - expect(sortButtonEl.attributes('title')).toBe('Sort direction: Descending'); - expect(sortButtonEl.props('icon')).toBe('sort-highest'); + it('renders sort dropdown items', () => { + wrapper = createComponent({ sortOptions: mockSortOptions }); + + const { sortOptions, sortBy } = findGlSorting().props(); + + expect(sortOptions).toEqual([ + { + value: mockSortOptions[0].id, + text: mockSortOptions[0].title, + }, + { + value: mockSortOptions[1].id, + text: mockSortOptions[1].title, + }, + ]); + + expect(sortBy).toBe(mockSortOptions[0].id); }); }); @@ -483,6 +484,10 @@ describe('FilteredSearchBarRoot', () => { value: { data: '' }, }; + beforeEach(() => { + wrapper = createComponent({ sortOptions: mockSortOptions }); + }); + it('syncs filter value', async () => { await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: true }); @@ -498,17 +503,33 @@ describe('FilteredSearchBarRoot', () => { it('syncs sort values', async () => { await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true }); - expect(findGlDropdown().props('text')).toBe('Last updated'); - expect(findGlButton().props('icon')).toBe('sort-lowest'); - expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Ascending'); + expect(findGlSorting().props()).toMatchObject({ + sortBy: 2, + isAscending: true, + }); }); it('does not sync sort values when syncFilterAndSort=false', async () => { await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: false }); - expect(findGlDropdown().props('text')).toBe('Created date'); - expect(findGlButton().props('icon')).toBe('sort-highest'); - expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Descending'); + expect(findGlSorting().props()).toMatchObject({ + sortBy: 1, + isAscending: false, + }); + }); + + it('does not sync sort values when initialSortBy is unset', async () => { + // Give initialSort some value which changes the current sort option... + await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true }); + + // ... Read the new sort options... + const { sortBy, isAscending } = findGlSorting().props(); + + // ... Then *unset* initialSortBy... + await wrapper.setProps({ initialSortBy: undefined }); + + // ... The sort options should not have changed. + expect(findGlSorting().props()).toMatchObject({ sortBy, isAscending }); }); }); }); 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 88618de6979..1d6834a5604 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 @@ -156,9 +156,12 @@ describe('BaseToken', () => { it('uses last item in list when value is an array', () => { const mockGetActiveTokenValue = jest.fn(); + const config = { ...mockConfig, multiSelect: true }; + wrapper = createComponent({ props: { - value: { data: mockLabels.map((l) => l.title) }, + config, + value: { data: mockLabels.map((l) => l.title), operator: '||' }, suggestions: mockLabels, getActiveTokenValue: mockGetActiveTokenValue, }, @@ -409,8 +412,9 @@ describe('BaseToken', () => { }); it('emits token-selected event when groupMultiSelectTokens: true', () => { + const config = { ...mockConfig, multiSelect: true }; wrapper = createComponent({ - props: { suggestions: mockLabels }, + props: { suggestions: mockLabels, config, value: { operator: '||' } }, groupMultiSelectTokens: true, }); @@ -419,9 +423,10 @@ describe('BaseToken', () => { expect(wrapper.emitted('token-selected')).toEqual([[mockTokenValue.title]]); }); - it('does not emit token-selected event when groupMultiSelectTokens: true', () => { + it('does not emit token-selected event when groupMultiSelectTokens: false', () => { + const config = { ...mockConfig, multiSelect: true }; wrapper = createComponent({ - props: { suggestions: mockLabels }, + props: { suggestions: mockLabels, config, value: { operator: '||' } }, groupMultiSelectTokens: false, }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js index 56a59790210..34d0c7f0566 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js @@ -42,7 +42,7 @@ describe('DateToken', () => { findDatepicker().vm.$emit('close'); expect(findGlFilteredSearchToken().emitted()).toEqual({ - complete: [[]], + complete: [['2014-10-13']], select: [['2014-10-13']], }); }); 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 36e82b39df4..ee54fb5b941 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 @@ -5,15 +5,12 @@ import { GlDropdownDivider, } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; 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 '~/alert'; -import axios from '~/lib/utils/axios_utils'; import { sortMilestonesByDueDate } from '~/milestones/utils'; - import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql'; import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; @@ -70,7 +67,6 @@ function createComponent(options = {}) { } describe('MilestoneToken', () => { - let mock; let wrapper; const findBaseToken = () => wrapper.findComponent(BaseToken); @@ -80,14 +76,9 @@ describe('MilestoneToken', () => { }; beforeEach(() => { - mock = new MockAdapter(axios); wrapper = createComponent(); }); - afterEach(() => { - mock.restore(); - }); - describe('methods', () => { describe('fetchMilestones', () => { it('sets loading state', async () => { 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 4462d1bfaf5..decf843091e 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 @@ -313,11 +313,11 @@ describe('UserToken', () => { describe('multiSelect', () => { it('renders check icons in suggestions when multiSelect is true', async () => { wrapper = createComponent({ - value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' }, + value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '||' }, data: { users: mockUsers, }, - config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers }, + config: { ...mockAuthorToken, multiSelect: true }, active: true, stubs: { Portal: true }, groupMultiSelectTokens: true, @@ -327,18 +327,17 @@ describe('UserToken', () => { const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion); - expect(findIconAtSuggestion(1).exists()).toBe(false); - expect(findIconAtSuggestion(2).props('name')).toBe('check'); - expect(findIconAtSuggestion(3).props('name')).toBe('check'); + expect(findIconAtSuggestion(0).props('name')).toBe('check'); + expect(findIconAtSuggestion(1).props('name')).toBe('check'); + expect(findIconAtSuggestion(2).exists()).toBe(false); // test for left padding on unchecked items (so alignment is correct) - expect(findIconAtSuggestion(4).exists()).toBe(false); - expect(suggestions.at(4).find('.gl-pl-6').exists()).toBe(true); + expect(suggestions.at(2).find('.gl-pl-6').exists()).toBe(true); }); it('renders multiple users when multiSelect is true', async () => { wrapper = createComponent({ - value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' }, + value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '||' }, data: { users: mockUsers, }, @@ -363,7 +362,7 @@ describe('UserToken', () => { it('adds new user to multi-select-values', () => { wrapper = createComponent({ - value: { data: [mockUsers[0].username], operator: '=' }, + value: { data: [mockUsers[0].username], operator: '||' }, data: { users: mockUsers, }, @@ -383,7 +382,7 @@ describe('UserToken', () => { it('removes existing user from array', () => { const initialUsers = [mockUsers[0].username, mockUsers[1].username]; wrapper = createComponent({ - value: { data: initialUsers, operator: '=' }, + value: { data: initialUsers, operator: '||' }, data: { users: mockUsers, }, @@ -399,7 +398,7 @@ describe('UserToken', () => { it('clears input field after token selected', () => { wrapper = createComponent({ - value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' }, + value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '||' }, data: { users: mockUsers, }, @@ -410,7 +409,7 @@ describe('UserToken', () => { findBaseToken().vm.$emit('token-selected', 'test'); - expect(wrapper.emitted('input')).toEqual([[{ operator: '=', data: '' }]]); + expect(wrapper.emitted('input')).toEqual([[{ operator: '||', data: '' }]]); }); }); diff --git a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js deleted file mode 100644 index f69a883ee4d..00000000000 --- a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import { nextTick } from 'vue'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; - -const SLOT_1 = { - slotKey: 'slot-1', - title: 'Hello 1', -}; -const SLOT_2 = { - slotKey: 'slot-2', - title: 'Hello 2', -}; - -describe('~/vue_shared/components/keep_alive_slots.vue', () => { - let wrapper; - - const createSlotContent = ({ slotKey, title }) => ` - <div data-testid="slot-child" data-slot-id="${slotKey}"> - <h1>${title}</h1> - <input type="text" /> - </div> - `; - const createComponent = (props = {}) => { - wrapper = mountExtended(KeepAliveSlots, { - propsData: props, - slots: { - [SLOT_1.slotKey]: createSlotContent(SLOT_1), - [SLOT_2.slotKey]: createSlotContent(SLOT_2), - }, - }); - }; - - const findRenderedSlots = () => - wrapper.findAllByTestId('slot-child').wrappers.map((x) => ({ - title: x.find('h1').text(), - inputValue: x.find('input').element.value, - isVisible: x.isVisible(), - })); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('doesnt show anything', () => { - expect(findRenderedSlots()).toEqual([]); - }); - - describe('when slotKey is changed', () => { - beforeEach(async () => { - wrapper.setProps({ slotKey: SLOT_1.slotKey }); - await nextTick(); - }); - - it('shows slot', () => { - expect(findRenderedSlots()).toEqual([ - { - title: SLOT_1.title, - isVisible: true, - inputValue: '', - }, - ]); - }); - - it('hides everything when slotKey cannot be found', async () => { - wrapper.setProps({ slotKey: '' }); - await nextTick(); - - expect(findRenderedSlots()).toEqual([ - { - title: SLOT_1.title, - isVisible: false, - inputValue: '', - }, - ]); - }); - - describe('when user intreracts then slotKey changes again', () => { - beforeEach(async () => { - wrapper.find('input').setValue('TEST'); - wrapper.setProps({ slotKey: SLOT_2.slotKey }); - await nextTick(); - }); - - it('keeps first slot alive but hidden', () => { - expect(findRenderedSlots()).toEqual([ - { - title: SLOT_1.title, - isVisible: false, - inputValue: 'TEST', - }, - { - title: SLOT_2.title, - isVisible: true, - inputValue: '', - }, - ]); - }); - }); - }); - }); - - describe('initialized with slotKey', () => { - beforeEach(() => { - createComponent({ slotKey: SLOT_2.slotKey }); - }); - - it('shows slot', () => { - expect(findRenderedSlots()).toEqual([ - { - title: SLOT_2.title, - isVisible: true, - inputValue: '', - }, - ]); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/list_selector/deploy_key_item_spec.js b/spec/frontend/vue_shared/components/list_selector/deploy_key_item_spec.js new file mode 100644 index 00000000000..96be5b345a1 --- /dev/null +++ b/spec/frontend/vue_shared/components/list_selector/deploy_key_item_spec.js @@ -0,0 +1,61 @@ +import { GlIcon, GlButton } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import DeployKeyItem from '~/vue_shared/components/list_selector/deploy_key_item.vue'; + +describe('DeployKeyItem spec', () => { + let wrapper; + + const MOCK_DATA = { title: 'Some key', owner: 'root', id: '123' }; + + const createComponent = (props) => { + wrapper = shallowMountExtended(DeployKeyItem, { + propsData: { + data: MOCK_DATA, + ...props, + }, + }); + }; + + const findIcon = () => wrapper.findComponent(GlIcon); + const findDeleteButton = () => wrapper.findComponent(GlButton); + const findWrapper = () => wrapper.findByTestId('deploy-key-wrapper'); + + beforeEach(() => createComponent()); + + it('renders a key icon component', () => { + expect(findIcon().props('name')).toBe('key'); + }); + + it('renders a title and username', () => { + expect(wrapper.text()).toContain('Some key'); + expect(wrapper.text()).toContain('@root'); + }); + + it('does not render a delete button by default', () => { + expect(findDeleteButton().exists()).toBe(false); + }); + + it('emits a select event when the wrapper is clicked', () => { + findWrapper().trigger('click'); + + expect(wrapper.emitted('select')).toEqual([[MOCK_DATA.id]]); + }); + + describe('Delete button', () => { + beforeEach(() => createComponent({ canDelete: true })); + + it('renders a delete button', () => { + expect(findDeleteButton().exists()).toBe(true); + expect(findDeleteButton().props('icon')).toBe('remove'); + }); + + it('emits a delete event if the delete button is clicked', () => { + const stopPropagation = jest.fn(); + + findDeleteButton().vm.$emit('click', { stopPropagation }); + + expect(stopPropagation).toHaveBeenCalled(); + expect(wrapper.emitted('delete')).toEqual([[MOCK_DATA.id]]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/list_selector/index_spec.js b/spec/frontend/vue_shared/components/list_selector/index_spec.js index 11e64a91eb0..6de9a77582c 100644 --- a/spec/frontend/vue_shared/components/list_selector/index_spec.js +++ b/spec/frontend/vue_shared/components/list_selector/index_spec.js @@ -7,6 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import ListSelector from '~/vue_shared/components/list_selector/index.vue'; import UserItem from '~/vue_shared/components/list_selector/user_item.vue'; import GroupItem from '~/vue_shared/components/list_selector/group_item.vue'; +import DeployKeyItem from '~/vue_shared/components/list_selector/deploy_key_item.vue'; import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -20,18 +21,21 @@ describe('List Selector spec', () => { let fakeApollo; const USERS_MOCK_PROPS = { - title: 'Users', projectPath: 'some/project/path', groupPath: 'some/group/path', type: 'users', }; const GROUPS_MOCK_PROPS = { - title: 'Groups', projectPath: 'some/project/path', type: 'groups', }; + const DEPLOY_KEYS_MOCK_PROPS = { + projectPath: 'some/project/path', + type: 'deployKeys', + }; + const groupsAutocompleteQuerySuccess = jest.fn().mockResolvedValue(GROUPS_RESPONSE_MOCK); const createComponent = async (props) => { @@ -56,6 +60,7 @@ describe('List Selector spec', () => { const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const findAllUserComponents = () => wrapper.findAllComponents(UserItem); const findAllGroupComponents = () => wrapper.findAllComponents(GroupItem); + const findAllDeployKeyComponents = () => wrapper.findAllComponents(DeployKeyItem); beforeEach(() => { jest.spyOn(Api, 'projectUsers').mockResolvedValue(USERS_RESPONSE_MOCK); @@ -254,4 +259,46 @@ describe('List Selector spec', () => { }); }); }); + + describe('Deploy keys type', () => { + beforeEach(() => createComponent(DEPLOY_KEYS_MOCK_PROPS)); + + it('renders a correct title', () => { + expect(findTitle().exists()).toBe(true); + expect(findTitle().text()).toContain('Deploy keys'); + }); + + it('renders the correct icon', () => { + expect(findIcon().props('name')).toBe('key'); + }); + + describe('selected items', () => { + const selectedKey = { title: 'MyKey', owner: 'peter', id: '123' }; + const selectedItems = [selectedKey]; + beforeEach(() => createComponent({ ...DEPLOY_KEYS_MOCK_PROPS, selectedItems })); + + it('renders a heading with the total selected items', () => { + expect(findTitle().text()).toContain('Deploy keys'); + expect(findTitle().text()).toContain('1'); + }); + + it('renders a deploy key component for each selected item', () => { + expect(findAllDeployKeyComponents().length).toBe(selectedItems.length); + expect(findAllDeployKeyComponents().at(0).props()).toMatchObject({ + data: selectedKey, + canDelete: true, + }); + }); + + it('emits a delete event when a delete event is emitted from the deploy key component', () => { + const id = '123'; + findAllDeployKeyComponents().at(0).vm.$emit('delete', id); + + expect(wrapper.emitted('delete')).toEqual([[id]]); + }); + + // TODO - add a test for the select event once we have API integration + // https://gitlab.com/gitlab-org/gitlab/-/issues/432494 + }); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 40875ed5dbc..57f6d751efd 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -82,6 +82,14 @@ describe('Markdown field header component', () => { }); }); + it('attach file button should have data-button-type attribute', () => { + const attachButton = findToolbarButtonByProp('icon', 'paperclip'); + + // Used for dropzone_input.js as `clickable` property + // to prevent triggers upload file by clicking on the edge of textarea + expect(attachButton.attributes('data-button-type')).toBe('attach-file'); + }); + it('hides markdown preview when previewMarkdown is false', () => { expect(findPreviewToggle().text()).toBe('Preview'); }); 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 544466a22ca..626b1df5474 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 @@ -43,7 +43,7 @@ describe('Metrics tab store actions', () => { it('should call success action when fetching metric images', () => { service.getMetricImages.mockImplementation(() => Promise.resolve(fileList)); - testAction(actions.fetchImages, null, state, [ + return testAction(actions.fetchImages, null, state, [ { type: types.REQUEST_METRIC_IMAGES }, { type: types.RECEIVE_METRIC_IMAGES_SUCCESS, @@ -80,7 +80,7 @@ describe('Metrics tab store actions', () => { it('should call success action when uploading an image', () => { service.uploadMetricImage.mockImplementation(() => Promise.resolve(fileList[0])); - testAction(actions.uploadImage, payload, state, [ + return testAction(actions.uploadImage, payload, state, [ { type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_SUCCESS, @@ -112,7 +112,7 @@ describe('Metrics tab store actions', () => { it('should call success action when updating an image', () => { service.updateMetricImage.mockImplementation(() => Promise.resolve()); - testAction(actions.updateImage, payload, state, [ + return testAction(actions.updateImage, payload, state, [ { type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPDATE_SUCCESS, @@ -140,7 +140,7 @@ describe('Metrics tab store actions', () => { it('should call success action when deleting an image', () => { service.deleteMetricImage.mockImplementation(() => Promise.resolve()); - testAction(actions.deleteImage, payload, state, [ + return testAction(actions.deleteImage, payload, state, [ { type: types.RECEIVE_METRIC_DELETE_SUCCESS, payload, @@ -151,7 +151,7 @@ describe('Metrics tab store actions', () => { describe('initial data', () => { it('should set the initial data correctly', () => { - testAction(actions.setInitialData, initialData, state, [ + return testAction(actions.setInitialData, initialData, state, [ { type: types.SET_INITIAL_DATA, payload: initialData }, ]); }); 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 7efc0e162b8..a67276ac64a 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 @@ -11,6 +11,7 @@ import searchProjectsWithinGroupQuery from '~/issues/list/queries/search_project import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import { stubComponent } from 'helpers/stub_component'; import { emptySearchProjectsQueryResponse, emptySearchProjectsWithinGroupQueryResponse, @@ -42,6 +43,7 @@ describe('NewResourceDropdown component', () => { queryResponse = searchProjectsQueryResponse, mountFn = shallowMount, propsData = {}, + stubs = {}, } = {}) => { const requestHandlers = [[query, jest.fn().mockResolvedValue(queryResponse)]]; const apolloProvider = createMockApollo(requestHandlers); @@ -49,6 +51,9 @@ describe('NewResourceDropdown component', () => { wrapper = mountFn(NewResourceDropdown, { apolloProvider, propsData, + stubs: { + ...stubs, + }, }); }; @@ -81,13 +86,18 @@ describe('NewResourceDropdown component', () => { }); it('focuses on input when dropdown is shown', async () => { - mountComponent({ mountFn: mount }); - - const inputSpy = jest.spyOn(findInput().vm, 'focusInput'); + const inputMock = jest.fn(); + mountComponent({ + stubs: { + GlSearchBoxByType: stubComponent(GlSearchBoxByType, { + methods: { focusInput: inputMock }, + }), + }, + }); await showDropdown(); - expect(inputSpy).toHaveBeenCalledTimes(1); + expect(inputMock).toHaveBeenCalledTimes(1); }); describe.each` diff --git a/spec/frontend/vue_shared/components/number_to_human_size/number_to_human_size_spec.js b/spec/frontend/vue_shared/components/number_to_human_size/number_to_human_size_spec.js new file mode 100644 index 00000000000..6dd22211c96 --- /dev/null +++ b/spec/frontend/vue_shared/components/number_to_human_size/number_to_human_size_spec.js @@ -0,0 +1,47 @@ +import { shallowMount } from '@vue/test-utils'; +import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; + +describe('NumberToHumanSize', () => { + /** @type {import('@vue/test-utils').Wrapper} */ + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(NumberToHumanSize, { + propsData: { + ...props, + }, + }); + }; + + it('formats the value', () => { + const value = 1024; + createComponent({ value }); + + const expectedValue = numberToHumanSize(value, 1); + expect(wrapper.text()).toBe(expectedValue); + }); + + it('handles number of fraction digits', () => { + const value = 1024 + 254; + const fractionDigits = 2; + createComponent({ value, fractionDigits }); + + const expectedValue = numberToHumanSize(value, fractionDigits); + expect(wrapper.text()).toBe(expectedValue); + }); + + describe('plain-zero', () => { + it('hides label for zero values', () => { + createComponent({ value: 0, plainZero: true }); + expect(wrapper.text()).toBe('0'); + }); + + it('shows text for non-zero values', () => { + const value = 163; + const expectedValue = numberToHumanSize(value, 1); + createComponent({ value, plainZero: true }); + expect(wrapper.text()).toBe(expectedValue); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js index c7b2363026a..cd18058abec 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js @@ -62,6 +62,7 @@ describe('Chunk component', () => { it('renders highlighted content', () => { expect(findContent().text()).toBe(CHUNK_2.highlightedContent); + expect(findContent().attributes('style')).toBe('margin-left: 96px;'); }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js index 49e3083f8ed..c84a39274f8 100644 --- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js @@ -6,6 +6,7 @@ import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/ jest.mock('highlight.js/lib/core', () => ({ highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }), registerLanguage: jest.fn(), + getLanguage: jest.fn(), })); jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({ @@ -28,11 +29,37 @@ describe('Highlight utility', () => { expect(registerPlugins).toHaveBeenCalled(); }); + describe('sub-languages', () => { + const languageDefinition = { + subLanguage: 'xml', + contains: [{ subLanguage: 'javascript' }, { subLanguage: 'typescript' }], + }; + + beforeEach(async () => { + jest.spyOn(hljs, 'getLanguage').mockReturnValue(languageDefinition); + await highlight(fileType, rawContent, language); + }); + + it('registers the primary sub-language', () => { + expect(hljs.registerLanguage).toHaveBeenCalledWith( + languageDefinition.subLanguage, + expect.any(Function), + ); + }); + + it.each(languageDefinition.contains)( + 'registers the rest of the sub-languages', + ({ subLanguage }) => { + expect(hljs.registerLanguage).toHaveBeenCalledWith(subLanguage, expect.any(Function)); + }, + ); + }); + it('highlights the content', () => { expect(hljs.highlight).toHaveBeenCalledWith(rawContent, { language }); }); - it('splits the content into chunks', () => { + it('splits the content into chunks', async () => { const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code const chunks = [ @@ -52,7 +79,7 @@ describe('Highlight utility', () => { }, ]; - expect(highlight(fileType, contentArray.join(NEWLINE), language)).toEqual( + expect(await highlight(fileType, contentArray.join(NEWLINE), language)).toEqual( expect.arrayContaining(chunks), ); }); @@ -71,7 +98,7 @@ describe('unsupported languages', () => { expect(hljs.highlight).not.toHaveBeenCalled(); }); - it('does not return a result', () => { - expect(highlight(fileType, rawContent, unsupportedLanguage)).toBe(undefined); + it('does not return a result', async () => { + expect(await highlight(fileType, rawContent, unsupportedLanguage)).toBe(undefined); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js index cfff3a15b77..c98f945fc54 100644 --- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js +++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js @@ -79,6 +79,7 @@ export const BLAME_DATA_QUERY_RESPONSE_MOCK = { titleHtml: 'Upload New File', message: 'Upload New File', authoredDate: '2022-10-31T10:38:30+00:00', + authorName: 'Peter', authorGravatar: 'path/to/gravatar', webPath: '/commit/1234', author: {}, diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js index ee7164515f6..86dc9afaacc 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js @@ -1,11 +1,15 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { setHTMLFixture } from 'helpers/fixtures'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_new.vue'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue'; -import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants'; +import { + EVENT_ACTION, + EVENT_LABEL_VIEWER, + CODEOWNERS_FILE_NAME, +} from '~/vue_shared/components/source_viewer/constants'; import Tracking from '~/tracking'; import LineHighlighter from '~/blob/line_highlighter'; import addBlobLinksTracking from '~/blob/blob_links_tracking'; @@ -13,6 +17,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import blameDataQuery from '~/vue_shared/components/source_viewer/queries/blame_data.query.graphql'; import Blame from '~/vue_shared/components/source_viewer/components/blame_info.vue'; import * as utils from '~/vue_shared/components/source_viewer/utils'; +import CodeownersValidation from 'ee_component/blob/components/codeowners_validation.vue'; import { BLOB_DATA_MOCK, @@ -43,16 +48,17 @@ describe('Source Viewer component', () => { const blameInfo = BLAME_DATA_QUERY_RESPONSE_MOCK.data.project.repository.blobs.nodes[0].blame.groups; - const createComponent = ({ showBlame = true } = {}) => { + const createComponent = ({ showBlame = true, blob = {} } = {}) => { fakeApollo = createMockApollo([[blameDataQuery, blameDataQueryHandlerSuccess]]); wrapper = shallowMountExtended(SourceViewer, { apolloProvider: fakeApollo, mocks: { $route: { hash } }, propsData: { - blob: BLOB_DATA_MOCK, + blob: { ...blob, ...BLOB_DATA_MOCK }, chunks: CHUNKS_MOCK, projectPath: 'test', + currentRef: 'main', showBlame, }, }); @@ -111,22 +117,18 @@ describe('Source Viewer component', () => { }); it('calls the query only once per chunk', async () => { - jest.spyOn(wrapper.vm.$apollo, 'query'); - // We trigger the `appear` event multiple times here in order to simulate the user scrolling past the chunk more than once. // In this scenario we only want to query the backend once. await triggerChunkAppear(); await triggerChunkAppear(); - expect(wrapper.vm.$apollo.query).toHaveBeenCalledTimes(1); + expect(blameDataQueryHandlerSuccess).toHaveBeenCalledTimes(1); }); it('requests blame information for overlapping chunk', async () => { - jest.spyOn(wrapper.vm.$apollo, 'query'); - await triggerChunkAppear(1); - expect(wrapper.vm.$apollo.query).toHaveBeenCalledTimes(2); + expect(blameDataQueryHandlerSuccess).toHaveBeenCalledTimes(2); expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith( expect.objectContaining({ fromLine: 71, toLine: 110 }), ); @@ -156,4 +158,20 @@ describe('Source Viewer component', () => { expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash); }); }); + + describe('Codeowners validation', () => { + const findCodeownersValidation = () => wrapper.findComponent(CodeownersValidation); + + it('does not render codeowners validation when file is not CODEOWNERS', async () => { + await createComponent(); + await nextTick(); + expect(findCodeownersValidation().exists()).toBe(false); + }); + + it('renders codeowners validation when file is CODEOWNERS', async () => { + await createComponent({ blob: { name: CODEOWNERS_FILE_NAME } }); + await nextTick(); + expect(findCodeownersValidation().exists()).toBe(true); + }); + }); }); 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 41cf1d2b2e8..21c58d662e3 100644 --- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js @@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils'; import { GlTruncate } from '@gitlab/ui'; import timezoneMock from 'timezone-mock'; -import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; -import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/constants'; +import { getTimeago } from '~/lib/utils/datetime_utility'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/locale_dateformat'; describe('Time ago with tooltip component', () => { let vm; @@ -33,7 +33,7 @@ describe('Time ago with tooltip component', () => { it('should render timeago with a bootstrap tooltip', () => { buildVm(); - expect(vm.attributes('title')).toEqual(formatDate(timestamp)); + expect(vm.attributes('title')).toEqual('May 8, 2017 at 2:57:39 PM GMT'); expect(vm.text()).toEqual(timeAgoTimestamp); }); diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js deleted file mode 100644 index 95f557b10c1..00000000000 --- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import { mount } from '@vue/test-utils'; -import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue'; - -const TestComponent = { - inject: ['vuexModule'], - template: `<div data-testid="vuexModule">{{ vuexModule }}</div> `, -}; - -const TEST_VUEX_MODULE = 'testVuexModule'; - -describe('~/vue_shared/components/vuex_module_provider', () => { - let wrapper; - - const findProvidedVuexModule = () => wrapper.find('[data-testid="vuexModule"]').text(); - - const createComponent = (extraParams = {}) => { - wrapper = mount(VuexModuleProvider, { - propsData: { - vuexModule: TEST_VUEX_MODULE, - }, - slots: { - default: TestComponent, - }, - ...extraParams, - }); - }; - - it('provides "vuexModule" set from prop', () => { - createComponent(); - expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE); - }); - - it('provides "vuexModel" set from "vuex-module" prop when using @vue/compat', () => { - createComponent({ - propsData: { 'vuex-module': TEST_VUEX_MODULE }, - }); - expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE); - }); -}); diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js index fc69e884258..8b4a68e394a 100644 --- a/spec/frontend/vue_shared/directives/track_event_spec.js +++ b/spec/frontend/vue_shared/directives/track_event_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Tracking from '~/tracking'; import TrackEvent from '~/vue_shared/directives/track_event'; @@ -10,34 +10,53 @@ describe('TrackEvent directive', () => { const clickButton = () => wrapper.find('button').trigger('click'); - const createComponent = (trackingOptions) => - Vue.component('DummyElement', { - directives: { - TrackEvent, + const DummyTrackComponent = Vue.component('DummyTrackComponent', { + directives: { + TrackEvent, + }, + props: { + category: { + type: String, + required: false, + default: '', }, - data() { - return { - trackingOptions, - }; + action: { + type: String, + required: false, + default: '', }, - template: '<button v-track-event="trackingOptions"></button>', - }); + label: { + type: String, + required: false, + default: '', + }, + }, + template: '<button v-track-event="{ category, action, label }"></button>', + }); - const mountComponent = (trackingOptions) => shallowMount(createComponent(trackingOptions)); + const mountComponent = ({ propsData = {} } = {}) => { + wrapper = shallowMount(DummyTrackComponent, { + propsData, + }); + }; it('does not track the event if required arguments are not provided', () => { - wrapper = mountComponent(); + mountComponent(); clickButton(); expect(Tracking.event).not.toHaveBeenCalled(); }); - it('tracks event on click if tracking info provided', () => { - wrapper = mountComponent({ - category: 'Tracking', - action: 'click_trackable_btn', - label: 'Trackable Info', + it('tracks event on click if tracking info provided', async () => { + mountComponent({ + propsData: { + category: 'Tracking', + action: 'click_trackable_btn', + label: 'Trackable Info', + }, }); + + await nextTick(); clickButton(); expect(Tracking.event).toHaveBeenCalledWith('Tracking', 'click_trackable_btn', { 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 1a490359040..94234a03664 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 @@ -16,6 +16,7 @@ const fullPath = '/full-path'; const labelsFilterBasePath = '/labels-filter-base-path'; const initialLabels = []; const issuableType = 'issue'; +const issuableSupportsLockOnMerge = false; const labelType = WORKSPACE_PROJECT; const variant = VARIANT_EMBEDDED; const workspaceType = WORKSPACE_PROJECT; @@ -36,6 +37,7 @@ describe('IssuableLabelSelector', () => { labelsFilterBasePath, initialLabels, issuableType, + issuableSupportsLockOnMerge, labelType, variant, workspaceType, 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 47da111b604..98a87ddbcce 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 @@ -6,6 +6,7 @@ import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vu import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import IssuableAssignees from '~/issuable/components/issue_assignees.vue'; +import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat'; import { mockIssuable, mockRegularLabel } from '../mock_data'; const createComponent = ({ @@ -168,15 +169,20 @@ describe('IssuableItem', () => { it('returns timestamp based on `issuable.updatedAt` when the issue is open', () => { wrapper = createComponent(); - expect(findTimestampWrapper().attributes('title')).toBe('Sep 10, 2020 11:41am UTC'); + expect(findTimestampWrapper().attributes('title')).toBe( + localeDateFormat.asDateTimeFull.format(mockIssuable.updatedAt), + ); }); it('returns timestamp based on `issuable.closedAt` when the issue is closed', () => { + const closedAt = '2020-06-18T11:30:00Z'; wrapper = createComponent({ - issuable: { ...mockIssuable, closedAt: '2020-06-18T11:30:00Z', state: 'closed' }, + issuable: { ...mockIssuable, closedAt, state: 'closed' }, }); - expect(findTimestampWrapper().attributes('title')).toBe('Jun 18, 2020 11:30am UTC'); + expect(findTimestampWrapper().attributes('title')).toBe( + localeDateFormat.asDateTimeFull.format(closedAt), + ); }); it('returns timestamp based on `issuable.updatedAt` when the issue is closed but `issuable.closedAt` is undefined', () => { @@ -184,7 +190,9 @@ describe('IssuableItem', () => { issuable: { ...mockIssuable, closedAt: undefined, state: 'closed' }, }); - expect(findTimestampWrapper().attributes('title')).toBe('Sep 10, 2020 11:41am UTC'); + expect(findTimestampWrapper().attributes('title')).toBe( + localeDateFormat.asDateTimeFull.format(mockIssuable.updatedAt), + ); }); }); @@ -409,7 +417,9 @@ describe('IssuableItem', () => { const createdAtEl = wrapper.find('[data-testid="issuable-created-at"]'); expect(createdAtEl.exists()).toBe(true); - expect(createdAtEl.attributes('title')).toBe('Jun 29, 2020 1:52pm UTC'); + expect(createdAtEl.attributes('title')).toBe( + localeDateFormat.asDateTimeFull.format(mockIssuable.createdAt), + ); expect(createdAtEl.text()).toBe(wrapper.vm.createdAt); }); @@ -535,7 +545,9 @@ describe('IssuableItem', () => { const timestampEl = wrapper.find('[data-testid="issuable-timestamp"]'); - expect(timestampEl.attributes('title')).toBe('Sep 10, 2020 11:41am UTC'); + expect(timestampEl.attributes('title')).toBe( + localeDateFormat.asDateTimeFull.format(mockIssuable.updatedAt), + ); expect(timestampEl.text()).toBe(wrapper.vm.formattedTimestamp); }); @@ -549,13 +561,16 @@ describe('IssuableItem', () => { }); it('renders issuable closedAt info and does not render updatedAt info', () => { + const closedAt = '2022-06-18T11:30:00Z'; wrapper = createComponent({ - issuable: { ...mockIssuable, closedAt: '2022-06-18T11:30:00Z', state: 'closed' }, + issuable: { ...mockIssuable, closedAt, state: 'closed' }, }); const timestampEl = wrapper.find('[data-testid="issuable-timestamp"]'); - expect(timestampEl.attributes('title')).toBe('Jun 18, 2022 11:30am UTC'); + expect(timestampEl.attributes('title')).toBe( + localeDateFormat.asDateTimeFull.format(closedAt), + ); expect(timestampEl.text()).toBe(wrapper.vm.formattedTimestamp); }); }); 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 51aae9b4512..a2a059d5b18 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 @@ -4,6 +4,7 @@ import VueDraggable from 'vuedraggable'; import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; +import { DRAG_DELAY } from '~/sortable/constants'; import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue'; import IssuableListRoot from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; @@ -476,6 +477,11 @@ describe('IssuableListRoot', () => { expect(findIssuableItem().classes()).toContain('gl-cursor-grab'); }); + it('sets delay and delayOnTouchOnly attributes on list', () => { + expect(findVueDraggable().vm.$attrs.delay).toBe(DRAG_DELAY); + expect(findVueDraggable().vm.$attrs.delayOnTouchOnly).toBe(true); + }); + it('emits a "reorder" event when user updates the issue order', () => { const oldIndex = 4; const newIndex = 6; 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 f2509aead77..d5c6ece8cb5 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 @@ -1,3 +1,4 @@ +import { GlButton, GlIcon } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { nextTick } from 'vue'; import Cookies from '~/lib/utils/cookies'; @@ -18,6 +19,10 @@ const createComponent = () => { <button class="js-todo">Todo</button> `, }, + stubs: { + GlButton, + GlIcon, + }, }); }; @@ -62,9 +67,8 @@ describe('IssuableSidebarRoot', () => { const buttonEl = findToggleSidebarButton(); expect(buttonEl.exists()).toBe(true); - expect(buttonEl.attributes('title')).toBe('Toggle sidebar'); - expect(buttonEl.find('span').text()).toBe('Collapse sidebar'); - expect(wrapper.findByTestId('icon-collapse').isVisible()).toBe(true); + expect(buttonEl.attributes('title')).toBe('Collapse sidebar'); + expect(wrapper.findByTestId('chevron-double-lg-right-icon').isVisible()).toBe(true); }); describe('when collapsing the sidebar', () => { @@ -116,12 +120,12 @@ describe('IssuableSidebarRoot', () => { assertPageLayoutClasses({ isExpanded: false }); }); - it('renders sidebar toggle button with text and icon', () => { + it('renders sidebar toggle button with title and icon', () => { const buttonEl = findToggleSidebarButton(); expect(buttonEl.exists()).toBe(true); - expect(buttonEl.attributes('title')).toBe('Toggle sidebar'); - expect(wrapper.findByTestId('icon-expand').isVisible()).toBe(true); + expect(buttonEl.attributes('title')).toBe('Expand sidebar'); + expect(wrapper.findByTestId('chevron-double-lg-left-icon').isVisible()).toBe(true); }); }); 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 109b7732539..716de45f4b4 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 @@ -116,21 +116,23 @@ describe('Experimental new namespace creation app', () => { expect(findLegacyContainer().exists()).toBe(true); }); - describe.each` - featureFlag | isSuperSidebarCollapsed | isToggleVisible - ${true} | ${true} | ${true} - ${true} | ${false} | ${false} - ${false} | ${true} | ${false} - ${false} | ${false} | ${false} - `('Super sidebar toggle', ({ featureFlag, isSuperSidebarCollapsed, isToggleVisible }) => { - beforeEach(() => { - sidebarState.isCollapsed = isSuperSidebarCollapsed; - gon.use_new_navigation = featureFlag; - createComponent(); + describe('SuperSidebarToggle', () => { + describe('when collapsed', () => { + it('shows sidebar toggle', () => { + sidebarState.isCollapsed = true; + createComponent(); + + expect(findSuperSidebarToggle().exists()).toBe(true); + }); }); - it(`${isToggleVisible ? 'is visible' : 'is not visible'}`, () => { - expect(findSuperSidebarToggle().exists()).toBe(isToggleVisible); + describe('when not collapsed', () => { + it('does not show sidebar toggle', () => { + sidebarState.isCollapsed = false; + createComponent(); + + expect(findSuperSidebarToggle().exists()).toBe(false); + }); }); }); @@ -170,17 +172,10 @@ describe('Experimental new namespace creation app', () => { }); describe('top bar', () => { - it('adds "top-bar-fixed" and "container-fluid" classes when new navigation enabled', () => { - gon.use_new_navigation = true; + it('has "top-bar-fixed" and "container-fluid" classes', () => { createComponent(); expect(findTopBar().classes()).toEqual(['top-bar-fixed', 'container-fluid']); }); - - it('does not add classes when new navigation is not enabled', () => { - createComponent(); - - expect(findTopBar().classes()).toEqual([]); - }); }); }); 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 f3d0d66cdd1..2b36344cfa8 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 @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants'; +import { featureToMutationMap } from 'ee_else_ce/security_configuration/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; |